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からダウンロードして登録します。
しかし以下の点が問題となります:
要するに、別のファイル名で登録されちゃったみたいです。確かに乱数かハッシュ化されたファイル名が結構見当たります。
今回の対応では、以下の手順で問題を一つひとつ解決していきました。
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", ・・・ ]
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 フォルダにできあがります。
のはずでした。(続く)
アップロード完了してもう大丈夫だろうとページを見直してみると、ちゃんと画像が現れて来ました。
大丈夫だ。
と、さらに見回ると、
なんと、まだ 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フォルダにできた画像セットをアップロードして、今度こそ本当に完了です。
画像ファイルが日本語のファイル名だっただけで、こんなにも苦労するとは。。
同じ課題にぶつかった方はご活用ください。
WordPressカスタマイズ事例やウェブ制作ノウハウの新着情報、お役立ち情報を
リアルタイムにメルマガ配信!