# 博客园本地博文迁移:从平铺到文件夹结构,兼谈 Skill 沉淀
博客园本地博文迁移:从平铺到文件夹结构,兼谈 Skill 沉淀
背景
我同时在用两个笔记/博客项目:
| 项目 | 定位 | 结构 |
|---|---|---|
| PreBlog | 日常笔记 + 本地博客 | 一文章一文件夹(index.md + images/) |
| Cnblogs | 博客园本地备份 | 80+ 个 .md 文件平铺在根目录 |
PreBlog 的结构很舒服——图片跟博文放在一起,不会散落。但博客园的本地文件是"大平层":所有 .md 堆在一个目录里,图片全是远程 URL(img2023.cnblogs.com)。本地图片根本没法管理。
于是决定把博客园也整成 PreBlog 风格。
目标
迁移前: 迁移后:
Cnblogs/ Cnblogs/
├── 001 双指针技巧.17408651.md ├── 001 双指针技巧/
├── 003 动态规划.17365130.md │ ├── index.17408651.md
├── 从模型到四面体剖分.md │ └── images/
└── ... (80+ 文件散落) ├── 从模型到四面体剖分/
│ ├── index.17370523.md
│ └── images/
│ ├── xxx.png
│ └── ...
└── ... (86 个文件夹)
三个要求:
- 一篇文章一个文件夹,
index.md+images/ - HTML 博文转 Markdown(博客园允许
.html格式发文) - 远程图片下载到本地
images/,引用改为相对路径
迁移脚本
用 Python 写了个脚本,核心逻辑分四步:
1. 提取 Post ID
博客园的本地文件命名是 标题.{postId}.md:
001 双指针技巧.17408651.md
^^^^^^^^
8 位 post ID
用正则 \.(\d{7,9})$ 从文件名末尾提取。去掉 post ID 后的标题作为文件夹名。
2. HTML → Markdown
博客园的 HTML 博文结构很规律,正则就能搞定:
| HTML | Markdown |
|---|---|
<p>...</p> |
段落 + 空行 |
<strong>...</strong> |
**粗体** |
<img src="url" /> |
 |
<div class="cnblogs_code"><pre>...</pre></div> |
```代码块``` |
这里踩了个坑:re.sub 调用漏了 text 参数,导致 3 个 HTML 文件转换失败。
# ❌ 报错: sub() missing 1 required positional argument: 'string'
text = re.sub(r'<p[^>]*>(.*?)</p>', r'\n\1\n', flags=re.DOTALL)
# ✅ 正确
text = re.sub(r'<p[^>]*>(.*?)</p>', r'\n\1\n', text, flags=re.DOTALL)
3. 下载远程图片
扫描  引用,用 urllib 下载到本地 images/,再把引用替换为相对路径。
最终下载了 94 张图片,涉及 25 篇文章。
4. 恢复 Post ID 关联
迁移后文件名变成了 index.md——post ID 丢了。而 cnblogs.vscode-cnb 扩展正是靠文件名末尾的 .17408651 来识别是哪篇博文的。
解决办法:从 VS Code 的状态数据库 state.vscdb 里读出了扩展存储的 85 条映射记录,批量把 index.md 重命名为 index.{postId}.md,并更新了映射表。
cnblogs.vscode-cnb 扩展的映射机制
这次搞清楚了扩展的内部逻辑:
| 机制 | 说明 |
|---|---|
| 文件名识别 | 从 index.17408651.md 末尾提取 17408651 |
| 显式映射表 | 存在 %APPDATA%\Code\User\globalStorage\state.vscdb 的 postFileMaps 字段 |
| 嵌套文件夹 | ✅ 原生支持,文件可以放在任意子目录 |
state.vscdb 操作示例
import sqlite3, json
db = r'%APPDATA%\Code\User\globalStorage\state.vscdb'
conn = sqlite3.connect(db)
cur = conn.execute("SELECT value FROM ItemTable WHERE key='cnblogs.vscode-cnb'")
data = json.loads(cur.fetchone()[0])
# 更新映射
data['postFileMaps'] = [[post_id, new_file_path], ...]
conn.execute("UPDATE ItemTable SET value=? WHERE key='cnblogs.vscode-cnb'",
(json.dumps(data, ensure_ascii=False),))
conn.commit()
踩坑记录
1. PowerShell 的 [] 通配符陷阱
文件夹名含方括号(如 int main(int argc, char argv[]) 的涵义),Get-ChildItem 会把 [] 当通配符,导致误判文件夹为空,差点删掉。
# ❌ 方括号被当通配符
Get-ChildItem "int main(int argc, char argv[]) 的涵义"
# ✅ 正确
Get-ChildItem -LiteralPath "int main(int argc, char argv[]) 的涵义"
2. Python GBK 终端不能输出 Emoji
Windows 终端默认 GBK 编码,print("✅") 会抛 UnicodeEncodeError。脚本里避免使用 emoji,改用纯文本标记。
3. 操作 state.vscdb 前要关 VS Code
修改扩展状态数据库时 VS Code 如果开着,可能被覆盖。操作前先关闭,或者做好备份。
最终结果
| 指标 | 数值 |
|---|---|
| 迁移博文 | 86 篇(83 .md + 3 .html) |
| HTML 转 MD | 3 篇 |
| 远程图片下载 | 94 张(25 篇文章) |
| post ID 修复 | 85 条映射自动更新 |
| 失败 | 0 |
沉淀的 Skill
这次经历提炼成了 3 个 Skill,放在 PreBlog 的 .github/skills/ 下,以后遇到同类问题自动加载:
Skill 1: cnblogs-migration
博文结构迁移的完整流程——Post ID 提取、HTML→MD 转换、远程图片下载、文件夹重组。
---
name: cnblogs-migration
description: '将博客园平铺的 .md/.html 博文重组成 PreBlog 风格...'
---
关键步骤:
- 正则
\.(\d{7,9})$提取 post ID re.sub务必传三个参数(pattern, repl, string)- cnblogs 图片域名:
img2023.cnblogs.com等 - PowerShell 用
-LiteralPath避开[]通配符
Skill 2: cnblogs-vscode-extension
cnblogs VS Code 扩展的内部机制——两种 post ID 识别方式、state.vscdb 映射表结构、修复同步断开的步骤。
---
name: cnblogs-vscode-extension
description: 'cnblogs.vscode-cnb 扩展的内部机制:Post ID 识别、文件映射存储...'
---
关键发现:
- 扩展支持嵌套文件夹
- 映射表键名:
cnblogs.vscode-cnb→postFileMaps - 修复三步走:改文件名 → 更新映射表 → Reload Window
Skill 3: vscode-state-db
通用的 VS Code 扩展状态数据库操作——适用于任意扩展,不限于 cnblogs。
---
name: vscode-state-db
description: '读写 VS Code 扩展的持久化状态数据库(state.vscdb SQLite)...'
---
通用操作:
- 数据库路径:
%APPDATA%\Code\User\globalStorage\state.vscdb - 表结构:
ItemTable(key TEXT, value TEXT) - 操作前备份,操作时关 VS Code
总结
这次迁移的核心收获:
- 结构即文档:一文章一文件夹的约定,比任何配置都可靠
- 理解工具的内部机制:搞清楚
cnblogs.vscode-cnb的映射原理后,修复就很简单 - 踩坑要沉淀:PowerShell
[]陷阱、re.sub缺参数、GBK emoji 问题——写成 Skill 下次就不会再犯 - 扩展状态数据库是宝藏:
state.vscdb可以诊断和修复几乎所有 VS Code 扩展的问题
现在两个项目的结构统一了,本地图片和博文不会再分离。以后写博客,截图直接粘贴到对应文件夹的 images/ 里就行。
后续:博文分类
结构统一之后,86 篇博文虽然各在自家文件夹,但全堆在根目录还是不好找。于是进一步按主题分类,搬进 9 个子文件夹。
分类方案
| 分类 | 篇数 | 内容 |
|---|---|---|
01-C++编程 |
30 | C++ 问答系列(1-140)、STL、C++11/17、内存、多态、异常等 |
02-算法与数据结构 |
14 | 双指针、DP、回溯、BFS、二分、滑动窗口、树、图、排序 |
03-计算机基础 |
3 | 计网 1-40、AI |
04-图形学与3D |
17 | Hypermesh、abaqus、tetgen、eigen、OpenGL、流形、PLA |
05-GUI开发 |
3 | ImGui、MFC |
06-工具与技巧 |
7 | wsl2、Linux、opencv、MAC地址、Python |
07-软件工程 |
5 | 设计模式、代码整洁、命名、读源码、Latex |
08-读书笔记 |
6 | 传记、大脑、极端的年代 |
09-其他 |
2 | Others + 本文 |
最终目录结构
Cnblogs/
├── 01-C++编程/
│ ├── C++ 1-20/
│ │ ├── index.17472034.md
│ │ └── images/
│ ├── STL/
│ └── ... (30篇)
├── 02-算法与数据结构/
│ ├── 001 双指针技巧/
│ └── ... (14篇)
├── 03-计算机基础/
├── 04-图形学与3D/
├── 05-GUI开发/
├── 06-工具与技巧/
├── 07-软件工程/
├── 08-读书笔记/
└── 09-其他/
分类脚本
同样是 Python 脚本批量处理:维护一个文件夹名→分类的映射字典,shutil.move 移动文件夹,再遍历 state.vscdb 的 postFileMaps,把旧路径中的 /{folder_name}/ 替换为 /{category}/{folder_name}/。
CATEGORIES = {
"C++ 1-20": "01-C++编程",
"001 双指针技巧": "02-算法与数据结构",
"LearnOpenGL": "04-图形学与3D",
# ... 87 条映射
}
# 移动文件夹
shutil.move(str(src), str(dst))
# 更新 state.vscdb 路径
for i, (pid, old_path) in enumerate(maps):
up = old_path.replace('\\', '/')
for folder_name, cat in moved.items():
marker = f"/{folder_name}/"
if marker in up:
maps[i] = [pid, up.replace(marker, f"/{cat}/{folder_name}/")]
踩坑续
这次又踩了一个坑:categorize.py 脚本第一次只更新了 1 条映射。排查发现 state.vscdb 里的路径还是旧平铺格式(/Cnblogs/C++ 61-80.17505321.md),而不是之前修好的文件夹格式。也就是说 fix_ids.py 的更新在 VS Code 重载后被覆盖回去了。
教训:修改 state.vscdb 后必须确保 VS Code 已经完全关闭,否则扩展启动时可能用内存中的旧数据覆盖数据库。最终的修复脚本从旧文件名中解析出 post ID 和标题,跳过中间状态,直接一步到位重建为分类路径。
最终统计
| 指标 | 数值 |
|---|---|
| 总博文 | 87 篇(含本文) |
| 分类文件夹 | 9 个 |
| state.vscdb 映射 | 87 条全部更新 |
index.{postId}.md + images/ |
每篇标配 ✅ |
现在找文章一目了然,扩展同步也在迁移、分类、映射修复三轮操作后完全正常。

浙公网安备 33010602011771号