SEO対策:画像のalt属性をハンドで書き込むスクリプト

<!-- markdown-mode-on --> ## 概要 SEOを考える上で、画像のalt属性内のテキストはかなり重要で、必ず設定することが推奨されています。 しかし、alt属性がなくても画像の表示には問題なく、いままでその必要性を認識していなかったこともあって、画像を貼り付けるたびに面倒なalt属性の設定をずっと避けてきました。 小生のブログはなぜか Google serch console でインデックス登録されないので、登録されることを期待して、貼り付け画像にalt属性を設定しようと思い立ちました。ただ、ブログ記事を検索してalt属性を追加していくのは、想像しただけでもかなりの労力がかかりそうです。これを何とか楽にできないかと、このスクリプトを作りました。<a name="more"></a> <!--more--> ## アプローチと実現策 対象ファイルの特定とか、alt属性の自動生成ロジックといったむつかしい処理は自分(人)でやります。 つまり、書き出したHTMLから、**空のalt属性を探して**、自分で、その画像をブラウザで見て、属性を書き込み、**alt属性付きの更新されたHTMLをつくる**ことを行います。(太字がスクリプトの処理) この時、少し楽をするために、alt属性の候補として見出しを利用します。 言い換えれば、 次の方法で、**ローカル環境でのHTML編集とBloggerへのインポート**を行います。 1. Bloggerから記事のコンテンツをエクスポートする(可能な場合)。 1. ローカルPC上で、以前に議論したようなHTML解析・編集ツール(Pythonスクリプトなど)を使って alt 属性を追加する。 1. 修正後のHTMLをBloggerにインポートする(可能な場合、または手動でコピー&ペーストする)。 ## Pythonスクリプト このスクリプトは、HTMLファイルを読み込み、`<img/>` タグの `alt` 属性が空である場合に、適切な代替テキストを提案しながら更新する処理を行います。具体的な動作は以下の通りです。 ### **スクリプトの処理概要** 1. **HTMLファイルの読み込み** - `input.html` を開き、BeautifulSoupを使って解析します。 - ファイルが存在しない場合は、エラーメッセージを表示して終了します。 2. **見出し情報の取得** - `h2` ~ `h6` の見出しを追跡し、現在の見出し内容を管理します。 - 新しい見出しが見つかった場合、より上位の見出しを保持しつつ、下位の見出しをリセットします。 3. **画像の処理** - `<img/>` タグを順番にチェックし、`alt` 属性がない、または空のものを検出します。 - `blogcard` クラスを持つ `<figure>` 内の画像はスキップします。 - 最も近い見出しを `alt` 属性の候補として提案し、ユーザーが確認・編集できるようにします。 - 入力された `alt` を画像に設定します。 - 処理が進むにつれて、同じ見出しが繰り返される場合は `(2)`, `(3)` のように番号を付与します。 4. **HTMLファイルの保存** - 修正後のHTML構造を `output.html` として保存します。 - 処理した画像の数を表示し、完了を通知します。 5. **追加機能** - `readline` を使用し、コマンドライン上で入力フィールドを事前に埋め込むことで、ユーザーの入力補助を行います。 - ANSIカラーコード(`\033[XXm`)を使用して、ターミナルの出力を見やすく色分けしています。 ### ライブラリ このスクリプトの実現には、クリップボードへのコピーを行うためのライブラリが必要です。`pyperclip` ライブラリを使用しますので、事前にインストールしてください。 ```bash pip install pyperclip ``` ### スクリプト 以下が、ご要望の動作を行う `find_no_alt.py` のスクリプトです。 <pre style="max-height: 30em;"><code class="language-python"> from bs4 import BeautifulSoup, Tag try: import readline # macOS / Linux except ImportError: import pyreadline as readline # Windows (pip install pyreadline3) YELLOW = "\033[93m" CYAN = "\033[96m" GREEN = "\033[92m" RESET = "\033[0m" def input_with_prefill(prompt, text): def hook(): readline.insert_text(text) readline.redisplay() readline.set_pre_input_hook(hook) try: return input(prompt) finally: readline.set_pre_input_hook() def process_images_with_heading_context(input_file_path, output_file_path): try: with open(input_file_path, 'r', encoding='utf-8') as f: html_content = f.read() except FileNotFoundError: print(f"エラー:入力ファイル '{input_file_path}' が見つかりません。") return soup = BeautifulSoup(html_content, 'html.parser') current_headings = {'h2': '', 'h3': '', 'h4': '', 'h5': '', 'h6': ''} heading_counters = {} processed_count = 0 image_index = 1 descendants = soup.body.descendants if soup.body else soup.descendants for tag in descendants: if isinstance(tag, Tag): tag_name = tag.name.lower() if tag_name in current_headings: current_headings[tag_name] = tag.get_text(strip=True) # 下位の見出しをリセット for lower in ['h6', 'h5', 'h4', 'h3', 'h2']: if lower > tag_name: current_headings[lower] = '' elif tag_name == 'img' and ('alt' not in tag.attrs or tag.attrs['alt'].strip() == ''): # <figure class="blogcard..."> 内の画像はスキップする if tag.find_parent('figure', class_=lambda c: c and c.startswith('blogcard')): continue # 最下層の見出しだけを使う suggested_alt = '' for level in ['h6', 'h5', 'h4', 'h3', 'h2']: if current_headings[level]: suggested_alt = current_headings[level] break key = suggested_alt if key not in heading_counters: heading_counters[key] = 1 else: heading_counters[key] += 1 if heading_counters[key] > 1 and suggested_alt: suggested_alt += f" ({heading_counters[key]})" print(f"\n{YELLOW}[Image {image_index}]{RESET}") print(f" src: {CYAN}{tag.get('src')}") print(f"{GREEN} alt:{RESET}", end="") user_input = input_with_prefill(" ", suggested_alt) tag['alt'] = user_input.strip() or suggested_alt processed_count += 1 image_index += 1 with open(output_file_path, 'w', encoding='utf-8') as f: f.write(soup.prettify()) print(f"\n{processed_count} 件の画像に alt を設定しました。出力先: '{output_file_path}'") if __name__ == "__main__": process_images_with_heading_context("input.html", "output.html") </code></pre> ### スクリプト修正 前項のスクリプトで生成された output.html には不必要な改行が多く、コードがとても見にくいHTMLに整形されます。これは、BeautifulSoup の soup.prettify() を使って出力しているからです。prettify() は構造をわかりやすくするためにインデントと改行を大量に挿入します。 元の構成を維持しつつ、適度にインデントや改行を入れるスクリプトに変更します。<svg aria-label="i-gemini" class="blog-logo" height="24" width="64"><use href="#i-gemini"></use></svg><span style="font-size:12px;"> </span>に聞いたら、次のように教えてくれました。そこで、**html5lib**を使ってみます。 <table> <thead> <tr> <th> 方法 </th> <th> 特徴 </th> <th> おすすめ度 </th> </tr> </thead> <tbody> <tr> <th> 方法1(html5lib) </th> <td> 適度な改行と構造保守。読みやすく安全。 </td> <td> ⭐⭐⭐⭐ </td> </tr> <tr> <th> 方法2(lxml) </th> <td> html5lib に近いが少しシンプル </td> <td> ⭐⭐⭐ </td> </tr> <tr> <th> 方法3(prettify後加工) </th> <td> 簡易的に行数減らすが根本的ではない </td> <td> ⭐⭐ </td> </tr> <tr> <th> `str(soup)` </th> <td> 改行なしのベタ出力 </td> <td> ⭐(ベタすぎ) </td> </tr> <tr> <th> `prettify()` </th> <td> 改行多すぎてうるさい </td> <td> ⭐(不快) </td> </tr> </tbody> </table> ✅ <b>方法1:html5lib パーサを使い、soup.encode() で整形出力</b> html5lib を使ってパースした上で、soup.encode() を使うと、元の構造に近く、適度に改行やインデントを含む出力が得られます。特に BeautifulSoup の html.parser よりも柔軟に整形してくれます。 📌 <b>修正内容:</b> 1. BeautifulSoup のパーサを html5lib にする。 1. 出力を soup.encode() に変える。 <pre style="max-height: 30em;"><code class="language-python"> from bs4 import BeautifulSoup, Tag try: import readline # macOS / Linux except ImportError: import pyreadline as readline # Windows (pip install pyreadline3) YELLOW = "\033[93m" CYAN = "\033[96m" GREEN = "\033[92m" RESET = "\033[0m" def input_with_prefill(prompt, text): def hook(): readline.insert_text(text) readline.redisplay() readline.set_pre_input_hook(hook) try: return input(prompt) finally: readline.set_pre_input_hook() def process_images_with_heading_context(input_file_path, output_file_path): try: with open(input_file_path, 'r', encoding='utf-8') as f: html_content = f.read() except FileNotFoundError: print(f"エラー:入力ファイル '{input_file_path}' が見つかりません。") return # html5lib を使用 soup = BeautifulSoup(html_content, 'html5lib') current_headings = {'h2': '', 'h3': '', 'h4': '', 'h5': '', 'h6': ''} heading_counters = {} processed_count = 0 image_index = 1 descendants = soup.body.descendants if soup.body else soup.descendants for tag in descendants: if isinstance(tag, Tag): tag_name = tag.name.lower() if tag_name in current_headings: current_headings[tag_name] = tag.get_text(strip=True) # 下位の見出しをリセット for lower in ['h6', 'h5', 'h4', 'h3', 'h2']: if lower > tag_name: current_headings[lower] = '' elif tag_name == 'img' and ('alt' not in tag.attrs or tag.attrs['alt'].strip() == ''): # <figure class="blogcard..."> 内の画像はスキップする if tag.find_parent('figure', class_=lambda c: c and c.startswith('blogcard')): continue # 最下層の見出しだけを使う suggested_alt = '' for level in ['h6', 'h5', 'h4', 'h3', 'h2']: if current_headings[level]: suggested_alt = current_headings[level] break # ★ 全角スペースを半角に変換 suggested_alt = suggested_alt.replace('\u3000', ' ') key = suggested_alt if key not in heading_counters: heading_counters[key] = 1 else: heading_counters[key] += 1 if heading_counters[key] > 1 and suggested_alt: suggested_alt += f" ({heading_counters[key]})" print(f"\n{YELLOW}[Image {image_index}]{RESET}") print(f" src: {CYAN}{tag.get('src')}") print(f"{GREEN} alt:{RESET}", end="") user_input = input_with_prefill(" ", suggested_alt) tag['alt'] = user_input.strip() or suggested_alt processed_count += 1 image_index += 1 # 整形されたHTMLとして保存(html5lib 使用) with open(output_file_path, 'w', encoding='utf-8') as f: f.write(soup.encode('utf-8').decode('utf-8')) print(f"\n{processed_count} 件の画像に alt を設定しました。出力先: '{output_file_path}'") if __name__ == "__main__": process_images_with_heading_context("input.html", "output.html") </code></pre> ## **このスクリプトの使い方:** このスクリプトは、`input.html` を読み込み、`alt` 属性がないか空の `<img>` タグを一つずつ検出し、その `src` を表示してクリップボードにコピーします。ユーザーが `alt` テキストを入力すると、その内容で `alt` 属性を更新し、次の `<img>` タグを探す処理を繰り返します。すべてのタグが処理されたら、結果を `output.html` に書き出します。 1. 上記のPythonコードを `find_no_alt.py` という名前で保存します。 2. 処理したいHTMLファイルを `input.html` という名前で、`find_no_alt.py` と同じディレクトリに置きます。 3. コマンドプロンプトまたはターミナルを開き、`find_no_alt.py` があるディレクトリに移動します。 4. 以下のコマンドを実行します。 ```bash python find_no_alt.py ``` 5. スクリプトは `input.html` を読み込み、`alt` 属性がない最初の `<img/>` タグを見つけると、その `src` を表示し、クリップボードにコピーします。 6. ターミナルに表示された `src` をブラウザにペーストして画像を確認し、適切な `alt` テキストをターミナルに入力して Enter キーを押します。 7. `alt` テキストを入力すると、スクリプトはその `<img/>` タグの `alt` 属性を更新し、次の `alt` 属性がないか空の `<img/>` タグを探します。 8. この処理を、すべての該当する `<img/>` タグに対して繰り返します。 9. すべてのタグが処理されると、更新されたHTML が `output.html` という名前で保存されます。 **注意点:** * このスクリプトは、HTMLファイルを順番に処理し、見つかった順にユーザーに `alt` テキストの入力を促します。 * 処理を途中で中断した場合、それまでの変更は `output.html` に保存されます。 * 非常に大きなHTMLファイルの場合、処理に時間がかかる可能性があります。 * `pyperclip` ライブラリがインストールされていないと、クリップボードへのコピー機能は動作しません。 このスクリプトが、ご希望のワークフローに沿って `alt` 属性の追加作業を支援できることを願っています。 ## alt属性が空のHTML抽出 個々の記事HTMLにalt属性が`alt=""`な画像があるかどうかを調べるためには、Blogger管理画面の「投稿」から、検索します。 検索欄に「`"alt="""`」と入力して検索します。探したいのは、「`alt=""`」なのですが、`"`は文字の囲み記号なので、「`alt=`」(全てのalt属性)が検索されます。 その対策として、検索文字を「`"alt="""`」とします。 ## 関連リンク </img></img></figure>
Next Post Previous Post