WordPress
カスタマイズ事例

WORDPRESS CUSTOMIZATION

WordPress移行でつまずいた「日本語ファイル名の画像がインポートされない問題」とその解決法

WordPressサイトの移行や再構築を行う際、「XMLエクスポート → インポート」でメディアを再登録するのはよくある作業です。しかし、今回直面したのは「日本語ファイル名の画像がインポートされない」という、地味ながら非常に厄介な問題でした。

このコラムでは、実際の状況と対応フローを記録として整理し、同じ問題に直面する開発者・運用者への実践的なガイドとしてまとめます。

《問題発覚前》とりあえずたんたんとインポート

クライアントに移行元サイトから投稿XMLをエクスポートしていただきます。その際、メディアも一緒に移行しないと、移行先で画像が not found になってしまうので、100ウェブの以下の別事例にしたがい、メディアも移行できるようなエクスポートXMLを作っていただきます。

【サイト移行】メディア(画像)ファイルを移行する
https://100webdesign.jp/services/wordpress/wp_result/wp_result-24884/

エクスポートしていただいたXMLを移行先でインポートします。
待つこと15分(2000記事くらいありましたからね)、エラー無く平和に終わりました。

うまく移行できたかどうか、数ページチェックしてみます。

・・・と!ここで問題が発覚します。

問題の発端

画像が無いページがちらほらある!

そのページのHTMLを見てみます。
よーく見てみると、どの not found な画像も、ファイル名が日本語じゃないですか!
数ページ調べたらすべてが日本語ファイル名でした。
もう一回インポートをかけてみます。すでに登録されていればスキップされるので、同じファイルをインポートしても大丈夫です。
ところが・・・。

メディア “” はすでに存在しています。

再インポートをかけても、どのメディアも上のメッセージが出てスキップされてしまいます。登録済みですと。
でも、実体ファイルはサーバー内に見当たらず、ブラウザ上でも画像は表示されません。

技術的背景:なぜ日本語ファイル名が問題になるのか

WordPressのインポーター(wordpress-importer)は、インポート対象の画像を wp:attachment_url タグに記載されたURLからダウンロードして登録します。

しかし以下の点が問題となります:

  • 日本語ファイル名はURL上では エンコードされる必要がある
  • そのままインポートすると、WordPressはファイルを保存できてもファイル名を変えて保存してしまう
  • その結果、DBに登録された attachment_url のパスと、実ファイルのパスが不一致となる

 
要するに、別のファイル名で登録されちゃったみたいです。確かに乱数かハッシュ化されたファイル名が結構見当たります。

解決へのアプローチ

今回の対応では、以下の手順で問題を一つひとつ解決していきました。

  1. XML中の wp:attachment_url タグに記載された画像URLを抽出し、日本語ファイル名のみをリストアップ
     
    generate_image_urls_py.py

    import xml.etree.ElementTree as ET
    import re
    import urllib.parse
    
    input_path = "your_export.xml"  # ← WordPressのXMLファイル名に変更
    output_path = "image_urls.py"
    
    NS = {'wp': 'http://wordpress.org/export/1.2/'}
    
    def contains_japanese(text):
        return re.search(r'[\u3040-\u30FF\u4E00-\u9FFF]', text) is not None
    
    image_urls = set()
    
    context = ET.iterparse(input_path, events=("end",))
    for event, elem in context:
        if elem.tag == "item":
            post_type = elem.find("wp:post_type", NS)
            if post_type is not None and post_type.text == "attachment":
                attachment_url = elem.find("wp:attachment_url", NS)
                if attachment_url is not None:
                    decoded_url = urllib.parse.unquote(attachment_url.text)
                    filename = decoded_url.split("/")[-1]
                    if contains_japanese(filename):
                        image_urls.add(decoded_url)
            elem.clear()
    
    # Pythonファイル形式で出力
    with open(output_path, "w", encoding="utf-8") as f:
        f.write("urls = [\n")
        for url in sorted(image_urls):
            f.write(f'    "{url}",\n')
        f.write("]\n")
    
    print(f"出力完了:{len(image_urls)} 件 → {output_path}")
    

     
    この generate_image_urls_py.py を実行すると同じ階層にimage_urls.pyができます。
    image_urls.py

    urls = [
        "https://sample.jp/wp-content/uploads/2018/09/写真1.png",
        "https://sample.jp/wp-content/uploads/2019/09/写真2.png",
        ・・・
    ]
    

     

  2. 日本語ファイル名の画像を正しいディレクトリ構造で一括ダウンロード
    ※ ダウンロードしたファイルを手動でディレクトリごと上げられるように、ダウンロードした段階でディレクトリ構造を維持するようにします。
    ※ さらに、ファイル名は ASCII 安全な連番に置き換え(Windows対策)ます。
    ※ 置き換えた上でダウンロードし、ダウンロードしたファイル名を最後に元の日本語ファイル名に戻せるように、CSVで変換前ファイル名、変換後ファイル名のリストを作ります。
     
    ファイル名を連番に書き換えた上でダウンロード&変換前後のファイル名をCSV化
    download_images.py

    import os
    import urllib.request
    import urllib.parse
    import csv
    
    base_dir = "downloaded_images"
    os.makedirs(base_dir, exist_ok=True)
    
    from image_urls import urls
    
    mapping = []
    
    for i, original_url in enumerate(urls):
        try:
            parsed = urllib.parse.urlparse(original_url)
    
            # 元のパス:/wp-content/uploads/2019/04/写真.jpg
            path_parts = parsed.path.strip("/").split("/")
            if "uploads" not in path_parts:
                continue
            uploads_index = path_parts.index("uploads")
            subdirs = path_parts[uploads_index + 1:-1]  # 2019/04
            filename = path_parts[-1]
    
            decoded_filename = urllib.parse.unquote(filename)
            ext = os.path.splitext(decoded_filename)[1]
            temp_filename = f"img{i:04d}{ext}"
    
            save_dir = os.path.join(base_dir, *subdirs)
            os.makedirs(save_dir, exist_ok=True)
    
            save_path = os.path.join(save_dir, temp_filename)
            encoded_url = f"{parsed.scheme}://{parsed.netloc}{urllib.parse.quote(parsed.path)}"
    
            urllib.request.urlretrieve(encoded_url, save_path)
            mapping.append((os.path.join(*subdirs, temp_filename), decoded_filename))
            print(f"{original_url} → {save_path}")
        except Exception as e:
            print(f"{original_url} → {e}")
    
    # 書き出し(相対パス対応)
    with open("download_mapping.csv", "w", encoding="utf-8", newline='') as f:
        writer = csv.writer(f)
        writer.writerow(["temp_path", "original_filename"])
        writer.writerows(mapping)
    
    print("マッピング保存完了 → download_mapping.csv")
    

     
    CSVを元にファイル名を元に戻す
    rename_images_from_mapping.py

    import os
    import csv
    
    base_dir = "downloaded_images"
    
    with open("download_mapping.csv", "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            temp_path = row["temp_path"]
            original_filename = row["original_filename"]
    
            temp_full = os.path.join(base_dir, temp_path)
            target_dir = os.path.dirname(temp_full)
            new_full = os.path.join(target_dir, original_filename)
    
            try:
                os.rename(temp_full, new_full)
                print(f"リネーム: {temp_path} → {original_filename}")
            except Exception as e:
                print(f"リネーム失敗: {temp_path} → {original_filename} ({e})")
    

     
    download_images.pyとrename_images_from_mapping.pyを実行すると、ディレクトリ構造を維持した日本語ファイル名の画像セットが downloaded_images フォルダにできあがります。
     

  3. 画像セットを /wp-content/uploads にアップロードして完了

 
のはずでした。(続く)

ガーン・・・未登録画像があった

アップロード完了してもう大丈夫だろうとページを見直してみると、ちゃんと画像が現れて来ました。
大丈夫だ。
と、さらに見回ると、
なんと、まだ not found な画像がある!!

調べてみると、Wordpressに登録されていない画像が本文中でしばしば使われていることを発見。
Wordpress管理画面からアップすれば必ず attachment として登録されるので、管理画面からアップせずにFTPなどで直接上げた画像ファイルがあったということです。ついさっき私たちがやってたこともそうですから、特にサイトのリニューアルをしているサイトにはよくあることです。対応しましょう。
 
XMLに出てくる全画像のうち、Wordpressに登録されていない(= attachment にない)画像だけを抽出します。
 
extract_unregistered_image_urls.py

import xml.etree.ElementTree as ET
import re
import urllib.parse

input_path = "your_export.xml"  # ← あなたのXMLファイル名に置き換えてください
output_path = "unregistered_image_urls.py"

NS = {
    'content': 'http://purl.org/rss/1.0/modules/content/',
    'wp': 'http://wordpress.org/export/1.2/'
}

# 登録済み画像URLセット(attachment)
registered_urls = set()

# 参照されている画像URLセット(本文中)
referenced_urls = set()

# XMLをストリーミングで解析(大容量でも安全)
context = ET.iterparse(input_path, events=("end",))
for event, elem in context:
    if elem.tag == "item":
        post_type = elem.find("wp:post_type", NS)
        if post_type is not None:
            # attachment の attachment_url を登録済みに追加
            if post_type.text == "attachment":
                attachment_url = elem.find("wp:attachment_url", NS)
                if attachment_url is not None:
                    decoded_url = urllib.parse.unquote(attachment_url.text)
                    registered_urls.add(decoded_url)
            # post や page の本文を走査
            elif post_type.text in ["post", "page"]:
                content_elem = elem.find("content:encoded", NS)
                if content_elem is not None and content_elem.text:
                    matches = re.findall(r'https?://[^\s\'"]+\.(?:jpg|jpeg|png|gif|webp)', content_elem.text, flags=re.I)
                    for match in matches:
                        referenced_urls.add(urllib.parse.unquote(match))
        elem.clear()

# 差分 = 本文中で使われているが attachment として登録されていない画像URL
unregistered_urls = sorted(referenced_urls - registered_urls)

# Python配列形式で出力
with open(output_path, "w", encoding="utf-8") as f:
    f.write("urls = [\n")
    for url in unregistered_urls:
        f.write(f'    "{url}",\n')
    f.write("]\n")

print(f"未登録の画像URLを {output_path} に出力しました({len(unregistered_urls)} 件)")

 
これで未登録画像リストが出せました。

最後に先ほど使った download_images.py の

from image_urls import urls
base_dir = "downloaded_images"
download_mapping.csv

をそれぞれ

from unregistered_image_urls import urls
base_dir = "downloaded_unregistered_images"
download_unregistered_mapping.csv

に変更し、
rename_images_from_mapping.py の

base_dir = "downloaded_images"
download_mapping.csv

をそれぞれ

base_dir = "downloaded_unregistered_images"
download_unregistered_mapping.csv

に直して、実行し、downloaded_unregistered_imagesフォルダにできた画像セットをアップロードして、今度こそ本当に完了です。
 
画像ファイルが日本語のファイル名だっただけで、こんなにも苦労するとは。。
 
同じ課題にぶつかった方はご活用ください。

【100ウェブ新着情報メルマガ】

WordPressカスタマイズ事例やウェブ制作ノウハウの新着情報、お役立ち情報を
リアルタイムにメルマガ配信!