Skip to content
GPT-4oを使ってブログの修復をした話
note記述記事
2024-05-26

個人ブログの画像リンクが崩れて、長年困っていたのです。

個人ブログの画像リンクが壊れた話

結構前からの個人ブログがあります。
大半は放置状態なのです。もう忘れているのですが、2005年からやってたようです。
黒歴史がたっぷり詰まった状態なのですが、そういうのも含めて何を考えていたかを自分が今まで何をしたのか残しておかないと何も残らない状態になりかねないので、未だにほそぼそ継続しています。
少し見応えがあるコンテンツとすれば、旧MacOSを中心としたフロッピーディスク/CDラベル写真のコレクション、現時点は旅bot達の旅アーカイブでしょうか。

https://akibakokoubou.jp/

https://akibakokoubou.jp/categories/floppy-disk-cd-rom-label-collections/

https://akibakokoubou.jp/mi-runner/

でもこのFD/CDラベルのコレクションを中心に画像リンクが崩れて数年困っていたのです。

ブログの遷移と画像リンク破綻

このブログの一番最初はココログだったかな。たしか

ココログ→Yahooブログ→WordPress→Vue+nuxt/content1+ipfsカスタマイズ→VitePress

みたいな遷移だったと思います。内部データ的にはMovableTypeベース独自(ココログ,Yahooブログ)→WordPress→マークダウンかな。引越過程で各ブログツールの独自仕様や仕様変更を経ている訳です。

各ブログツールの引越の理由はそれぞれに理由はあったはずなのですがよく覚えてないです。
たしかYahooブログは閉鎖の話しが出て追い出されて、WordPressは流行りだした静的サイトでコスト削減を考えたとか。
まあ、それなりに量があったコンテンツを変換ツールを自作して移動していったのです。

ところがWordPressからマークダウンへの変換のときに致命的ミスをして、数百の画像ファイルのリンクが崩れたのです。。
これが結構面倒で、各ブログツールでパスの書き方も図版命名の方法も基のURLも異なる。とりわけWordPressが小賢い余計な加工変換をしていて例外ルールが多すぎる。これに自分の加工ツールが中途半端に変換してしまった崩れたリンクが含まれる。。そりゃ数百だから一日に少しずつ直せばいつかは直るでしょうがちょっと途方に暮れてた状態です。。

旧MacOS系のフロッピーやCDのラベルの写真を撮った画像は記録した時期がWordPress運用期だったので丁度変換失敗の影響を直接受けて、そのあたりががっつり崩れているという状況。画像元データのロストはなんとか気づいて保持したのが救いでした。

(余談)独自ブログにした理由

今風に個人ブログを置くならオールインワンの賢いブログシステムがいろいろあるのですが、VitePressを使っているのは

後々TypeScriptでがっつり処理を書けるようにしておきたい

と思ったのです。オールインワンのブログシステムだと結局パブリッシャーが中身を細かく改良していくので、ユーザがカスタマイズしたいと思っても小さなJavaScript+cssくらいまでみたいな感じになってしまいます。
自分としてはがっつりクライアントサイドTypeScript、必要があればサーバーサイドも書けるようにしておきたいと考えて、素材に近いVitePressを選んだ感じです。実際旅記事アーカイブもVue/TypeScriptで書いてますし。

今回旅botの旅記事にもうちょい新機能を入れて、ブログとの連動性で新要素ということも考えているのですが、その段で画像の欠損問題が残っ
てるなとちょっと頭が痛い話しを思い出したのです。

あと技術勉強の意図も結構あります。
丁度独自ブログに変更を検討したときにVue系案件を手伝っていたので、それの勉強も含めてVueで起こしたり、nuxt/content1を使ってみたり、ipfsも興味を持ってつなげてみたのですがまぁ欲張りすぎて余りうまく回らなかった。今はreact案件を手伝ってメリットは理解できたものの、趣味時間でやるならVueくらいの感じが気楽かなと考えて、現在VitePressベースにしています(と、まとめて書いてますがブログシステムの選択は間違い続けたという認識があるので結構悩んでるんですよ。。)
で、この間リンク切れの問題はどうにも出来なかったんです。。

ブログ修復にChatGPT-4oの力を借りる

LLMでプログラム支援という話しも増えてきてるし、ChatGPT-4oも出たことだし、今回はがっつりChatGPTにコードを書いて貰おうと考えました。
GPTはプログラム言語だとpythonが得意とのことだったのでpythonを使ってみます。私はPythonは苦手なのですが、全部書いて貰うならAIが得意な言語が無難でしょう。

とりあえず自分が認識していた一番大きなリンク崩れは、WordPressがスクリーンサイズごとに複数の画像ファイルを合成して、動的なリンク名に置き換えていた問題です。それをGPT-4oになんとか説明します。

次の課題を解決するpythonプログラムを生成してください。あるディレクトリの下にあるマークダウンファイルの中の画像ファイルのリンクの誤りを修正してください。 マークダウンファイルは指定のディレクトリの下に階層をもって配置されています(例 "\\xxxx\2021\03\04\apple-internet-スタータキット\index.md" ) このindex.mdと同じ階層にimagesという名前のフォルダがあり、その中に画像ファイルがあります。(例 "\\xxxx\2021\03\04\apple-internet-スタータキット\images\IMG_20210302_122357_5.jpg") index.mdには次のような画像リンクのタグが入っていますが、ファイルの末尾が間違っていることがあります (例 "![](images/IMG_20210302_122357_5-1024x1014.jpg)" ) マークダウンファイル内のリンク文字列を修正してリンクが合致するように修正してください。

(ルートパス名は書き換えてます)

import os
import re

def correct_image_links(root_dir):
    # 再帰的にすべてのマークダウンファイルを検索
    for subdir, _, files in os.walk(root_dir):
        for file in files:
            if file.endswith('.md'):
                file_path = os.path.join(subdir, file)
                correct_links_in_file(file_path)

def correct_links_in_file(file_path):
    # マークダウンファイルの読み込み
    with open(file_path, 'r', encoding='utf-8') as file:
        content = file.read()

    # 画像リンクを探して修正
    corrected_content = re.sub(r'!\[(.*?)\]\((images/.+?)-\d+x\d+\.(jpg|png|jpeg|gif)\)', correct_link, content)

    # 修正されたコンテンツをファイルに書き戻す
    with open(file_path, 'w', encoding='utf-8') as file:
        file.write(corrected_content)

def correct_link(match):
    alt_text = match.group(1)
    link = match.group(2)
    ext = match.group(3)
    
    # リンクからディレクトリを取得
    dir_path = os.path.dirname(link)

    # ディレクトリ内のファイルリストを取得
    full_dir_path = os.path.join(os.path.dirname(os.path.abspath(file_path)), dir_path)
    files = os.listdir(full_dir_path)
    
    # ベースファイル名を取得
    base_filename = os.path.basename(link).rsplit('-', 1)[0]
    
    # ベースファイル名と一致するファイルを探す
    for file in files:
        if file.startswith(base_filename) and file.endswith(ext):
            correct_filename = file
            break
    else:
        # 一致するファイルが見つからなかった場合は元のリンクを返す
        return match.group(0)
    
    # 新しいリンクを生成
    new_link = f'images/{correct_filename}'
    return f'![{alt_text}]({new_link})'

# メイン処理の実行
root_directory = r'\\xxxx'
correct_image_links(root_directory)

結構がっつりソースを書いてくるね。最近のLLMはすごい!
でもIDEが文法エラーが1箇所指摘してきます。

指摘したら直してきた。

すごいね!
でもドライランになるように修正して試してみると修正すべき問題はそれだけではなくいろいろ出てくる。

もう少し細かく要件を追加します。1. imagesディレクトリに入っている画像ファイルはxxxx-yyyy.拡張子の形で入っています。このため-で区切るのは正しいのですが、同じ形式の複数のファイルが入っていることがあります(例 xxxx-yyyy.jpg ,xxxx-zzzz.jpg) この場合ファイルの実サイズが大きいファイルを選択して修正してください。 2. リンクはすでに正常に修正されていることがあります。(例 "![](images/IMG_20210302_122357_5-1024x1014.jpg)" のリンクに対して、IMG_20210302_122357_5-1024x1014.jpgの画像ファイルが存在することがある) 同じ名前のファイルが存在していてすでに修正する必要がない場合はそのままにしておいてください。

指示を追加

すでに部分的に手作業で修正している部位もあります。ファイルのマッチにしてもすでにWordPress変換済み画像しか残ってない部分もあります。例外だらけです。。

ChatGPT:了解しました。以下のPythonコードでは、指定された要件を考慮して、画像リンクを修正するようにします。…

ソースは省略

感心してしまいます。すぐ手直ししてきました。
このソースも実行してみますがやはりうまくいかないケースがぼろぼろあります。
調べているうちにやりかたを少し変えないといけないと気づく。
コピーすべき画像ファイルも抽出しなおさないといけないケースがある。

処理の仕方を変更します。マークダウンの中のリンク先はhttpsで始まっている場合とimagesで始まっている場合があります。(例 "![](https://i0.wp.com/akibakokoubou.jp/wp-content/uploads/2020/02/IMG\_20200206\_095245\_0.jpg?fit=525%2C546&ssl=1)", "![](images/IMG_20210302_121906_9-1024x1020.jpg)") imagesで始まっている場合はimagesに入っている画像ファイルとファイル名を比較して合致していればそのままにします。違った場合はprint文で報告してください。 httpで始まっている場合はファイル名のみを切り出してそのファイルがimagesに入っている画像ファイルとファイル名を比較して、合致していれば "images/ファイル名.拡張子"の形式にリンクを変更します。合致しない場合はprint文で報告します。合致しない場合はいずれの場合も別途指定するフォルダ("D:\mfuku\Downloads")の中に同じファイルが存在するかを調べます。同じファイルが存在したら見つけた画像ファイルをimagesフォルダ内にコピーしてください。そしてリンクを"images/ファイル名.拡張子"の形式に変更します。

面倒くさい上流SEか顧客になった気分ですね。。。
PGに相談したらイラッとこられるレベルの変更です。。
これを文句言わずに瞬時に修正してくるのはやっぱり助かります。

import os
import re
import shutil

def correct_image_links(root_dir, download_dir):
    # 再帰的にすべてのマークダウンファイルを検索
    for subdir, _, files in os.walk(root_dir):
        for file in files:
            if file.endswith('.md'):
                file_path = os.path.join(subdir, file)
                correct_links_in_file(file_path, download_dir)

def correct_links_in_file(file_path, download_dir):
    # マークダウンファイルの読み込み
    with open(file_path, 'r', encoding='utf-8') as file:
        content = file.read()

    # correct_link関数にfile_pathとdownload_dirを渡すためにpartialを使用
    correct_link_partial = partial(correct_link, file_path=file_path, download_dir=download_dir)

    # 画像リンクを探して修正
    corrected_content = re.sub(r'!\[(.*?)\]\((.*?)\)', correct_link_partial, content)

    # 修正されたコンテンツをファイルに書き戻す
    with open(file_path, 'w', encoding='utf-8') as file:
        file.write(corrected_content)

def correct_link(match, file_path, download_dir):
    alt_text = match.group(1)
    link = match.group(2)
    
    if link.startswith('images/'):
        # ローカル画像リンクの処理
        if not validate_local_image_link(file_path, link):
            print(f'Incorrect link in {file_path}: {link}')
        return match.group(0)  # そのまま返す
    
    elif link.startswith('http://') or link.startswith('https://'):
        # リモート画像リンクの処理
        filename = os.path.basename(link.split('?')[0])  # URLからファイル名を取得
        
        # ローカルimagesフォルダ内に同じファイルが存在するかチェック
        dir_path = os.path.join(os.path.dirname(file_path), 'images')
        full_local_path = os.path.join(dir_path, filename)
        if not os.path.exists(full_local_path):
            # ダウンロードディレクトリ内に同じファイルが存在するかチェック
            download_path = os.path.join(download_dir, filename)
            if os.path.exists(download_path):
                # ファイルをimagesフォルダにコピー
                shutil.copy(download_path, full_local_path)
                new_link = f'images/{filename}'
                return f'![{alt_text}]({new_link})'
            else:
                print(f'File not found in {file_path} or {download_dir}: {filename}')
                return match.group(0)  # リンクはそのまま返す
        else:
            new_link = f'images/{filename}'
            return f'![{alt_text}]({new_link})'
    
    return match.group(0)  # その他の場合はそのまま返す

def validate_local_image_link(file_path, link):
    dir_path = os.path.join(os.path.dirname(file_path), os.path.dirname(link))
    filename = os.path.basename(link)
    full_path = os.path.join(dir_path, filename)
    return os.path.exists(full_path)

# メイン処理の実行
root_directory = r'\\xxxx'
download_directory = r'D:\mfuku\Downloads'
correct_image_links(root_directory, download_directory)

例外的な指示に例外的な指示が重なったような要件をGPT-4oが平気にこなすのはちょっと驚きでした。ここまで理解できるなら人に振らなくてもLLMに振ったほうがよいという気にもなります。

でも結局は日和った

しかし結局どうしたのかというと、最終的に出たソースを自分で一部処理を修正しました。さらに変換後のマークダウンを直接手直しした部分もあります。

なぜ全部GPT-4oに依頼しなかったのかというと

指示する要件文のほうがプログラムを修正するより訳のわからない文になって書く気が起きなくなったため

です。。
先のソースはかなりの部分を修正して変換出力できましたが、それでもかなりの例外が出ました。その例外の指示を書こうとすると、項番を振って、aの場合は、bの場合は…みたいなのを長々書かなければならないというのが見えてきました。
そこまでの仕様書をがっちり書くには、やはりきちんと検討して仕様書を書かないといけない。
でももしそこまで考えた仕様書をGPT-4oが
「よくわかりません。hahaha」
とか返されたら徒労感が半端ないと感じたのです。
であれば、現状のソースを試しながら修正したほうが確実と判断して、ごそごそ修正して置き換えをしました。

仕事で考えたら、ダメSEのやり方ですね。。

でもこれは仕事じゃないんだから自分が一番やりやすい方法で最後の解決まで持って行く

と割り切りました。自分はそうやって渡ってきた人間です。

本当ならAIで以下の作業がやれれば理想的なのでしょう。

ディレクトリ/xxxにブログを構成するマークダウンと画像ファイルがありますが、かなりの部分に画像リンクの崩れが有ります。画像ファイルとして/yyyyを置いています。画像リンクのファイル名と置いている画像ファイル名は類似点がありますが同じではありません。/xxxxの画像リンクを修正するpythonプログラムを書いてください。

顧客要件レベルの言い方ですね。
しかし、これでもGPT-4oは結果を返してきました。

import os
import re
from difflib import get_close_matches

# パスを指定
markdown_dir = '/xxx'
image_dir = '/yyyy'

# ファイル名を取得
markdown_files = [f for f in os.listdir(markdown_dir) if f.endswith('.md')]
image_files = [f for f in os.listdir(image_dir) if re.search(r'\.(png|jpg|jpeg|gif)$', f)]

# 画像リンクを修正する関数
def fix_image_links(markdown_text, image_files):
    def replace_link(match):
        original_link = match.group(1)
        image_name = os.path.basename(original_link)
        closest_match = get_close_matches(image_name, image_files, n=1)
        if closest_match:
            new_link = closest_match[0]
            return f"![image]({new_link})"
        return match.group(0)  # マッチが見つからない場合は元のリンクを保持

    return re.sub(r'!\[.*?\]\((.*?)\)', replace_link, markdown_text)

# マークダウンファイルを読み込み、リンクを修正して保存
for md_file in markdown_files:
    md_path = os.path.join(markdown_dir, md_file)
    with open(md_path, 'r', encoding='utf-8') as f:
        md_content = f.read()

    fixed_content = fix_image_links(md_content, image_files)

    with open(md_path, 'w', encoding='utf-8') as f:
        f.write(fixed_content)

print("画像リンクの修正が完了しました。")

get_close_matches(文字列類似度比較)を使った案を提示してきました。pythonにはこういう関数もあるのか。。
これを基にはじめてもよかったかもしれません。ただここからやったとしても結構難しい問題があります。
ブログシステムを複数に渡ったため、別ファイルが同一のファイル名だったりすることもかなりあったことが分かっているからです。

これって顧客-今風のPGで話ししてやると、次のような話しになるんじゃないかな。

客「ブログの画像リンクが壊れてるんだよ。見てみて直して!」
PG「分かりました!(python使えばなんとかなるだろ)」
… (しばらくやって簡単にはいかないことが分かる)
PG「いや、これは簡単にはいかないですね。時間と工数がかかります。」
客「すぐ出来るって言ったじゃないか!」
PG「いや、そもそもですね…」

寸劇

「あのPG使えねぇ」「あの顧客分かってねぇ」となってお互い不幸って話しになります。
でも間に賢いLLMを置けば、問題を直に解決出来る訳ではなくとも、不幸は回避出来るかもしれない。
案件会議の参加メンバーにLLMを入れるとか、もう現実的になりつつあるのかもしれない。

もちろん今まではここに賢いSEがいて相互解決してお代をいただいていた訳ですから、これがSEの負担軽減なのか権威低下なのかは考え方次第です。

とりあえずは直った。そしてこれから

最終的には、本当にロストした画像以外は直ったはずです。
数年以上悩んでた問題がGPT-4oの支援を使って直ったのはすごく助かった状態です。
LLM支援のプログラミングもどんな感じがいけるかの感触もつかめたし。
今回指示した修正って、論理的/合理的に説明が通る要件ではなく、現実現場によくある例外の積み重ねという内容なので、人に依頼するのもしにくいし、AIにも説明しにくい種類の作業です。
AIはこの手の作業はまだ無理なんじゃないかと思っていたのですが、使い方次第でかなり楽になると感じました。

  • 最後の例のように(実現は困難でも)合理的な見解が得られる

  • 人ともめなくて済む

  • 即座に具体的提案が出てくる

そういう泥臭い作業もAIでかなり楽できそうという感触が得られたのは結構面白かったのです。

せっかくここまで直したので、もっと根っこからブログを直したい気もあるのですが、旅botやAI絵日記もあるし、今までと同じように気が乗ったときだけ手を入れる感じかなと思います。

(初出 note https://note.com/marble_walkers/n/n5b8ec6a65382 )