将mem.ai中最大的一篇图片笔记导出的历程

mem.ai本来很方便,有点什么零碎想法都可以粘贴到那里。但其优势也正是劣势所在:某篇笔记经年累月后积攒了太大的体积,最近就总也打不开了,好像连累得其他笔记也都打开缓慢。所以,就想将其导出到Notion,Notion处理大体积文件完全没问题!

想打开这篇笔记已经很难了,经过无数次引导刷新,总算打开了,全选、复制、粘贴到Notion,好,结束了?

并没有

因为发现粘过来Notion的所有图片,其地址链接仍然在mem.ai也就是Google这里,一旦哪天它发现盗链或者干脆图库崩了,那这笔记就没法看了!

于是开始吧,又一次折腾之旅。


第一步,将Notion中的笔记内容导出为Markdown

image-20260818165151869

用Typora打开此MD文件,发现所有图片内容都能正常显示,只不过查看其地址,都是Google的地址。

第二步,将笔记内所有图片下载到本地并关联

导出的是一个zip文件,将其中的md文件解压到目标文件夹。用Typora打开后,如图所示,Notion 在导出时根本没有下载图片,因为它在 Markdown 里将这些内容解析成了纯文本网址(Text Link),而不是 Markdown 的图片语法 ![图片](url)。既然 Notion 认为这只是几行普通网址,导出时就不会去下载图片文件。

观察截图中的 URL,这其实是 Mem 的代理图片地址,里面已经包含了图片的原始真实地址(例如 [https://cdn.beekka.com/blogimg/asset/](https://cdn.beekka.com/blogimg/asset/)...),但并非真正的Markdown标准图片(显示)语法。

image-20260818170136335

让 Notion 导出的 Markdown 包含标准图片语法 ![alt](url)的方法

使用 Emeditor 一键正则替换(如果你想在本地生成标准 .md

如果你手头有之前解压出来的 .md 文件,想在本地把它快速改成标准 Markdown 图片格式(即加上 ![] 包裹):

  1. VS CodeEmeditor 打开该 .md 文件。
  2. Ctrl + H 打开查找和替换
  3. 勾选 正则表达式模式(正则图标 .\*
  4. 输入以下内容(原链接格式类似:[https://www.mem.storage/proxy?url=https%3A%2F%2Fcdn.beekka.com%2Fblogimg%2Fasset%2F202407%2Fbg2024070211.webp&token=59ce0544-cee1-41e9-b0c1-0a9baaf9d84d](https://www.mem.storage/proxy?url=https%3A%2F%2Fcdn.beekka.com%2Fblogimg%2Fasset%2F202407%2Fbg2024070211.webp&token=59ce0544-cee1-41e9-b0c1-0a9baaf9d84d)):
    • 查找(Find)\[https?://[^\s\)]*?token=([a-f0-9-]+)\]\((https?://[^\s\)]+?\.(?:webp|png|jpg|jpeg|gif|svg))(?:&token=[^\s\)]*)?\)
    • 替换为(Replace)![$1]($2)
  5. 点击 全部替换(Replace All)

最快修复方案:用 Python 脚本一键批量下载图片并替换

如果你导出的 zip 包里有个 .md 文件,可以用以下 Python 脚本自动提取这些链接中的真实图片,下载到本地并自动修改 Markdown 语法,这样重新导入 Notion 就完美了:

  1. 打开刚才解压出来的文件夹,在里面新建一个名为 fix_md.py 的文件。
  2. 将以下代码粘贴进去并保存:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import os
import re
import urllib.parse
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning

# 禁用 SSL 警告
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

# ==================== 读取文件 ====================
md_files = [f for f in os.listdir('.') if f.endswith('.md') and not f.startswith('._')]
if not md_files:
print("❌ 未找到 .md 文件!")
exit()

md_file = md_files[0]
print(f"📄 当前处理文件: {md_file}")

images_dir = "assets"
os.makedirs(images_dir, exist_ok=True)

with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()

session = requests.Session()
session.trust_env = False

global_counter = 0

def resolve_smart_filename(raw_url, idx):
"""提取图片唯一标识 UUID 或真实文件名"""
try:
actual_url = raw_url
if 'url=' in raw_url:
actual_url = urllib.parse.unquote(raw_url.split('url=')[1].split('&')[0])

# 针对 UUID 路径寻找文件名 (如 /images/374da85e-.../image.png)
uuid_match = re.search(r'/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/', actual_url, re.I)
if uuid_match:
return f"{uuid_match.group(1)}.png"

# 普通 CDN 提取文件名
pathname = urllib.parse.urlparse(actual_url).path
filename = pathname.split('/')[-1]

if filename and len(filename) >= 3 and 'proxy' not in filename and 'fimage' not in filename and filename != 'image.png':
return filename

ext = ".png"
if ".webp" in actual_url.lower(): ext = ".webp"
elif ".jpg" in actual_url.lower() or ".jpeg" in actual_url.lower(): ext = ".jpg"
return f"img_{idx + 1}{ext}"
except Exception:
return f"img_{idx + 1}.png"

md_image_pattern = r'!\[(.*?)\]\((https?://[^\s\)]+)\)'

def process_md_image(match):
global global_counter
alt_text = match.group(1)
raw_url = match.group(2)

global_counter += 1
img_name = resolve_smart_filename(raw_url, global_counter)
img_path = os.path.join(images_dir, img_name)

# 1. 如果之前已经成功下载过了,直接替换为本地路径
if os.path.exists(img_path) and os.path.getsize(img_path) > 200:
print(f"⏩ [{global_counter}] 已存在,直接链接本地: {img_name}")
return f"![{alt_text}]({images_dir}/{img_name})"

# 2. 如果包含 url=,提取真正的外链地址,否则直接使用原地址
target_url = raw_url
if 'url=' in raw_url:
target_url = urllib.parse.unquote(raw_url.split('url=')[1].split('&')[0])

print(f"⬇️ [{global_counter}] 正在尝试下载: {img_name}")

# 使用标准浏览器 User-Agent,无需 Cookie 和 Authorization Token
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}

try:
resp = session.get(target_url, headers=headers, timeout=10, verify=False)
if resp.status_code == 200 and len(resp.content) > 200:
with open(img_path, 'wb') as f:
f.write(resp.content)
print(f" └─ ✅ 下载成功: {img_name}")
return f"![{alt_text}]({images_dir}/{img_name})"
else:
print(f" └─ ❌ 请求失败/需鉴权拦截 (状态码 {resp.status_code}),保留原链接")
return match.group(0)
except Exception as e:
print(f" └─ ❌ 网络连接失败,保留原链接")
return match.group(0)

# 执行替换并回写文件
new_content = re.sub(md_image_pattern, process_md_image, content)

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

print(f"\n🎉 处理完毕!通用开放图片已全部存入 assets/ 目录。")

因为 Mem 官方对此类私有存储图片的鉴权逻辑做了修改:必须携带登录状态下的 Cookie 凭证 才能访问 mem.storage/proxy 端点,仅靠链接末尾带的 token= 已经无法单独通过脚本直接下载(会返回 403 错误)。

但是,即使带着下面的种种条件(包括cookie/token/headers),依然无法下载需要谷歌鉴权的图片文件,于是对上面代码做了精简,以下内容仅供学习参考。

针对这种情况,这里为你提供两个解决方案:

利用你在浏览器中登录 Mem 的会话 Cookie,让 Python 伪装成你的浏览器去下载。

  1. 打开浏览器登录 Mem.ai

  2. F12 打开开发者工具,切换到 Network (网络) 标签页。

  3. 随意点击左侧的一篇笔记或刷新一下页面,在 Network 列表中找到任意一个请求(如 graphqlproxy?url=...)。

  4. 点击该请求,在右侧 Headers (请求头) 中找到 Cookie:,右键选择 Copy value (复制值)

  5. 关键不只有 Cookie,还有 Mem 的核心认证机制所使用的 Bearer / OAuth Token

    在截图的请求头中,Authorization 字段下有一长串 OAuthAccessToken eyJhbGciOi...,这才是访问 Mem 云端资源的真实通行证!

    我们需要把这个 Token 提取出来,加进请求头里(同时保留 Cookie 和正确的代理重组逻辑)。

storage.googleapis.com 的谷歌私有存储图片依然报错,下载不来。对比这两种链接可以发现根源所在:

  • AIGIF 的链接:真实地址是 cdn.beekka.com(公共图床),由于不限制访问,脚本解码后直接去请求这个 CDN 就能顺利下载。而且,下载完毕后即**自动于md文件中完成同文件夹下assets文件夹的路径替换**。
  • Imagenie 的链接:真实地址是 [storage.googleapis.com/memvp-25499](https://storage.googleapis.com/memvp-25499)...(Mem 存储在谷歌云上的私有图片)。这个地址需要配合 Mem 代理服务 加上 你的登录 Cookie 以及 token 才能放行,直连或格式拼错就会抛出 403。

image-20260818200359252

现在的情况是,前一种链接能够顺利下载,而后一种Imagenie链接,即使加上了Cookie和token也是依然不能下载,看来就是权限的问题。

这时候又出现另一种思路。


既然mem.ai的网页(偶尔)能打开笔记并正常显示图片,就表明所有图片(一定要用page down在页面下拉到最底)均已完成缓存加载,那就直接到缓存里去取下载不来的图片不就好了?!

在运行打包脚本前,先在 Mem 网页里快速按住 Page Down 或按住空格键,一路滚动到底部。

确保页面上所有的图片都加载呈现出来。

此时再运行控制台脚本,就能抓取到全部图片(数量就会对上了)。

直接在浏览器控制台(Console)一键批量下载

因为浏览器控制台发起的 fetch自动带上当前页面的所有 Session、Cookie 和 Header,所以直接在 Console 运行下载代码是不会触发 403 的。

  1. 在 Chrome 中打开 Mem.ai 该笔记页面(确保图片能正常显示)。
  2. F12 切换到 Console(控制台) 标签页。
  3. 粘贴以下代码并回车,它会自动提取页面里所有的 Google/Mem 图片并触发浏览器下载:

要实现“自动读取 MD 文件里的所有链接,并一次性批量下载剩余的全部GCP图片”,最流畅的方案是:写一个 Python 自动化脚本,自动把 Markdown 中的所有未能完成下载的GCP图片 URL 提取出来,生成控制台代码,你在浏览器里粘贴运行即可一键打包。

第一步:运行 Python 脚本生成浏览器提取代码

在你的电脑上运行这段 Python 脚本。它会读取 .md 文件,把所有图片的原始 URL 自动解析并整理好,直接在控制台输出一段可执行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import os
import re

md_files = [f for f in os.listdir('.') if f.endswith('.md') and not f.startswith('._')]
if not md_files:
print("❌ 未找到 .md 文件!")
exit()

md_file = md_files[0]
print(f"📄 读取 Markdown 文件: {md_file}")

with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()

# 精准只匹配 Markdown 图片语法: ![alt](url)
md_image_pattern = r'!\[.*?\]\((https?://[^\s\)]+)\)'
raw_urls = re.findall(md_image_pattern, content)

# 过滤去重并排除明显的网页链接
urls = []
for url in list(dict.fromkeys(raw_urls)):
# 只要是包含 mem.storage、googleapis、或者常见图片后缀的才算图片,排除常规网页
if 'mem.storage' in url or 'googleapis.com' in url or re.search(r'\.(png|jpg|jpeg|webp|gif|svg)', url, re.I):
urls.append(url)

print(f"🔍 共提取到 {len(urls)} 条有效图片链接(已自动过滤常规网页链接)")

urls_js_array = ",\n ".join([f'"{url}"' for url in urls])

js_code = f"""
(async () => {{
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
document.head.appendChild(script);
await new Promise(r => script.onload = r);

const zip = new JSZip();
const folder = zip.folder("assets");

const urls = [
{urls_js_array}
];

console.log(`🚀 开始批量提取 ${{urls.length}} 张真正图片...`);

let count = 0;
for (let i = 0; i < urls.length; i++) {{
const rawUrl = urls[i];

let fileName = `img_${{i + 1}}.png`;

// 1. 匹配 UUID
const uuidMatch = rawUrl.match(/([a-f0-9]{{8}}-[a-f0-9]{{4}}-[a-f0-9]{{4}}-[a-f0-9]{{4}}-[a-f0-9]{{12}})/i);
// 2. 匹配 CDN 原文件名
let cdnName = '';
if (rawUrl.includes('url=')) {{
try {{
const decoded = decodeURIComponent(rawUrl.split('url=')[1].split('&')[0]);
cdnName = decoded.split('/').pop();
}} catch(e) {{}}
}}

if (uuidMatch) {{
fileName = `${{uuidMatch[1]}}.png`;
}} else if (cdnName && cdnName.length > 3 && !cdnName.includes('proxy') && !cdnName.includes('fimage')) {{
fileName = cdnName;
}}

try {{
const resp = await fetch(rawUrl, {{ credentials: 'include' }});
if (resp.ok) {{
const blob = await resp.blob();
folder.file(fileName, blob);
count++;
console.log(`✅ [${{count}}/${{urls.length}}] 下载成功: ${{fileName}}`);
}} else {{
console.warn(`⚠️ 跳过非图片/无权限资源 (${{resp.status}}): ${{fileName}}`);
}}
}} catch (err) {{
console.warn(`⚠️ 跨域/无法连接被拦截,已忽略: ${{fileName}}`);
}}
}}

if (count > 0) {{
console.log('⚡ 打包中...');
const content = await zip.generateAsync({{ type: "blob" }});
const a = document.createElement('a');
a.href = URL.createObjectURL(content);
a.download = "mem_all_assets.download.zip";
a.click();
console.log('🎉 批量提取完成!已跳过网页链接,ZIP 自动下载。');
}} else {{
console.error('😭 无图片下载成功');
}}
}})();
"""

with open("browser_runner.js", "w", encoding="utf-8") as f:
f.write(js_code)

print("\n✅ 生成完毕!重新复制 `browser_runner.js` 到浏览器控制台运行即可。")

第二步:一键运行并下载 ZIP

  1. 打开刚刚生成的 browser_runner.jsCtrl + A 复制全部内容
  2. 打开已经登录 Mem 的浏览器,按 F12 打开控制台(Console)。
  3. 粘贴并回车,浏览器就会自动把你在 MD 文件里的所有图片逐个提取,并自动打包成 mem_all_assets.download.zip 触发下载!

PS:有时会遇到鉴权隔段时间失效的情形,在Console中复制命令会提示401无鉴权。这时还有一套解决方案:image-20260818210101993

当控制台依然全量返回 401 鉴权失败时,说明 Mem 的后端在过去一段时间更新了静态资源的代理鉴权机制:它不再使用 localStorage 中的 JWT Token 校验,而是改用统一的网页前端主 API Token(Session Credentials)或直接依赖内存中的 Cookie 握手

既然自动化注入 Header 仍触发 401,这里为你提供一个 100% 成功、零鉴权问题的最终解法:直接利用浏览器已经加载在 DOM/内存中的 <img> 标签或 Blob URL,把图片通过 Canvas 画布绘制并导出为本地文件。

一键破局:DOM 渲染法(控制台脚本)

请直接在打开了该 Mem 笔记的页面控制台(F12 -> Console)粘贴运行以下代码。这段代码不发起任何重新鉴权的网络请求,而是直接抓取当前网页上已经渲染出来的图片像素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
(async () => {
// 1. 动态加载 JSZip
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
document.head.appendChild(script);
await new Promise(r => script.onload = r);

const zip = new JSZip();
const folder = zip.folder("assets");

// 2. 抓取页面上所有的 img 元素
const imgElements = Array.from(document.querySelectorAll('img'));
console.log(`🚀 页面共找到 ${imgElements.length} 个 DOM 图片元素,准备提取像素...`);

let count = 0;

for (let i = 0; i < imgElements.length; i++) {
const img = imgElements[i];

// 忽略头像、Icon 等小图标,只提取笔记主体图片
if (img.naturalWidth < 50 || img.naturalHeight < 50) continue;

try {
// 创建 Canvas 重新绘制图像导出 Base64
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);

// 获取图片 UUID 或生成标准序号
const src = img.src || '';
let fileName = `img_${count + 1}.png`;
const uuidMatch = src.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
if (uuidMatch) {
fileName = `${uuidMatch[1]}.png`;
}

// 转为 Blob 写入 ZIP
const dataUrl = canvas.toDataURL('image/png');
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
folder.file(fileName, base64Data, { base64: true });

count++;
console.log(`✅ [${count}] 提取 DOM 图像成功: ${fileName}`);
} catch (e) {
console.warn(`⚠️ 无法读取 DOM 像素 (受跨域 Canvas 污染限制):`, img.src);
}
}

if (count > 0) {
console.log('⚡ 打包导出中...');
const content = await zip.generateAsync({ type: "blob" });
const a = document.createElement('a');
a.href = URL.createObjectURL(content);
a.download = "mem_dom_assets.zip";
a.click();
console.log('🎉 DOM 图像提取完成!已自动触发 ZIP 下载。');
} else {
console.error('😭 页面 DOM 中未能捕捉到可绘制的图片,请确保页面已完全向下滚动加载所有图片!');
}
})();

操作建议与注意点:

  1. 先在 Mem 笔记页面中滑动到底部:确保所有的 122 张图片都已经完成了懒加载(Lazy Load),并在页面上呈现。
  2. 复制粘贴上述代码并按回车:脚本会把页面上能看到的真实像素图像直接画到 Canvas 里导出为 ZIP,完全绕过任何 HTTP 接口层面的 401 拦截。

第三步:把下载的 ZIP 解压,并替换 MD 链接

解压得到的 assets 文件夹放到 MD 同级目录下。原网址如:![59ce0544-cee1-41e9-b0c1-0a9baaf9d84d](https://www.mem.storage/proxy?url=https%3A%2F%2Fstorage.googleapis.com%2Fmemvp-25499.appspot.com%2Fimages%2F374da85e-19cf-4452-9e1f-3b095d7aabac%2Fimage.png)

打开 VS Code 载入你的 .md 文件,按 Ctrl + H 打开正则替换(勾选 .*):

  • 查找 (Find)

    1
    (!\[[a-f0-9-]+\])\(https?://[^\s\)]*?%2F(?:images|mem-uploads)%2F([a-f0-9-]+)%2Fimage(\.[a-zA-Z0-9]+)\)
  • 替换为 (Replace)

    Code snippet

    1
    $1(assets/$2$3)

正则原理拆解

  1. (!\[[a-f0-9-]+\]) ➔ 捕获组 $1: 完整保留前半段的 ![59ce0544-cee1-41e9-b0c1-0a9baaf9d84d]

    %2Fimages%2F: 吃掉前面一直到 %2Fimages%2F 的所有域名及代理前缀,并在替换结果中替换为 assets/

    ([a-f0-9-]+) ➔ 捕获组 $2: 精准捕获图片的 36 位 UUID 哈希值(即 374da85e-19cf-4452-9e1f-3b095d7aabac)。

    (\.[a-zA-Z0-9]+) ➔ 捕获组 $3: 捕获 %2Fimage 后面的动态扩展名(如 .png.webp 等),原样保留在末尾。


如上操作后,相信绝大部分图片都已经改成了本地链接'assets\xxxx.png'之类,如何查找仍未替换成本地链接的图片链接?

方法一:在 VS Code 中使用正则表达式精准查找(最直观)

打开你的 .md 文件,按 Ctrl + F 打开查找框,勾选右上角的 .\*(开启正则表达式),输入以下正则:

1
!\[.*?\]\(https?://[^\s\)]+\)

📌 原理拆解:

  • !\[.*?\]:匹配图片的前缀 ![alt文本]
  • \(https?://[^\s\)]+\):匹配以 http://https:// 开头的完整远程 URL。
  • 这样就可以自动跳过所有已经修改为 (assets/...)(assets\...) 的本地路径,只高亮并定位依然是网络链接的图片!你可以按 Enter 逐个检查剩余的链接。

方法二:用 Python 脚本自动扫描并汇总剩余远程链接

如果你想把所有漏网之鱼的行号、图片 URL 统一提取出来看,可以在同级目录下运行这个简单脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import os
import re

md_files = [f for f in os.listdir('.') if f.endswith('.md') and not f.startswith('._')]
if not md_files:
print("❌ 未找到 .md 文件!")
exit()

md_file = md_files[0]
print(f"📄 正在检查文件: {md_file}\n")

# 正则匹配所有 http(s) 开头的图片链接
remote_img_pattern = r'!\[(.*?)\]\((https?://[^\s\)]+)\)'

unreplaced_count = 0

with open(md_file, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
matches = re.findall(remote_img_pattern, line)
for alt, url in matches:
unreplaced_count += 1
print(f"📍 第 {line_num} 行:")
print(f" Alt: {alt if alt else '(无)'}")
print(f" URL: {url}\n")

if unreplaced_count == 0:
print("🎉 太棒了!文件中所有图片均已成功替换为本地路径,没有任何遗漏!")
else:
print(f"⚠️ 统计完成:共有 {unreplaced_count} 处图片链接尚未替换成本地路径。")

脚本运行后会直接输出具体的行号未替换的 URL,方便你针对性地进行手动修改或补全。


有张图片不知什么格式,被刚才的代码自动更改为png打不开,怎么办?

出现这种情况是因为脚本默认给它赋予了 .png 后缀,但图片的实际底层数据其实是 WebP、JPEG、GIF 或 SVG 等格式。扩展名不匹配导致图片查看器或 Typora 无法正常解码解析。

解决办法非常简单,分为“定位并修复单张图片”“批量修正可能混错格式的图片”两种:

方法一:查看图片的“真身”格式(最直接)

你可以通过以下 2 种小技巧找到它的真实格式,然后把文件名后缀改过来:

  1. 用浏览器打开该图片
    • 在文件管理器里,把这张打不开的 .png 文件直接拖进 Chrome / Edge 浏览器
    • 浏览器具有极强的容错解码能力,会自动渲染显示图片。
    • 在图片上右键选择“另存为”,观察弹窗默认提示的保存类型(例如 WebP 图像JPEG 图像)。
  2. 用记事本查看文件头(Header)
    • 用记事本(Notepad)打开这个打不开的文件,看最前面几个字符:
      • 如果开头包含 RIFF...WEBP真实格式是 .webp,将文件名重命名为 .webp 即可。
      • 如果开头包含 JFIFExif真实格式是 .jpg
      • 如果开头包含 GIF89a真实格式是 .gif
      • 如果开头包含 <svg真实格式是 .svg

修改后缀名(例如把 xxxx.png 改为 xxxx.webp),并在 Markdown 里把链接同步改过来(如 ![](assets/xxxx.webp))就能立刻正常显示了!

方法二:运行 Python 脚本自动校正 assets 目录下所有图片的真实扩展名

如果你不确定还有没有其他图片的后缀被误标注了,可以运行下面这个自动检测脚本。它会读取图片的文件魔数(二进制 Header),自动把后缀名修正为它真正的格式,并同步自动更新 Markdown 里的文件名

自动化修复脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import os
import re

# 1. 检测文件真实格式的函数
def get_real_ext(filepath):
try:
with open(filepath, 'rb') as f:
header = f.read(16)
if header.startswith(b'\x89PNG\r\n\x1a\n'):
return '.png'
elif header.startswith(b'\xff\xd8\xff'):
return '.jpg'
elif header.startswith(b'GIF89a') or header.startswith(b'GIF87a'):
return '.gif'
elif header.startswith(b'RIFF') and header[8:12] == b'WEBP':
return '.webp'
elif b'<svg' in header.lower() or b'<?xml' in header.lower():
return '.svg'
except Exception:
pass
return None

# 2. 获取 MD 文件
md_files = [f for f in os.listdir('.') if f.endswith('.md') and not f.startswith('._')]
if not md_files:
print("❌ 未找到 .md 文件!")
exit()

md_file = md_files[0]
assets_dir = "assets"

if not os.path.exists(assets_dir):
print("❌ 未找到 assets 文件夹!")
exit()

print(f"🔍 开始检查 {assets_dir} 目录下的图片文件真实格式...\n")

with open(md_file, 'r', encoding='utf-8') as f:
md_content = f.read()

fixed_count = 0

for file in os.listdir(assets_dir):
file_path = os.path.join(assets_dir, file)
if os.path.isdir(file_path):
continue

real_ext = get_real_ext(file_path)
old_ext = os.path.splitext(file)[1].lower()

# 如果检测到真实后缀和当前后缀不一致
if real_ext and real_ext != old_ext:
base_name = os.path.splitext(file)[0]
new_filename = base_name + real_ext
new_file_path = os.path.join(assets_dir, new_filename)

# 重命名本地磁盘上的文件
os.rename(file_path, new_file_path)

# 同步替换 MD 文档里的引用
old_ref = f"assets/{file}".replace('\\', '/')
new_ref = f"assets/{new_filename}".replace('\\', '/')
old_ref_win = f"assets\\{file}"
new_ref_win = f"assets\\{new_filename}"

md_content = md_content.replace(old_ref, new_ref)
md_content = md_content.replace(old_ref_win, new_ref_win)

fixed_count += 1
print(f"🔧 修正文件: {file}{new_filename} (真实格式: {real_ext})")

if fixed_count > 0:
with open(md_file, 'w', encoding='utf-8') as f:
f.write(md_content)
print(f"\n🎉 处理完成!自动修正了 {fixed_count} 张图片的实际格式及 MD 对应链接。")
else:
print("✅ 所有本地图片的扩展名与其实际二进制格式完全匹配!")

运行完这个脚本后,打不开的图片格式就会被自动改成 .webp / .jpg 等真实格式,Typora 就能完美加载了!


如何计算出一个md文件中有多少图片?比如通过assets\字样查找?又或者有更准确更方便的方式?

要统计一个 Markdown 文件中有多少张图片,直接查找 assets\ 可以大概估算,但不够准确(因为文中普通的文本路径、代码块或重复路径也可能会触发匹配)。

最准确、最方便的方式主要分为 “编辑器工具内查找”“脚本精准统计” 两种:

方法一:在 VS Code 中使用正则查找(最方便)

在 VS Code 中打开你的 .md 文件,按 Ctrl + F 打开查找框,勾选右上角的 .\*(开启正则表达式)

1. 统计所有图片(无论本地 assets 还是网络图片):

  • 查找正则

    1
    !\[.*?\]\(.*?\)
  • 查看结果:查找框右侧会直接显示 1 of 128 类似的数字,后面的 128 就是该文档中的图片总数

  • 统计数字在Emeditor中未见,但可以通过查找、提取看出有多少图片

2. 精准统计本地 assets 目录下的图片:

  • 查找正则

    1
    !\[.*?\]\(assets[\\/].*?\)
  • 这种方式可以精确排查只引用了 assets\assets/ 文件夹的本地图片,排除掉了普通的网络图片。

方法二:用 Python 脚本统计并去重(最准确,含详细分析)

如果你想知道图片引用总次数使用了多少张不重复的图片,以及是否有损坏/不存在的本地图片,直接运行下面这段 Python 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import os
import re

md_files = [f for f in os.listdir('.') if f.endswith('.md') and not f.startswith('._')]
if not md_files:
print("❌ 未找到 .md 文件!")
exit()

md_file = md_files[0]

with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()

# 1. 匹配所有 Markdown 图片标准语法
all_images = re.findall(r'!\[.*?\]\((.*?)\)', content)

# 2. 匹配 HTML <img> 标签图片(防止部分 MD 混用了 html 标签)
html_images = re.findall(r'<img[^>]+src=["\'](.*?)["\']', content, re.I)

total_images = all_images + html_images

# 分类统计
assets_images = [img for img in total_images if img.startswith('assets') or 'assets\\' in img or 'assets/' in img]
remote_images = [img for img in total_images if img.startswith('http://') or img.startswith('https://')]
other_images = [img for img in total_images if img not in assets_images and img not in remote_images]

# 检查 assets 本地文件是否存在
missing_files = []
for img_path in set(assets_images):
# 统一路径格式
clean_path = img_path.strip().replace('/', os.sep).replace('\\', os.sep)
if not os.path.exists(clean_path):
missing_files.append(img_path)

print(f"📄 分析文件: {md_file}")
print("=" * 40)
print(f"🖼️ 图片引用总次数 : {len(total_images)} 次")
print(f"📂 本地 assets 图片 : {len(assets_images)} 张")
print(f"🌐 剩余网络远程图片 : {len(remote_images)} 张")
if other_images:
print(f"❓ 其他相对路径图片 : {len(other_images)} 张")
print(f"🧩 唯一不重复图片数 : {len(set(total_images))} 张")
print("=" * 40)

if missing_files:
print(f"⚠️ 警告: 有 {len(missing_files)} 张 assets 路径的图片在本地文件夹中未找到文件!")
for mf in missing_files:
print(f" └─ 缺失: {mf}")
else:
print("✅ 完美!所有 assets 引用的图片在本地均存在。")

总结建议:

  • 如果只是平时快速看一下:在 VS Code 中用 !\[.*?\]\(assets[\\/].*?\) 正则查看匹配数量最快。
  • 如果需要核对图片有没有漏下、本地文件齐不齐:跑一次 Python 脚本最省心。