# 博客园本地博文迁移:从平铺到文件夹结构,兼谈 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 个文件夹)

三个要求:

  1. 一篇文章一个文件夹index.md + images/
  2. HTML 博文转 Markdown(博客园允许 .html 格式发文)
  3. 远程图片下载到本地 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" /> ![image](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. 下载远程图片

扫描 ![...](https://img2023.cnblogs.com/...) 引用,用 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.vscdbpostFileMaps 字段
嵌套文件夹 ✅ 原生支持,文件可以放在任意子目录

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-cnbpostFileMaps
  • 修复三步走:改文件名 → 更新映射表 → 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

总结

这次迁移的核心收获:

  1. 结构即文档:一文章一文件夹的约定,比任何配置都可靠
  2. 理解工具的内部机制:搞清楚 cnblogs.vscode-cnb 的映射原理后,修复就很简单
  3. 踩坑要沉淀:PowerShell [] 陷阱、re.sub 缺参数、GBK emoji 问题——写成 Skill 下次就不会再犯
  4. 扩展状态数据库是宝藏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.vscdbpostFileMaps,把旧路径中的 /{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/ 每篇标配 ✅

现在找文章一目了然,扩展同步也在迁移、分类、映射修复三轮操作后完全正常。

posted @ 2026-06-13 22:00  Loong_vyg  阅读(4)  评论(0)    收藏  举报