☰
公众号文章批量抓取与静态博客搭建实战
2026/9/26 15:09:32 网站建设 项目流程

1. 从"翻历史消息翻到手指抽筋"说起

公众号文章有个很别扭的地方:你明明记得某天看过一篇特别有用的干货,但想再找回来的时候,只能在那条对话里往上滑,滑到天荒地老。微信自带的搜索功能对公众号历史文章的覆盖也不算友好,尤其是那些更新频率高、内容量大的号,翻起来简直是体力活。

我这个需求的起点很朴素:我关注了几个更新很勤的公众号,每天推送好几条,时间一长,想找某篇旧文基本靠缘分。试过在聊天记录里搜关键词,结果要么搜不到,要么搜出来一堆不相关的。后来我想,与其每次都在微信里受罪,不如把这些文章"搬"出来,做成一个自己能掌控的博客,想按日期查就按日期查,想按关键词搜就按关键词搜,一秒定位。

这篇就聊聊我具体是怎么搭的,中间踩了哪些坑,以及如果你也想干同样的事,哪些环节可以省事、哪些环节千万别偷懒。整套方案的核心链路其实就四步:批量获取文章链接、抓取正文内容、转成结构化数据、生成静态博客。听起来简单,但每一步都有细节能把人卡住,尤其是批量下载和格式转换这两块。

先说清楚这套东西适合谁:如果你只是偶尔想存几篇文章,直接用微信的"收藏"就够了,没必要折腾;但如果你像我一样,关注的号多、历史文章存量大、又希望有一个统一的检索入口,那自己搭一个博客确实是最舒服的方案。下面按实际搭建顺序展开。

2. 批量拿到历史文章链接:别一上来就写脚本

2.1 先搞清楚你能拿到什么数据

动手之前得先明确一件事:公众号文章的公开访问入口是文章链接,而历史文章的列表并不是随便就能完整拉到的。我的做法是先从自己能接触到的入口把链接收集起来,比如从公众号的推送记录、分享出来的文章链接、以及一些公开的文章聚合页面。这一步的目标只有一个——拿到尽可能全的文章URL清单,正文内容后面再抓。

这里有个经验:不要指望一次性拿到全部历史文章。很多号的文章跨度好几年,链接散落在各处。我的策略是分批收集,先拿到最近半年的,跑通整条链路,再回头补历史数据。这样你能快速验证方案可行性,而不是卡在数据收集阶段就放弃了。

收集链接的时候,我建议直接存成一个纯文本文件,一行一个URL,命名成urls.txt。别小看这个格式选择,后面写脚本读取的时候,纯文本是最省事的,不用解析JSON也不用管编码问题(记得统一存成UTF-8)。

2.2 用脚本批量下载,而不是手动复制

手动复制链接这件事,超过二十条就会让人崩溃。这时候脚本就派上用场了。我用的是最基础的Shell脚本配合循环,逻辑很简单:读一行URL,请求一次,把返回的HTML存下来,文件名用序号或者文章标题。

#!/bin/bash i=1 while read -r url; do echo "正在抓取第 $i 篇: $url" curl -s -A "Mozilla/5.0" "$url" -o "raw/${i}.html" i=$((i+1)) sleep 2 done < urls.txt

这段脚本里有几个关键点值得说。-A参数是设置请求头里的User-Agent,不加的话有些页面会返回空内容或者被拦截。sleep 2是刻意加的延迟,别嫌慢,批量请求如果太密集,很容易触发对方的访问限制,到时候整批都拿不到,反而更亏。我一开始图快把sleep去掉,结果跑到三十多条就开始大量失败,加了延迟之后稳定多了。

提示:抓取频率一定要克制。这不是技术问题,是基本的礼貌问题,也是保证你自己能长期稳定拿到数据的前提。

2.3 文件名别用标题,用日期加序号

我踩过一个坑:一开始想用文章标题当文件名,结果标题里带各种特殊字符——斜杠、问号、冒号、emoji,在文件系统里全是非法字符,脚本直接报错。后来改成日期_序号.html的格式,比如20240315_001.html,世界一下子清净了。

日期从哪来?如果URL里带日期信息最好,没有的话就在抓取的时候从HTML里解析发布时间。实在解析不出来,就按抓取顺序编号,后面人工补日期也行。文件名规范这件事看着小,但当你手上有几千个文件的时候,一个统一的命名规则能救命。

3. 把HTML变成能读的正文:解析比抓取更磨人

3.1 为什么不能直接存HTML就完事

有人可能觉得,HTML存下来不就能看了吗?能看,但不好用。公众号文章的HTML里塞满了各种样式、脚本、广告位、推荐阅读模块,你打开一个文件,真正想看的那几百字正文可能被埋在一堆无关内容里。而且你要做的是博客,需要的是干净的正文内容,不是原始页面。

所以解析这一步的目标很明确:从HTML里把标题、正文、发布时间、作者这几个字段抽出来,其他全部丢掉。这一步用Python做最顺手,BeautifulSoup或者lxml都行。

3.2 定位正文的那几个选择器

公众号文章的正文通常在一个固定的容器里,常见的是#js_content这个id。但不同时期、不同模板的文章结构可能不一样,所以不能死磕一个选择器。我的做法是写一个优先级列表,挨个尝试:

from bs4 import BeautifulSoup def extract_content(html): soup = BeautifulSoup(html, 'lxml') # 按优先级尝试多个选择器 for selector in ['#js_content', '.rich_media_content', 'article']: node = soup.select_one(selector) if node and len(node.get_text(strip=True)) > 100: return node return None

这里加了个长度判断> 100,是为了避免选到一个空的或者只有几个字的容器。实测下来,这个兜底逻辑能覆盖绝大多数情况。如果三个选择器都没命中,那篇文章就标记为"解析失败",单独放到一个列表里,后面人工处理,不要让整个批处理因为一篇失败就中断。

3.3 图片和代码块的处理

正文里的图片是个麻烦事。公众号的图片是外链,直接引用的话,哪天对方图床挂了你的博客就全是裂图。我的处理方式是把图片也下载到本地,存到images/目录,然后把正文里的img标签的src替换成本地路径。这样博客就是完全自包含的,不依赖任何外部资源。

代码块相对好办,公众号的代码块在HTML里通常有特定的样式类,解析的时候保留<pre>和<code>标签的内容就行。但要注意,有些代码块里的换行是用<br>实现的,转成Markdown的时候得把<br>换成真正的换行符,否则代码会挤成一行,完全没法看。

3.4 转成Markdown还是直接存HTML

这是个值得纠结的问题。存HTML的好处是保留原始排版,坏处是文件大、不好编辑、后续处理麻烦。转Markdown的好处是干净、通用、方便二次加工,坏处是转换过程会丢失一些复杂排版。

我最后选了Markdown,理由是:我要的是一个能长期维护、方便检索的知识库,不是原页面的备份。Markdown转出来之后,我还能用各种工具做全文搜索、生成目录、导出PDF。转换用的库是html2text,配置里把body_width设成0(不自动换行),ignore_links设成False(保留链接),基本就够用了。

4. 用Excel做中间层:一个被低估的整理工具

4.1 为什么中间要过一道Excel

你可能会问,抓完直接生成博客不就行了,为什么要用Excel?我的理由是:Excel是一个极好的"人工校对层"。自动抓取难免有错——标题抓错了、日期解析错了、正文抓空了,这些问题在生成博客之前如果不发现,后面就是一堆脏数据。

把每篇文章的元数据(标题、日期、作者、URL、正文文件路径、解析状态)写进一个Excel表格,我可以快速扫一眼,把明显有问题的行标出来手动修。这个环节花十分钟,能省掉后面几个小时的返工。

用Python写Excel很简单,openpyxl或者pandas都行。我习惯用pandas,因为后面做统计和筛选方便:

import pandas as pd data = [] for item in parsed_items: data.append({ 'title': item['title'], 'date': item['date'], 'author': item['author'], 'url': item['url'], 'content_path': item['path'], 'status': item['status'] }) df = pd.DataFrame(data) df.to_excel('articles.xlsx', index=False)

4.2 表格里该放哪些列

列的设计直接决定了这个表格好不好用。我最后定下来的列是:title(标题)、date(发布日期)、author(作者)、url(原文链接)、content_path(正文Markdown文件路径)、status(解析状态)、tags(人工打的标签)。

status这一列特别有用,我用三个值:ok表示解析正常,partial表示正文抓到了但可能不完整,failed表示完全没抓到。筛一下failed的行,集中处理,效率很高。

tags列是我手动加的,因为自动打标签的准确率实在感人。手动打虽然累,但打完之后检索体验完全不一样。我一般按主题打,比如"工具""方法论""案例"这种粗粒度标签,够用就行,不用太细。

4.3 Excel和Markdown表格的互转

这里顺带说一个高频需求:有时候我需要把Excel里的数据转成Markdown表格贴到博客里,或者反过来把Markdown表格转回Excel。手动转太蠢了,我写了个小脚本处理。

Excel转Markdown,用pandas的to_markdown()就行(需要装tabulate):

df.to_markdown('table.md', index=False)

Markdown转Excel稍微麻烦点,得先解析表格结构。简单的做法是按行读,用|分割,去掉首尾的空格和分隔线,然后塞进DataFrame。这个转换不常用,但偶尔需要的时候有个现成脚本能省不少事。

5. 生成静态博客:选对工具,半小时上线

5.1 静态博客生成器的选择

到了生成博客这一步,选择就多了。Hugo、Hexo、Jekyll、VuePress 都能干这活。我选的是Hugo,理由很直接:编译速度快。我手上有几千篇文章,用Hexo编译一次要等好几分钟,Hugo几秒钟就完事。对于需要频繁重新生成整个站点的场景,这个速度差异是决定性的。

Hugo的目录结构很清晰:content/放Markdown文章,layouts/放模板,static/放静态资源,config.toml放配置。我要做的就是把解析出来的Markdown文件按Hugo要求的格式放进content/posts/目录,每篇文章的头部加上front matter(就是那几行---包起来的元数据)。

5.2 Front matter怎么写

Hugo的文章头部需要元数据,格式是YAML:

--- title: "文章标题" date: 2024-03-15 author: "作者名" tags: ["工具", "方法论"] source_url: "原文链接" ---

这里有个细节:date字段的格式必须是Hugo能识别的,我统一用YYYY-MM-DD,最省心。source_url是我自己加的字段,用来在文章底部显示原文链接,方便回溯。

写front matter的时候要注意转义。标题里如果有双引号,得转义成\",否则YAML解析会报错。我一开始没注意这个,结果有几篇文章标题带引号,整个站点生成直接失败,排查了半天才发现是这个小问题。

5.3 按日期归档和全文搜索

博客搭好之后,最核心的两个功能就是按日期浏览和全文搜索。

按日期归档,Hugo自带支持,配置里打开archives就行,它会自动按年月生成归档页面。我还在首页加了一个日期选择器,点某一天就跳到那天的文章列表,这就是标题里说的"想看哪天的文章一秒就能找到"。

全文搜索稍微费点事。静态博客没有后端,搜索得靠前端实现。我用的是lunr.js,思路是在生成站点的时候把所有文章的纯文本内容导出成一个JSON索引文件,前端加载这个索引,用户输入关键词的时候在本地做匹配。几千篇文章的索引文件大概几MB,首次加载稍慢,但之后搜索是瞬时的。

注意:索引文件别把全文都塞进去,只放标题和摘要就够了,否则文件会大到影响加载速度。需要看全文的时候点进文章就行。

5.4 部署和更新

静态博客的部署是最省心的部分。生成出来的public/目录就是一堆静态文件,扔到任何能托管静态资源的地方都能跑。我用的是对象存储加CDN,成本几乎可以忽略不计。

更新流程也很简单:新文章抓取解析完,放进content/posts/,跑一次hugo,把public/同步上去,完事。我写了个一键脚本把这几步串起来,新增文章之后执行一次,几十秒就能在线上看到。

6. 那些让我熬夜的坑,以及怎么绕过去

6.1 编码问题:中文乱码的根源

抓取和解析过程中最容易遇到的就是编码问题。有些页面的HTML声明是GBK,有些是UTF-8,还有的声明和实际不符。如果处理不当,中文全是乱码。

我的处理方式是:不信任HTML里的声明,直接用chardet检测实际编码,然后统一转成UTF-8。这一步在解析之前做,能避免后面所有的乱码问题。

import chardet def decode_html(raw_bytes): detected = chardet.detect(raw_bytes) encoding = detected['encoding'] or 'utf-8' return raw_bytes.decode(encoding, errors='replace')

errors='replace'是兜底,遇到实在解不出来的字符就用占位符代替,保证程序不崩。个别字符乱码可以接受,整个文件读不出来才是灾难。

6.2 脚本闪退和命令找不到

在Windows上跑脚本的时候,我遇到过两个典型问题。一个是"脚本命令闪退",双击运行窗口一闪就没了,根本看不到报错。解决办法是在脚本末尾加pause,或者在命令行里手动执行,这样报错信息能留住。

另一个是"npm不是可运行的程序"这类报错,本质是环境变量没配好。装完Node.js之后,npm的路径没加到PATH里,命令行就找不到。这个问题的排查思路很简单:先确认装没装,再确认PATH里有没有,最后确认版本对不对。别一上来就重装,先看PATH。

6.3 批量下载的失败重试

批量下载的时候,总有一定比例会失败——网络抖动、对方限流、页面结构变了,各种原因都有。我的做法是给每个下载任务记录状态,失败的单独存一个failed.txt,跑完一轮之后对失败列表重试,重试还是失败的再人工看。

重试的时候把延迟调大一点,比如从2秒加到5秒,成功率会明显提升。这个策略我用了很久,基本上两轮之后失败率就能降到很低。

6.4 正文抓空的几种情况

正文抓空是最让人头疼的,因为你不看不知道,一看发现几十篇都是空的。常见原因有几个:一是页面结构变了,选择器没命中;二是文章是纯图片形式发的,没有文字正文;三是文章被删了,页面返回的是错误页。

针对第一种,我的选择器列表里加了兜底逻辑,命中不了就标记failed,不硬抓。针对第二种,纯图片文章我单独处理,把图片下载下来,正文就放图片。针对第三种,检查返回内容里有没有"该内容已被发布者删除"之类的关键词,有的话直接标记为deleted,不浪费时间。

7. 几个让整套流程更顺手的补充技巧

7.1 用标签和全文搜索配合

光有全文搜索还不够,因为搜索是精确匹配,你搜"下载"可能搜不到讲"抓取"的文章。标签的作用就是补这个短板。我给每篇文章打两三个粗标签,搜索的时候可以按标签筛选,再在筛选结果里搜关键词,命中率高很多。

标签体系别搞太复杂,我一开始设计了十几个标签,结果自己都记不住哪个是哪个。后来精简到六七个,反而用得更顺。

7.2 定期备份原始数据

抓取和解析的结果一定要备份,尤其是那个Excel表格和Markdown文件。我吃过亏,有一次误操作把content/目录清空了,幸好Excel表格还在,重新生成一遍就行。如果连表格都没了,那就得从头抓,那才叫绝望。

备份策略很简单:每周把content/、articles.xlsx、urls.txt打包存一份到另一个地方。不用搞什么复杂的增量备份,全量打包就行,反正体积不大。

7.3 导出PDF作为离线备份

有时候我需要把某几篇文章导出成PDF,方便离线看或者分享给别人。Markdown转PDF用pandoc最方便:

pandoc article.md -o article.pdf --pdf-engine=xelatex -V mainfont="Noto Sans CJK SC"

关键是mainfont要指定一个支持中文的字体,否则中文全是方块。这个坑我踩过,导出来的PDF中文全变成豆腐块,排查了半天才发现是字体问题。

7.4 关于自动化程度的取舍

整套流程我并没有做成全自动。抓取和解析是自动的,但打标签、校对标题、处理失败项这些环节我保留了手动。原因很简单:全自动的准确率达不到我的要求,与其花时间调自动化,不如手动处理那百分之几的异常,总时间反而更少。

这个取舍因人而异。如果你只是想要一个能看的备份,全自动没问题;如果你像我一样把这个博客当成日常检索工具,那手动校对这一步值得保留。

8. 我实际用下来的几点体会

这套东西搭好到现在用了大半年,最大的感受是:前期在数据清洗上多花的每一分钟,后期都能省回来。我一开始图快,解析完直接生成博客,结果站上一堆乱码标题和空文章,后来不得不回头重新清洗,反而更费时间。

另一个体会是,工具选型别追求新潮,追求稳定。Hugo不是最时髦的,但它的编译速度和稳定性让我省了很多心。同理,脚本用最基础的Shell和Python就够了,不需要上什么复杂的框架。

最后说个实际的:这个博客最大的价值不是"存了多少文章",而是"找文章有多快"。我现在想找某篇旧文,打开博客,输入关键词或者点日期,几秒钟就定位到了。这个体验是微信里翻历史消息完全比不了的。如果你也有类似的需求,按上面这套流程走一遍,一个周末基本就能跑通。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询