EMLファイル変換ツールにHTML→Markdown変換とファイル名改善を実装した

はじめに

以前開発したEMLファイル変換ツールで、HTMLメールを処理すると大きな問題が発生していました。HTMLタグがそのまま出力されてしまい、非常に読みにくい状態になっていたのです。

今回はこの問題を解決するため、HTMLメールを見やすいMarkdown形式に変換する機能を追加しました。同時に、ファイル名にメールの受信日時を追加する改善も行いました。

HTMLメール処理の課題

従来の処理では、HTMLメールのコンテンツが次のような状態で出力されていました。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
</head>
<body style="height: 100% !important; width: 100% !important;">
<!-- 以下、大量のHTMLタグが続く -->
Code language: HTML, XML (xml)

これではメールの内容が全く読み取れません。HTMLタグを単純に除去するだけでは、構造化された情報が失われてしまいます。

HTML→Markdown変換機能の実装

html2textライブラリの導入

HTMLをMarkdownに変換するため、html2textライブラリを採用しました。このライブラリは、HTMLの構造を保持しながらMarkdown形式に変換できる優れたツールです。

まず、依存関係を追加します。

# setup.pyに追加
install_requires=[
    'tkinterdnd2>=0.3.0',
    'html2text>=2020.1.16',  # 新規追加
],
Code language: PHP (php)

本文抽出機能の改良

従来のextract_body()メソッドを拡張し、HTMLとプレーンテキストを適切に分離処理するよう改良しました。

def extract_body(self, msg):
    """メールの本文を抽出する"""
    body = ""
    html_body = ""
    text_body = ""
    
    if msg.is_multipart():
        for part in msg.walk():
            content_type = part.get_content_type()
            
            if content_type == "text/plain":
                payload = part.get_payload(decode=True)
                if payload:
                    charset = part.get_content_charset() or 'utf-8'
                    try:
                        text_body = payload.decode(charset)
                    except:
                        text_body = payload.decode('utf-8', errors='ignore')
            
            elif content_type == "text/html":
                payload = part.get_payload(decode=True)
                if payload:
                    charset = part.get_content_charset() or 'utf-8'
                    try:
                        html_body = payload.decode(charset)
                    except:
                        html_body = payload.decode('utf-8', errors='ignore')
    
    # HTMLがある場合はMarkdownに変換、なければプレーンテキストを使用
    if html_body:
        body = self.convert_html_to_markdown(html_body)
    elif text_body:
        body = text_body
    else:
        body = "(本文なし)"
    
    return body.strip()
Code language: PHP (php)

この改良により、HTMLコンテンツとプレーンテキストを明確に分離し、適切な処理を選択できるようになりました。

Markdown変換処理の実装

HTMLからMarkdownへの変換処理を担当する新しいメソッドを追加しました。

def convert_html_to_markdown(self, html_content):
    """HTMLをMarkdownに変換する"""
    try:
        # html2textの設定
        h = html2text.HTML2Text()
        h.ignore_links = False  # リンクを保持
        h.ignore_images = False  # 画像を保持
        h.body_width = 0  # 行の折り返しを無効化
        h.protect_links = True  # リンクを保護
        h.wrap_links = False  # リンクの折り返しを無効化
        
        # HTMLをMarkdownに変換
        markdown_content = h.handle(html_content)
        
        # 余分な改行を整理
        markdown_content = self.clean_markdown(markdown_content)
        
        return markdown_content
    
    except Exception as e:
        self.log(f"HTML変換エラー: {str(e)}")
        # エラーの場合は従来のHTMLタグ除去方式にフォールバック
        import re
        cleaned = re.sub(r'<[^>]+>', '', html_content)
        return cleaned.replace('&nbsp;', ' ').replace('&lt;', '<').replace('&gt;', '>')
Code language: PHP (php)

このメソッドでは、html2textライブラリを細かく設定しています。リンクや画像を保持し、行の折り返しを無効化することで、元のHTMLの構造を可能な限り維持します。

エラーが発生した場合は、従来の単純なHTMLタグ除去方式にフォールバックする仕組みも実装しました。これにより、予期しないHTML形式でも処理が継続できます。

Markdown内容の整理

変換されたMarkdownの品質を向上させるため、整理処理も追加しました。

def clean_markdown(self, markdown_content):
    """Markdownの内容を整理する"""
    # 連続する改行を最大2つまでに制限
    import re
    markdown_content = re.sub(r'\n{3,}', '\n\n', markdown_content)
    
    # 先頭と末尾の余分な空白を削除
    markdown_content = markdown_content.strip()
    
    return markdown_content
Code language: PHP (php)

この処理により、見栄えの良いMarkdown出力が得られます。

ファイル名への日時追加機能

メール受信日時の抽出

ファイル名にメールの受信日時を追加することで、時系列での管理を容易にしました。メールヘッダーから日時情報を抽出し、取得できない場合はファイルの作成日時を使用します。

def extract_email_datetime(self, msg, eml_file_path):
    """メールの受信日時を抽出する(不明な場合はファイル作成日時を使用)"""
    try:
        # メールヘッダーから日時を取得
        date_header = msg.get('Date')
        if date_header:
            # RFC2822形式の日時をパース
            from email.utils import parsedate_tz, mktime_tz
            parsed_date = parsedate_tz(date_header)
            if parsed_date:
                timestamp = mktime_tz(parsed_date)
                return datetime.datetime.fromtimestamp(timestamp)
    
    except Exception as e:
        self.log(f"デバッグ: 日時解析エラー = {str(e)}")
    
    # メールヘッダーから取得できない場合は、ファイルの作成日時を使用
    try:
        file_timestamp = os.path.getctime(eml_file_path)
        return datetime.datetime.fromtimestamp(file_timestamp)
    except:
        # それでも失敗した場合は現在時刻を使用
        return datetime.datetime.now()
Code language: PHP (php)

RFC2822形式の日時解析には、Pythonの標準ライブラリであるemail.utilsを使用しました。この方式により、様々な形式の日時文字列を正確に処理できます。

ファイル名形式の改善

日時情報をファイル名の先頭に配置し、時系列での並び替えを自然に行えるようにしました。

def format_datetime_for_filename(self, dt):
    """ファイル名用の日時文字列を生成"""
    return dt.strftime("%Y-%m-%d-%H%M")
Code language: PHP (php)

この形式により、ファイルは自動的に時系列順に並びます。従来の連番も不要になりました。

出力ファイル名の生成

# メール日時を取得
email_datetime = self.extract_email_datetime(msg, eml_file)
datetime_prefix = self.format_datetime_for_filename(email_datetime)

# 出力ファイル名を生成
safe_subject = self.make_safe_filename(subject)
output_filename = f"{datetime_prefix}_{safe_subject}.txt"
Code language: PHP (php)

最終的なファイル名は「2025-05-20-0017_ミーティング要約.txt」のような形式になります。

重複ファイル処理とフォルダ名の改善

重複ファイルのスキップ機能

同じファイルが既に存在する場合は、自動的にスキップする機能を追加しました。

# 同じファイルが既に存在する場合はスキップ
if os.path.exists(output_path):
    self.log(f"  ⚠ スキップ: {output_filename} (既に存在)")
    skip_count += 1
    continue
Code language: PHP (php)

これにより、重複変換を避けて効率的な処理が可能になりました。

フォルダ名の簡素化

出力フォルダ名も改善し、日付のみを含むシンプルな形式に変更しました。

# 従来: 変換済みメール_20250524_143025
# 改善後: 変換済みメール_20250524
timestamp = datetime.datetime.now().strftime("%Y%m%d")
output_dir = os.path.join(first_file_dir, f"変換済みメール_{timestamp}")
Code language: PHP (php)

処理結果の詳細レポート

変換処理の完了時に、成功・スキップ・失敗の件数を分けて表示するようにしました。

# 完了メッセージ
self.log(f"\n変換完了!")
self.log(f"成功: {success_count}個")
self.log(f"スキップ: {skip_count}個")
self.log(f"失敗: {len(self.selected_files) - success_count - skip_count}個")
Code language: PHP (php)

ビルド設定の更新

py2appでのビルド時にhtml2textライブラリが正しく含まれるよう、setup.pyの設定も更新しました。

'includes': [
    # 既存の設定に加えて
    'html2text',
    'html2text.config',
    # その他の依存関係...
],
Code language: PHP (php)

実装結果

これらの改善により、HTMLメールが次のような見やすいMarkdown形式で出力されるようになりました。

**重要なお知らせ: 新システム導入について**

田中 様、いつもお世話になっております。

来月より新しい業務システムを導入いたします。主な変更点は以下の通りです:

- ログイン方法の変更
- 新機能の追加
- セキュリティの強化

詳細については添付の資料をご確認ください。

ご不明な点がございましたら、お気軽にお問い合わせください。

システム管理部

TEL: 03-1234-5678
Email: [support@example.com](mailto:support@example.com)
Code language: CSS (css)

HTMLタグは完全に除去され、リンクやテキスト構造は適切に保持されています。ファイル名も「2025-05-20-1430_新システム導入について.txt」となり、時系列での管理が容易になりました。

まとめ

今回の改善により、EMLファイル変換ツールはHTMLメールを適切に処理できるようになりました。html2textライブラリによるMarkdown変換、メール受信日時を活用したファイル名改善、重複ファイルのスキップ機能を実装することで、実用性が大幅に向上しています。

  1. html2text · PyPI – HTMLをMarkdownに変換するPythonライブラリの公式パッケージページ
  2. html2text GitHub Repository – html2textライブラリの公式GitHubリポジトリと使用方法の詳細
  3. email.utils: Miscellaneous utilities — Python 3.13.3 documentation – RFC2822形式の日時解析に使用したPython標準ライブラリの公式ドキュメント
  4. email.header: Internationalized headers — Python 3.13.3 documentation – MIMEヘッダーのデコードに関するPython標準ライブラリの公式ドキュメント
  5. py2app – Create standalone Mac OS X applications with Python – macOSアプリケーション作成ツールpy2appの公式ドキュメント
  6. tkinterdnd2 · PyPI – ドラッグ&ドロップ機能を実現するtkinterdnd2ライブラリの公式パッケージページ
  7. email.message: Representing an email message — Python 3.13.3 documentation – EMLファイル処理に使用したPython標準ライブラリの公式ドキュメント
  8. html2text Usage Documentation – html2textライブラリの詳細な使用方法とオプション設定の説明
  9. py2app Tutorial – py2appを使ったmacOSアプリケーション作成の詳細なチュートリアル