问题背景
爬取某些英文网页用来翻译时,也下载了网页中包含的图片,原始图片有很多是这样命名的:
https://.../chapter3/1.png
我在本地使用的是扁平的存储结构,为了防止多个 1.png 冲突,重命名为 chapter3-1.png 储存。把英文版翻译了几篇后觉得图片命名方式不太好,应该用 chapter3--1.png 更方便以后重新解析。于是重新解析原始文档并重命名图片,但是这是发现已经翻译的文档中图片名已经难以溯源,不太容易对应到新命名的文件。
关键洞察
原始 URL 是唯一稳定标识,文件名只是临时表示。必须解耦“逻辑资源”与“物理存储”。
解决方案
- 建立
image_registry.json:{ url: { names: [v1, v2, ...] } } - 下载时追加新命名到历史数组
- 构建反向索引:
old_name → url → current_name - 提供脚本自动修复 Markdown 中的引用
可复用模式
- 资源注册表模式(Resource Registry Pattern)
- 引用可演化设计(Evolvable Reference)
- 适用于:PDF 转图、多语言文档媒体管理、静态站点资产迁移
实现
用一个字典来保存数据,结构如下:
{
"https://.../chapter1/transformers_chrono.svg": {
"names": [
"chapter1-transformers_chrono.svg",
"chapter1--transformers_chrono.svg",
"chapter1_transformers_chrono.svg"
],
"downloaded_at": "2025-11-08T10:21:00Z",
"content_hash": "sha256:abcd1234..." // 可选:防重复下载
}
}
使用原始URL作为键,names 包含了每次变更文件名的记录,约定列表最后一项是最新的名字,每次下载完图片把使用的文件名追加到 names 列表中,如果新文件名和 names 的最后一项相同,则不做任何更改。
例如,文档中正式在使用的图片名是 chapter1-transformers_chrono.svg,但是你已经改成了 chapter1_transformers_chrono.svg,怎么找出来这个新的名字呢?
把字典中的数据建立反向索引,以 names 中的每个值作为键,URL作为值,反转的索引类似:
{
"chapter1-transformers_chrono.svg": "https://.../chapter1/transformers_chrono.svg",
"chapter1--transformers_chrono.svg": "https://.../chapter1/transformers_chrono.svg",
"chapter1_transformers_chrono.svg": "https://.../chapter1/transformers_chrono.svg"
}
用旧名字在这个逆向索引中找到原始URL,再用URL再原始数据中到对应 names 的最后一项,这就获得了最新的文件名。
首先针对字典中使用的数据建立一个类:
from pydantic import BaseModel
from datetime import datetime
class ImageRegistryEntry(BaseModel):
names: List[str]
downloaded_at: str
content_hash: str
def add_name(self, name: str):
if not self.names:
self.names = [name]
self.downloaded_at = isonow()
return
if self.names[-1] == name:
return
self.names.append(name)
self.downloaded_at = datetime.now().astimezone().isoformat(timespec='seconds')
def merge(self, other: 'ImageRegistryEntry'):
if not other.names:
return
self.add_name(other.names[-1])
@classmethod
def from_dict(cls, data: Dict):
return cls(**data)
接下来是存储类:
class ImageRegistry:
def __init__(self, data: Dict[str, Dict] = {}):
self._data = {
k: ImageRegistryEntry.from_dict(v)
for k, v in data.items()
}
self._reversed_index: Dict[str, str] = {}
def __len__(self):
return len(self._data)
def get(self, url: str) -> ImageRegistryEntry | None:
return self._data.get(url, None)
def set_or_merge(self, url: str, item: ImageRegistryEntry) -> None:
if url not in self._data:
self._data[url] = item
return
# If exists, merge the last name from item into current entry.
entry = self._data[url]
entry.merge(item)
def add_name(self, url: str, name: str) -> ImageRegistryEntry:
entry: ImageRegistryEntry | None = self._data.get(url, None)
if not entry:
entry = ImageRegistryEntry.new(name)
self._data[url] = entry
return entry
entry.add_name(name)
return entry
def reverse_index(self):
self._reversed_index = {
name: url
for url, entry in self._data.items()
for name in entry.names
}
def latest_name(self, url: str) -> str | None:
entry = self._data.get(url, None)
if not entry:
return None
return entry.names[-1] if entry.names else None
def find_latest_name(self, current_name: str) -> str | None:
url = self._reversed_index.get(current_name, None)
if not url:
return None
return self.latest_name(url)
def to_dict(self):
return self._data.copy()
注意,这里的实现中每次写入数据后需要重新调用 reverse_index 来重建逆向索引。
假设你更改了命名规则,需要和已经存在文档同步文件名:
# 
MARKDOWN_IMAGE = re.compile( r'!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)')
registry = load_registry()
def change_image_url(markdown: str, callback: Callable[[str], str]) -> str:
def replace(match: re.Match):
alt = match.group(1)
src = match.group(2)
new_src = callback(src)
return f''
return MARKDOWN_IMAGE.sub(replace, markdown)
def rename_callback(src: str) -> str:
p = Path(src)
latest_name = registry.find_latest_name(p.name)
if latest_name:
return str(p.parent / latest_name)
return src
markdown = load_file('file.md')
markdown = change_image_url(markdown, rename_callback)