四年前我还在用一部旧安卓手机写博客。没有电脑没有编辑器所有文章都从九宫格键盘一个个字敲出来保存在备忘录里定时同步到 CSDN。那时候我完全没想过数据归属这件事直到有一天发现平台编辑器的老文章排版一塌糊涂、导出功能形同虚设我才开始正视一个问题我写的东西到底归谁管这四年里我断断续续把近两百篇 CSDN 博文从“平台账号里的文章”变成了“自己硬盘上可以随时解析和检索的数据”。整个过程绕不开两个关键词手机键盘和正则解析。这篇文章不是教程合集而是把我从数据权限、文本清洗、正则匹配到归档检索的全过程完整拆开附上我实际在用的源码讲清楚每个环节为什么这样设计、踩过哪些坑、有哪些代码可以直接抄走。无论你是天天用手机码字的写作者还是想把平台文章备份下来的开发者这篇文章应该都能给你几条能落地的思路。1. 项目缘起我为什么跟自己的博客数据死磕了四年1.1 从手机键盘开始的写作习惯我的写作场景非常固定通勤、午休、睡前用手机备忘录敲一个个段落。当时的习惯是每篇文章存成一个 txt 文件用一段特殊的标题格式开头比如“【2021-04-12】从零搭建你的第一个爬虫”后面的正文按“段落之间空一行”的方式组织。这个习惯看上去很原始但恰恰是它决定了后来整个解析方案的方向所有原始素材都以纯文本形式存在没有富文本没有格式干扰只有文字本身。你可能会问为什么不在手机上直接用 CSDN 编辑器写我当时在用的是第三方输入法和系统备忘录原因是 CSDN 的移动端编辑器在弱网环境下保存经常失败加上微信等应用频繁切换后台内容丢过两三次之后就彻底放弃了。现在回看这个“被迫的”选择反而是整件事能顺利推进的最大前提纯文本是所有数据结构里最容易被解析的没有之一。1.2 “数据自由”到底在解决什么问题文章写了两年之后平台积累的内容越来越多我慢慢意识到三个具体痛点平台没有一键导出全部文章的完整功能只有逐篇文章复制粘贴的操作方式。老文章经常因为编辑器版本升级导致格式错乱代码块变成普通文本列表层级丢失。我想建立一个属于自己的本地知识库把博文、草稿、微信聊天里记录的灵感碎片统一检索但平台的数据是“孤岛”没法直接接入本地工具。所以“数据自由”这个词对我来说不是口号而是一个很具体的工程目标不管平台还在不在我都能把曾经发布的所有内容拉回到本地并且用统一的结构化方式重新解析、索引、搜索。我需要的是“数据的可迁移性”和“数据的可解析性”这两点缺一不可。1.3 四年里我踩过的第一个坑平台导出的无力感最开始我尝试了当时所有能找到的导出方案。平台自带的导出功能会生成一份 PDF 或 HTML 文件然而 PDF 里的代码块和中文标点经常错乱HTML 文件又夹杂了大量样式标签和广告脚本。我试着把 HTML 直接另存为 Markdown结果每个文件都残留几十行无用代码手动清理一个文件就要十分钟。这之后我才意识到所谓“数据自由”并不是有一个现成的按钮让你一键下载而是你必须自己写一层清洗和解析逻辑把平台给你的一堆“半成品”数据重新加工成真正属于自己的东西。这就是为什么项目标题里把“手机键盘”和“正则解析”放在一起前者是我的内容生产端后者是内容加工端两者之间隔着整整一层数据清洗的脏活累活。2. 整体设计思路先把“数据自由”拆成四个模块2.1 数据采集层手机端怎么低成本沉淀内容数据自由的前提是“数据先能集中到一个地方”。我最开始的做法是靠手动同步每写完一篇就复制到 CSDN 后台再粘贴发布但这个流程对草稿、灵感、半成品非常不友好。后来我调整了方案在手机端用同步盘 App 建立一个“博客草稿箱”目录每篇文章一个 txt 文件文件名就是日期加标题。这样草稿会自动同步到电脑端我再在电脑上做后续处理。这个方案没有引入任何复杂的工具核心思路就是“文件名即元数据”。文件名里带了日期和标题意味着哪怕文件内容完全混乱我也能通过文件名恢复基本的时间线和主题信息。这是低成本数据采集的关键也是很多做个人知识库的人容易忽略的地方采集阶段的结构设计决定了后续解析阶段能省多少事。2.2 传输与落盘层从碎片文本到结构化文件每次从手机同步过来的 txt 文件都只能算是“原始素材”还不能直接进知识库。我在电脑端做了第一次整理把所有文件分成三个目录publish已发布、draft半成品、source引用资料。整理规则很简单发布过的文章进 publish没写完的进 draft参考的外部资料单独放。传输过程遇到过一个问题手机备忘录导出的文本经常在句末带上“来自 XXX 手机”的签名还有一些输入法的全角括号被替换成半角括号。我必须在传输后加一道清洗脚本统一做三件事去掉签名行把半角括号统一为全角把多个连续空行压缩成一个。这道工序我用 Python 脚本跑批处理本质上是逐文件做正则替换后续所有解析工作才不会被这些无关字符干扰。2.3 解析层为什么最终选定了正则表达式很多人一听到“解析”就想到用 BeautifulSoup 或 lxml 去解析 HTML。但我的场景里有大量纯文本文件、少量 HTML 备份以及从剪贴板复制过来的零散段落没有一个统一的格式。BeautifulSoup 擅长处理 HTML但遇到纯文本和半结构化的 Markdown 反而杀鸡用牛刀。我需要一个不依赖数据格式、只依赖内容模式的解析工具最终我选择了正则表达式。正则表达式的优势不在于它能解析一切而在于它能把“文本里的模式”直接转换成“程序认识的字段”。比如一段文章只要我知道标题固定以日期开头、正文以“正文开始”标记起始、标签放在文章末尾那么用三个正则表达式就能把元数据提取干净。代价是正则表达式容易写得脆边界条件一多就会断裂但这可以通过在输入侧设定严格的书写规范来弥补。2.4 归档与查询层让旧文章能再被翻出来解析完成之后所有文章统一转成本地 Markdown 文件并且用一个 metadata 头记录发布日期、修改日期、原文链接、标签列表。归档目录按年份拆分每年一个文件夹每篇文章一个 .md 文件文件名格式是“YYYY-MM-DD-短横线标题.md”。查询层我用的是 Windows 自带搜索加上一个简单的 Python 命令行工具。命令行工具做的事情很朴素输入一个关键词遍历所有 Markdown 文件用正则匹配标题和正文输出命中文件列表。后来我干脆把解析结果导入 SQLite这样就能支持更复杂的组合查询比如“查所有 2022 年发布且带‘正则’标签的文章”。这一步让我真正体会到解析的价值不是把数据拆成字段而是拆完之后还能合起来用。3. 核心实现手把手拆解源码里的每个关键环节3.1 手机键盘侧的输入规范设计要让正则解析稳定工作输入侧必须制定一套“简陋但严格”的书写规范。我最终固定的格式是这样【2021-04-12】从零搭建你的第一个爬虫 正文开始 这里是第一段内容可以包含多个句子。 如果出现代码块我使用四个空格缩进表示像这样 import requests print(hello) 正文结束 标签爬虫, Python, 入门这套规范的要点是日期格式固定为 YYYY-MM-DD标题前面有“【】”符号正文用“正文开始”和“正文结束”包裹标签统一放在末尾用“标签”开头。这看起来非常简单甚至有点死板但绝对的统一格式换来了极高的解析稳定性后面的正则表达式几乎不用处理例外情况。如果你想复现这个项目我强烈建议先把书写规范固定下来否则你会在解析阶段不断遇到“又一个特例”。3.2 用正则表达式清洗原始博文拿到一个 txt 文件后我首先做的操作不是解析而是清洗。清洗分三个步骤每个步骤都用一组正则表达式完成核心代码如下import re def clean_raw_text(raw: str) - str: # 1. 去掉手机签名行比如“来自X米手机” text re.sub(r\n来自.{2,8}手机, , raw) # 2. 压缩多个连续空行为单个空行 text re.sub(r\n{3,}, \n\n, text) # 3. 把半角括号统一成全角括号避免中文环境下显示错乱 text text.replace((, ).replace(), ) # 4. 去掉行尾多余空格 text re.sub(r[ \t]$, , text, flagsre.MULTILINE) return text清洗操作的顺序是有讲究的必须先处理签名行再压缩空行最后才处理标点和行尾空格。如果先压缩空行签名行前后的换行会被一起压缩后续正则替换签名时可能误删正文第一段。这类细节就是实操和理论之间差出来的经验我一开始就吃过顺序颠倒的亏。3.3 从 HTML 页面批量抽取 CSDN 文章数据由于早期有些文章没有在手机端留底稿只有 CSDN 页面本身我还写了一个 HTML 解析器用来从文章页面源码里抽取标题、正文和发布时间。CSDN 的页面结构不同年代差别很大但总有规律可循比如标题通常出现在h1 classtitle-article标签中正文在一个idarticle_content的 div 里。我的抽取函数如下def extract_article_from_html(html: str) - dict: title re.search(rh1[^]*classtitle-article[^]*(.*?)/h1, html, re.S) content re.search(rdiv[^]*idarticle_content[^]*(.*?)/div\s*/div, html, re.S) date re.search(rspan[^]*classtime[^]*(.*?)/span, html, re.S) result {} if title: result[title] re.sub(r[^], , title.group(1)).strip() if content: result[content] clean_html_content(content.group(1)) if date: result[date] date.group(1).strip() return result def clean_html_content(raw_html: str) - str: # 去掉脚本和样式 raw_html re.sub(rscript.*?/script, , raw_html, flagsre.S) raw_html re.sub(rstyle.*?/style, , raw_html, flagsre.S) # 代码块替换pre 标签内容保留换行 raw_html re.sub(rpre[^]*(.*?)/pre, lambda m: \n re.sub(r[^], , m.group(1)) \n, raw_html, flagsre.S) # 段落标签转换为空行分隔 raw_html re.sub(r/p, \n\n, raw_html) # 剩余标签全部去除 text re.sub(r[^], , raw_html) # 还原常见 HTML 实体 text text.replace(nbsp;, ).replace(amp;, ).replace(lt;, ).replace(gt;, ) return text这里需要特别注意正则表达式中非贪婪匹配的使用即.*?。如果你写成贪婪的.*当页面里有多个div标签时匹配结果会一路延伸到整个页面末尾把无关内容全部吞进来。我最初就犯过这个错误提取出来的“正文”里塞满了侧边栏推荐内容和底部广告后来把所有跨行匹配都改成了.*?加re.S标志问题才彻底解决。3.4 完整解析流程从原始文件到结构化 Markdown上面的清洗和抽取都是局部函数真正落地的核心是一个parse_article_file函数把整个文件从原始文本转换为最终的结构化 Markdowndef parse_article_file(filepath: str) - dict: with open(filepath, encodingutf-8) as f: raw f.read() # 先清洗 text clean_raw_text(raw) # 提取日期和标题兼容【2021-04-12】标题 和 2021-04-12-标题 两种格式 meta { date: None, title: None, tags: [], content: } date_title re.search(r【(\d{4}-\d{2}-\d{2})】(.), text) if not date_title: date_title re.search(r(\d{4}-\d{2}-\d{2})[- ](.), text) if date_title: meta[date] date_title.group(1) meta[title] date_title.group(2).strip() # 提取正文 content_match re.search(r正文开始\s*(.*?)\s*正文结束, text, re.S) if content_match: meta[content] content_match.group(1).strip() # 提取标签 tag_match re.search(r标签[:](.), text) if tag_match: meta[tags] [t.strip() for t in tag_match.group(1).split(,)] # 组装为 Markdown 文件内容 md_lines [] md_lines.append(f# {meta[title]}) md_lines.append(f发布日期{meta[date]}) if meta[tags]: md_lines.append(标签 , .join(meta[tags])) md_lines.append() md_lines.append(meta[content]) return { meta: meta, markdown: \n.join(md_lines) }这个函数把前面所有模块串在一起清洗层负责把脏数据变干净正则层负责从干净文本里抽字段最后的 Markdown 组装层则是生成本地归档文件。整个项目的核心逻辑其实只有这三层代码量也就是百来行没有用到任何重型框架。4. 实操过程记录从采集到归档的完整流程4.1 一次完整的博文导入流程我做了一个批处理脚本把所有待处理的 txt 文件统一跑一遍生成 Markdown 文件并按日期归档到对应年份目录。完整流程分五步把手机草稿箱里的文件同步到电脑本地目录。运行清洗和解析脚本生成 Markdown 文件和一个 JSON 索引文件。人工抽查 5 到 10 篇重点看标题有没有提取错、正文有没有被截断。把 Markdown 文件移动到按年份归档的目录。更新 SQLite 数据库把新文章的元数据和全文索引插入进去。脚本的核心调度逻辑很简单用一个for循环遍历目录文件逐个调用解析函数。为了不覆盖已有文件我加了一个判断如果目标 Markdown 文件已存在就自动重命名为“原名_重复.md”。这一步看着不起眼但避免了重复导入导致的数据混乱是我在实际运行中吃过亏之后补上的。4.2 参数与边界条件的处理日期、标签、代码块日期是解析中最容易出问题的字段。我最初只支持【YYYY-MM-DD】一种格式后来发现很多草稿文件用的是2021年4月12日这种写法于是加了一组兼容正则。日期解析的优先级必须固定先匹配标准格式再匹配中文格式如果都没有就默认取文件的修改时间。标签字段也有类似的问题既有全角冒号“标签”又有半角冒号“标签:”解析时候必须两个都匹配。代码块是另一个高频坑。手机端输入代码块时无法像电脑端一样方便地切换代码块标记所以我的规范是“四个空格缩进代表代码块”。解析时我需要把所有四空格缩进的行统一替换成 Markdown 的围栏代码块格式。这个过程用一个简单正则就能做到def convert_indented_code_to_fence(md_text: str) - str: lines md_text.split(\n) in_code False new_lines [] for line in lines: if line.startswith( ): if not in_code: new_lines.append() in_code True new_lines.append(line[4:]) else: if in_code: new_lines.append() in_code False new_lines.append(line) if in_code: new_lines.append() return \n.join(new_lines)这个函数不需要正则但它解决的是正则也容易出错的场景逐行状态机比正则更适合处理跨多行的结构性转换。经验就是能逐行处理用逐行处理不要把简单问题复杂化。4.3 跨设备同步的小技巧手机和电脑之间的文件同步我在不同时期用过不同方案体验差别很大。最初用数据线手动复制最大的问题是忘记同步导致两边文件不一致。后来改用局域网文件共享和自动同步工具终于解决了实时同步问题但随之而来的是同步冲突同一个 txt 文件在手机端和电脑端都被改动生成了“文件名(冲突)”这样的副本。我的解决办法是在手机端固定只做“写入”在电脑端固定只做“读取与归档”。手机端的文件一旦写完并同步出去就不再修改只增不改。电脑端归档完成后把已处理文件移动到archived子目录避免脚本重复处理。数据流向是单向的冲突自然就消失了。这是个人项目里最容易被忽略但收益极大的设计原则。5. 常见问题与排查技巧实录5.1 正则匹配不到内容先查转义和换行我用这套方案跑了半年后发现一个很诡异的现象个别文章的正文提取出来只有第一段后面全部丢失。排查后发现问题出在手机端写正文时用了两个连续换行而我的正则写的却是\s*。理论上\s应该能匹配换行但在某些场景配合re.S标志使用时如果文件末尾有\r\n和\n混在一起就会在正文结束这个标记处提前终止匹配。我的排查思路是分步定位先打印原始文本里“正文结束”附近的字符码确认到底是\n还是\r\n再调整正则。最终我把所有文本统一做了换行符标准化把\r\n全部替换成\n正则解析的稳定性瞬间提高了一个台阶。后来我形成了一条经验凡是正则匹配不到内容先别急着改表达式先去检查文本里的不可见字符。5.2 手机端换行符与电脑端格式不兼容这个问题和上面相关值得单独拿出来说一下。安卓手机上的很多文本编辑器默认使用\n但有些第三方输入法会在粘贴时插入\r\n。当这些文件被传回 Windows 电脑我用记事本打开时一切正常但用 Python 读取时\r会残留在每行末尾导致正则里的$锚点经常失效。解决办法是在脚本开头加一行标准化text raw.replace(\r\n, \n).replace(\r, \n)这行代码看着简单但如果没有它后面所有正则都会像踩在棉花上一样看起来匹配了实际提取出来的文本里全是隐形符号。我在项目文档第一版里甚至专门用红色字体标注了这句话先标准化换行符再做任何正则解析。5.3 文章乱序和重复导入怎么处理因为同步冲突同一个文件可能出现多个副本导入后就会产生重复文章。我的处理方案是建立“文章指纹”用标题加日期的组合作为唯一标识如果 SQLite 里已经存在相同标识就跳过导入。如果标题和日期都相同但正文不同说明有两个版本我会生成一个_v2后缀文件而不是覆盖旧文件。乱序问题则涉及文章编号。有些文章发布时没有固定顺序但在本地归档时我希望文件能按时间排序。处理方式是文件名统一用日期开头脚本自动按日期排序归档月份顺序自然就理顺了。这套逻辑也让我意识到数据自由不仅是能拿到数据还包括数据能按照自己的规则被重新组织和排序。5.4 常见问题速查表为了方便你直接对照排查我把四年里遇到的高频问题整理成了一张速查表问题现象可能原因解决方案正则匹配不到标题标题里有全角空格或特殊符号先调用re.sub(r\s, , text)做归一化正文只提取到一半换行符不一致导致匹配中断统一把\r\n替换为\n代码块丢失缩进HTML 解析时 pre 标签没处理单独补一层pre标签转换逻辑标签提取为空使用了全角冒号但正则只匹配了半角同时匹配[:]重复导入文件同步产生副本用标题日期做唯一指纹校验文件名乱码手机端编码非 UTF-8统一在手机端使用 UTF-8 编码保存我把这张表打印出来贴在了电脑显示器侧面后来每次解析出问题时都会先对着表检查一遍大部分问题在五分钟内就能定位。6. 四年实践下来的几点心得6.1 正则不是万能的但它是性价比最高的如果你的数据本身就是从手机键盘一个一个敲出来的纯文本那正则解析就是最合适的工具。它不需要你维护一个庞大的解析框架也不依赖网络哪怕十年后打开当年的脚本依然能运行。相比之下那些依赖在线 API 的数据导出工具反而不稳定API 升级一次你的代码就要跟着改一次这种“依赖别人的系统拿到自己数据”的方式本身就是对数据自由的一种背离。当然正则也有它的天花板。遇到嵌套结构的 HTML 或者层级复杂的 Markdown正则写起来会很痛苦这时候适当配合逐行处理和 DOM 解析会更好。我的原则是能用简单正则解决的绝不上复杂框架但也不要神化正则该妥协的地方果断妥协。6.2 数据自由的真义不是把数据搬走而是随时能再解析做了四年这个项目我最大的体会是数据自由不是把文章从 CSDN 复制到本地硬盘就算完成而是你能不能在自己掌握的本地系统里对这批数据做任意形式的解析、重组、重新导出。复制只是搬运解析才是拥有。现在哪怕 CSDN 明天改版、甚至关闭我的每一篇文章都已经以标准 Markdown 格式保存在自己的文件系统里可以用本地工具搜索、转换、发布到任意平台。我不用依赖任何人的导出按钮。6.3 给后来者的三个建议第一务必从第一天就规范输入格式。书写规范越严格后续解析越省力不要在源头偷懒否则解析阶段会用十倍的时间还回来。第二先写清洗脚本再写解析脚本。把原始文本中所有可见和不可见的脏字符都处理干净正则解析才能真正发挥作用。第三建立一套自己的唯一标识规则。无论是文章标题加日期还是文件名前缀都要保证同一篇文章不会重复入库这是长期运行数据系统的基本底线。如果你也有大量散落在平台和手机里的文章数据我建议你从今天开始做一个小实验挑五篇文章用最简单的正则表达式提取标题和正文转成本地 Markdown。你会发现一旦迈出这一步后面的事情会越来越顺手最终你收获的不只是数据备份而是一套完全属于你自己的内容管理基础设施。