ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

从中文标题到结构化数据:正则解析与静态页生成实践

从中文标题到结构化数据:正则解析与静态页生成实践 整理《为了N》的完结纪念内容时会看到类似“他是我黑暗世界里唯一可以守护的光”这样的标题标题里同时还带着“安希”CP、“豆瓣8.6”、“12年”和“完结纪念”等标签。对普通观众这是一句观后感对开发者它却是一个值得抽成字段的小型文本问题。我们把这段文案作为样本数据设计一个可以从中文标题中提取剧名、评分、年份关键词、CP标签和语录并生成可浏览静态页面的小工具。整个过程不依赖重型框架却能覆盖正则解析、中文清洗、JSON 结构化、静态页面渲染和本地调试等常见知识点。下面先把需求拆清楚再逐步搭建脚本最后补充常见坑和可复用清单。文章的目标是让读者拿到一整套思路后不只是看懂这段《为了N》示例而是能应用到自己的追剧、书影音或内容管理系统里。1. 需求拆解为什么要把“观后纪念文案”转成结构化数据1.1 先把标题当成一条样本数据看我们手头有一个很典型的用户标题“他是我黑暗世界里唯一可以守护的光”⚡️12年了...依然磕安希啊啊啊《为了N》豆瓣8.6|完结纪念这一行里包含了很多信息。自然语言阅读时人能快速区分哪些是语录、哪些是剧名、哪些是个人观感。但机器做不到尤其是中文标题里的书名号、评分、年份标签和 CP 标签经常混在一起。如果只做一次性人工整理完全不需要写程序。可一旦数据量变成几十条、几百条比如一个社区要整理近期完结剧集的“纪念帖”、一个内容运营要统计“哪些 CP 讨论度高”、或者一个个人收藏夹想把每一条动态都做成卡片就必须先把文案解析成统一字段。这个场景的关键点不是“看懂标题在说什么”而是“从声音混杂的自然语言里取出可统计、可筛选、可展示的字段”。1.2 目标字段要贴合标题里的真实信息对上面这一行《为了N》不是代码技术栈但不妨碍我们把它当作领域数据来处理。解析后至少需要出现这些字段字段示例值用途剧名为了N按作品聚合区分本项目属于哪部剧评分字段8.6作为口碑标签展示不修改原数值年份标签12年保留“12年”这样的纪念性说法不强行推测具体日期语录他是我黑暗世界里唯一可以守护的光卡片主标题、文案引言CP 标签安希话题筛选、同类内容聚合动态类型完结纪念判断这条内容属于复盘、推荐还是纪念字段命名时要注意不应该把“12年”直接解释成“播出12年”因为粉丝写下“12年了”的基准点可能各自不同。程序只负责识别标题里出现了年份表述并把原文保留下来。真正的时间统计适合放到另一个步骤里由人工或权威资料确认。1.3 学习环境与生产环境的差异在学习环境里直接用一个 Python 文件解析某个标题就够了不需要考虑并发和权限。但真实生产环境里这条文案可能来自用户发布的动态可能是 HTML 转义后的文本可能包含错别字也可能在“安希”前面出现空格或零宽字符。生产环境至少要多考虑几类问题输入不可信不能把标题内容未转义就写入 HTML。解析规则要版本化不能因为某条新标题改变了匹配逻辑导致历史数据回退。原始字段要保留新增字段只做增量解析不要覆盖源数据。解析结果要有日志和告警尤其是“剧名没识别出来”“CP 标签为空”等情况。下面用最小可复现的方式把学习环境先跑通再逐步讨论可以升级的方向。2. 环境准备与项目结构不引入重型框架也能跑通2.1 运行环境要求解析脚本建议使用 Python 3.10 或更高版本主要原因是类型注解、Path操作和字符串处理在较新版本里更顺手。其实 Python 3.8 以上也能运行只是项目里可以顺手把类型注解写规范便于维护。依赖管理使用标准库venv流程可以固定下来项目说明操作系统Windows / macOS / Linux 均可Python3.10建议用 3.11 或 3.12第三方库emoji仅用于清理文本中的 Emoji浏览器用于预览生成的 HTML 卡片命令行系统自带终端或 PowerShell 均可emoji不是必须的理论上可以自己写正则跳过一部分 Emoji但 Emoji 的编码范围非常杂有的字符由多个码点组合而成。使用emoji.replace_emoji()比自己维护正则更稳妥。2.2 目录结构先规划好项目不要做成单一脚本堆在桌面至少要区分输入、输出和代码。推荐结构如下n_project/ ├── requirements.txt ├── input/ │ └── titles.txt ├── parse_entry.py ├── generate_html.py └── output/ ├── entries.json └── index.html目录职责input/titles.txt放原始文案一行一条。parse_entry.py负责读取输入文件解析字段生成 JSON。generate_html.py读取 JSON渲染成静态 HTML。output/保存每次运行生成的结果避免手改源数据。把“解析”和“渲染”拆成两个文件是因为两个环节的改动频率不同。解析规则频繁变化而页面模板往往相对稳定。拆开后解析出错时可以单独调整parse_entry.py不会影响已经生成好的 HTML 文件。2.3 创建虚拟环境并安装依赖在终端里执行以下命令mkdir -p n_project/{input,output} cd n_project python3 -m venv .venv source .venv/bin/activate如果当前系统是 Windows激活命令要换成.venv\Scripts\activate接着写requirements.txtemoji2.8.0安装依赖pip install -r requirements.txt安装完成后可以用一行命令确认环境已生效python -c import emoji; print(emoji.__version__)能打印出版本号说明后续脚本里的emoji.replace_emoji()可以正常调用。2.4 放一条最小输入样例把下面这行内容写入input/titles.txt“他是我黑暗世界里唯一可以守护的光”⚡️12年了...依然磕安希啊啊啊《为了N》豆瓣8.6|完结纪念这里把用户输入当作原始数据。后续所有解析逻辑都以这一行作为验证样本。需要注意的是输入文件必须保存为 UTF-8 编码。如果用 Windows 记事本保存成 GBK脚本读取中文时很容易出现乱码或UnicodeDecodeError。代码读取文件时也固定指定encodingutf-8不要依赖操作系统默认编码。3. 核心解析实现从原始标题提取字段3.1 先清洗 Emoji 和不可见字符原始标题里有一个雷雨闪电样式的字符即⚡️这串字符通常是两个 Unicode 码点组合而成。如果不对它做清洗后续正则匹配很容易受影响。例如某些正则会因为中间出现 Emoji把本来连续的“文案片段”切断。先定义清洗函数import emoji def clean_text(raw: str) - str: return emoji.replace_emoji(raw, replace_string).strip()清洗后标题会变成“他是我黑暗世界里唯一可以守护的光”12年了...依然磕安希啊啊啊《为了N》豆瓣8.6|完结纪念这里只移除 Emoji不删除中文引号、书名号、数字和小数点因为这些字符本身承载有效字段。3.2 用正则提取剧名、豆瓣评分和年份关键词中文文本里书名号是很好的边界标识。提取剧名时直接找《...》结构即可import re SHOW_PATTERN re.compile(r《(?Pshow[^》])》) RATING_PATTERN re.compile(r豆瓣\s*(?Prating\d(?:\.\d)?)) YEAR_PATTERN re.compile(r(?Pyear\d{1,3})\s*年) def find_first(pattern, text): m pattern.search(text) return m.group(1).strip() if m else 三个正则说明如下《(?Pshow[^》])》取书名号之间的内容剧名可以是中文、英文、数字或符号不限定语言。豆瓣\s*(?Prating\d(?:\.\d)?)先匹配“豆瓣”两个字再匹配整数或小数评分。标题里“豆瓣8.6”会被解析成8.6。(?Pyear\d{1,3})\s*年匹配“12年”这样的文本保留成原格式。从样本里解析出的中间结果是show 为了N rating 8.6 year 12年这里刻意把“12年”当作字符串保存。因为标题表达的是“已经12年了”的感叹并不等于严谨的“电视剧首播于12年前”。解析程序不应该在信息不足时替用户补全语义。3.3 提取语录和 CP 标签时要注意中文语气词语录通常是引号内的内容。可以同时兼容中文双引号和英文双引号QUOTE_PATTERN re.compile(r[“](?Pquote[^”])[”])对样本来说提取结果是他是我黑暗世界里唯一可以守护的光CP 标签相对麻烦。标题里的写法是依然磕安希啊啊啊常规想法是提取“磕”后面的两个字得到“安希”。但中文标题往往伴随“啊啊啊”“呜呜呜”“哈哈哈”这类语气词。如果直接用磕([\u4e00-\u9fff]{2})只匹配两个字符那么“安希”刚好命中但如果文本是“依然磕安希啊”还需要去掉末尾语气词。更稳妥的做法是先扩大匹配范围再对结果做一次尾部清理CP_PATTERNS [ re.compile(r磕(?Pcp[\u4e00-\u9fffA-Za-z0-9]{1,8})), re.compile(r(?Pcp[\u4e00-\u9fffA-Za-z0-9]{1,8})\s*CP), ] def normalize_cp(cp_value: str) - str: if not cp_value: return return re.sub(r[啊吧呀哦哈]$, , cp_value)第一条正则匹配“磕”后面的中文、英文或数字允许 1 到 8 个字符。对样本来说匹配到的是“安希啊啊啊”经过normalize_cp()去掉尾部语气词后得到“安希”。第二条正则用于匹配“安希CP”这类写法。实际项目里两条规则可以同时保留。3.4 把解析结果统一组装成字典把前面所有方法组合成一个parse_entry()函数def parse_entry(raw: str) - dict: cleaned clean_text(raw) show rating year quote cp if SHOW_PATTERN.search(cleaned): show SHOW_PATTERN.search(cleaned).group(1).strip() if RATING_PATTERN.search(cleaned): rating RATING_PATTERN.search(cleaned).group(1).strip() if YEAR_PATTERN.search(cleaned): year YEAR_PATTERN.search(cleaned).group(1).strip() if QUOTE_PATTERN.search(cleaned): quote QUOTE_PATTERN.search(cleaned).group(1).strip() for cp_pattern in CP_PATTERNS: m cp_pattern.search(cleaned) if m: cp normalize_cp(m.group(1).strip()) break memo 完结纪念 if 完结纪念 in cleaned else return { original: raw, show: show, rating: rating, year_label: year, quote: quote, cp: cp, memo: memo, }这个函数虽然基于样本设计但已经考虑了可能的空值情况。当某条标题里没有语录时quote会保持空字符串不直接导致程序崩溃。3.5 批量读取输入并生成 JSON解析函数只处理单条文本外层需要一个批量入口import json import sys from pathlib import Path def main(): input_path sys.argv[1] if len(sys.argv) 1 else input/titles.txt records [] with open(input_path, r, encodingutf-8) as f: for line_no, line in enumerate(f, 1): line line.strip() if not line: continue record parse_entry(line) records.append({ line_no: line_no, **record, }) output_path Path(output/entries.json) output_path.parent.mkdir(parentsTrue, exist_okTrue) with open(output_path, w, encodingutf-8) as f: json.dump(records, f, ensure_asciiFalse, indent2) print(fparsed {len(records)} records) print(fwritten to {output_path}) if __name__ __main__: main()保存为parse_entry.py后执行python parse_entry.py得到output/entries.json[ { line_no: 1, original: “他是我黑暗世界里唯一可以守护的光”⚡️12年了...依然磕安希啊啊啊《为了N》豆瓣8.6|完结纪念, show: 为了N, rating: 8.6, year_label: 12年, quote: 他是我黑暗世界里唯一可以守护的光, cp: 安希, memo: 完结纪念 } ]JSON 输出有两个细节需要注意。第一ensure_asciiFalse会保留中文避免输出成\u4e3a\u4e86N提升可读性。第二original字段保留完整原始文案即使未来清洗规则变化历史数据也不会丢原始信息。4. 渲染成静态卡片让数据变为页面4.1 页面为什么要做成静态 HTML解析完成后数据已经变成 JSON。如果只是在终端里打印字段很难观察效果也不方便给非技术同事看。更好的做法是把 JSON 渲染成一个简单页面。选择静态 HTML 而不是后端模板系统有三个原因个人项目中数据量不大不需要数据库和后台服务。静态文件可以直接用python -m http.server预览也能上传到对象存储或 GitHub Pages。渲染脚本只执行一次不依赖在线服务离线环境也能看结果。4.2 写一个 HTML 渲染脚本先准备一个最小generate_html.py作用是读取entries.json把每一条记录渲染成卡片并输出到index.html。代码如下import html import json from pathlib import Path HTML_TEMPLATE !DOCTYPE html html langzh-CN head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title剧集纪念条目展示/title style body { font-family: system-ui, -apple-system, Segoe UI, sans-serif; background: #f7f5f0; margin: 0; padding: 32px; } .container { max-width: 960px; margin: 0 auto; } .card { background: #fff; border-radius: 16px; padding: 24px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06); margin-bottom: 24px; border-top: 6px solid #7c3aed; } .badge { display: inline-block; background: #2d2a32; color: #fff; font-size: 12px; padding: 2px 10px; border-radius: 999px; margin-bottom: 12px; } .card h2 { margin: 0 0 8px; color: #18181b; } .quote { font-size: 20px; line-height: 1.8; color: #27272a; border-left: 4px solid #c4b5fd; padding-left: 16px; margin: 16px 0; } .meta { display: flex; flex-wrap: wrap; gap: 8px; color: #52525b; font-size: 14px; } .meta span { background: #f4f4f5; padding: 4px 10px; border-radius: 8px; } /style /head body div classcontainer !-- CARDS -- /div /body /html def build_card(record: dict) - str: memo html.escape((record.get(memo) or 条目)) show html.escape((record.get(show) or 未知剧名)) quote html.escape((record.get(quote) or 暂无语录)) rating html.escape((record.get(rating) or 暂无)) return f article classcard span classbadge{memo}/span h2{show}/h2 p classquote{quote}/p div classmeta span豆瓣 {rating}/span span{html.escape(record.get(year_label) or )}/span spanCP: {html.escape(record.get(cp) or 未设置)}/span /div /article def main(): entries_path Path(output/entries.json) records json.loads(entries_path.read_text(encodingutf-8)) cards \n.join(build_card(record) for record in records) page HTML_TEMPLATE.replace(!-- CARDS --, cards) output_path Path(output/index.html) output_path.write_text(page, encodingutf-8) print(fwritten to {output_path}) if __name__ __main__: main()这里有一个容易被新手忽略的点模板内插值前必须做 HTML 转义。如果直接把用户输入的quote拼进 HTML一旦文本里包含script就会形成脚本注入。用html.escape()把类似、、转成安全形式是渲染用户内容时不可省略的步骤。4.3 启动本地 HTTP 服务查看效果执行渲染脚本python generate_html.py然后启动本地静态服务python3 -m http.server 8000 -d output浏览器访问http://localhost:8000正常情况下会看到一张卡片。卡片顶部有“完结纪念”的徽标下面显示《为了N》的剧名、语录、豆瓣 8.6、12年和CP: 安希。如果只是临时看效果也可以直接用浏览器打开output/index.html。不过推荐使用静态服务因为后续如果页面里加入了fetch(entries.json)直接双击 HTML 文件触发浏览器跨域限制可能拿不到数据。4.4 通过响应结果验证脚本是否正常本地服务启动后可以在另一个终端执行curl -s http://localhost:8000/entries.json如果终端能打印出刚刚生成的 JSON说明文件路径和服务端口正确。再把返回内容里的show字段是否等于“为了N”作为验证点避免出现页面已打开但数据是旧版本的情况。5. 本方案可能遇到的坑和排查路径5.1 常见问题与解决方案速查下面这张表格总结了整个流程里最容易遇到的几类问题问题现象常见原因检查方式处理建议运行python parse_entry.py后报ModuleNotFoundError当前虚拟环境没激活或未安装 emoji执行pip list查看依赖激活虚拟环境后重新pip install -r requirements.txtJSON 文件里中文变成了\u4e3a写文件时没有指定ensure_asciiFalse用文本编辑器打开 JSON 查看在json.dump中加入ensure_asciiFalseCP 标签解析成“安希啊啊啊”正则范围包含语气词输出中间结果查看cp原始值用normalize_cp()去掉末尾语气词剧名匹配为空文本中的书名号不是中文全角符号查看原始输入文件的字符编码统一使用全角《》或补充半角 规则HTML 页面上语录出现乱码HTML 文件编码不是 UTF-8到浏览器 view-source 查看 meta 标签写文件时固定encodingutf-8重复执行后看到旧数据脚本运行失败文件没被覆盖查看终端是否有报错输出先保证entries.json生成成功再执行渲染5.2 从现象倒推原因不要直接改正则当解析结果不符合预期时建议按以下顺序排查不要上来就调整正则表达式。第一先看输入文本有没有被正确读取。在parse_entry()开头临时加一行print(raw)确认原始文本里没有乱码或隐藏字符。第二看清洗后的文本。Emoji 可能被移除也可能被替换成空字符串后留下多余空格。先打印cleaned确认后续正则面对的是干净文本。第三单独验证字段规则。例如只保留剧名正则在命令行测试python -c import re; print(re.findall(r《([^》])》, 《为了N》))这种最小化验证可以快速定位是正则写错还是整体流程传参有问题。第四查看 JSON 是否真的覆盖成功。如果文件时间戳没变化说明脚本可能在读取阶段就抛了异常。生产环境建议用日志记录输入路径、读取行数和输出条数避免静默失败。5.3 中文文本边界的额外风险中文文本处理最麻烦的是“边界不清晰”。英文单词有空格分隔中文往往连续书写。例如“依然磕安希啊啊啊”里“安希”和“啊啊啊”之间没有分隔符。如果只按正则提取很可能拿到多余内容。一种做法是建立受控词典。对于剧名、CP 名这类封闭集合与其完全依赖正则不如在解析后增加一层“候选人比对”并把比对结果存入日志。比如已知候选里有“安希”清洗后得到的标签如果包含“安希”再截取出来。这种方法更稳但需要有人维护候选列表。它可以放到后续的配置文件中而不是写死在代码里。6. 最佳实践从单人脚本进化为可维护的小工具6.1 不要把所有解析逻辑堆在一个文件里这个示例能跑通但继续增加字段时
返回列表