Python爬虫实战:批量抓取微信公众号历史文章并存入SQLite

发布时间:2026/10/3 1:28:33
Python爬虫实战:批量抓取微信公众号历史文章并存入SQLite 公众号没开放批量导出网页端又不给复制全文我为了把某个号的历史文章整理成资料库硬是用 Python 把抓取链路整条跑通了。这篇文章就把完整方案写出来从登录态的获取、列表接口的翻页、正文解析到入库、断点续爬和常见风控问题照着做就能从最新一篇一直翻到最早一篇把标题、摘要、作者、封面、正文、发布时间全部落成本地 SQLite 数据库。适合想批量整理公众号内容的人也适合想通过真实场景练手 Python 爬虫的同学。先泼一盆冷水这不是一个“填个 URL 就全自动”的开箱工具因为微信的接口参数会变、风控也会升级。但它是一套可以复现的方法论。我实际跑下来几百篇文章的公众号控制好节奏半小时内能抓完全程没触发严重风控。下面直接进入正题。1. 整体思路与方案选型1.1 公众号文章列表背后到底走的是什么接口很多人第一反应是去搜狗微信搜索或者各种第三方聚合站。搜狗确实能搜公众号文章但它只提供最近十页的收录结果而且搜索词是必要条件根本没法把一个号的历史文章全量拉出来。第三方聚合站数据不全、更新滞后还经常包装成付费服务。真正靠谱的入口是微信自己在用的“历史消息”页面。你在微信里点开任意公众号右上角的“历史消息”其实访问的是mp/profile_ext?actionhome__biz...这个地址。浏览器打开后页面往下翻时会继续调用mp/appmsglist?actiongetmsg这个 JSON 接口把文章以分页形式返回。这个接口返回的字段很全文章 ID、标题、摘要、作者、封面图、发布时间、原文链接如果图文包含多篇文章还会在multi_app_msg_item_list里返回子文章列表。所以抓取的核心不是去解析 HTML 正文页面而是先攻下这个列表接口。拿到列表数据后再顺着content_url去抓每篇文章的正文 HTML最后用解析器把正文提取出来。整体链路很清晰列表接口负责“翻目录”正文页面负责“取内容”。1.2 常见抓取方式横向对比我在选型时列过一张表把能想到的途径全过了一遍方案能做到全量吗主要限制搜狗微信搜索否只能按关键词搜收录不全页码受限第三方聚合站点/API否数据源不稳定常有延迟和缺漏微信公众平台后台素材接口否仅限自己账号且需要公众号管理员权限手机端物流化手动复制否效率极低碰到长文想死appmsglist列表接口直接抓是需要登录态和 token要控频冰点的问题就在“登录态”上。微信不会让匿名用户翻历史列表所以你需要用浏览器扫码登录一次把 Cookie、appmsg_token、pass_ticket等参数拿到手。这不是绕过风控只是让程序使用一个正常用户的身份去访问公开内容。1.3 这套方案到底能拿到哪些数据实测下来单篇文章可以稳定拿到以下字段文章唯一 IDmsgid标题、摘要、作者封面图 URL、原文链接发布时间Unix 时间戳图文消息里包含的所有子文章标题和链接正文 HTML 和纯文本通过正文页解析意味着你可以用这套数据做本地全文搜索、生成离线 PDF、导出成 Markdown 笔记库或者做数据分析。这也正是大多数人来写这类脚本的真实需求。2. 准备工作拿到“敲门砖”和运行环境2.1 从公众号主页找到__biz参数__biz是每个公众号的唯一标识形如MzA3ND...一长串 Base64 风格的字符串。它藏在公众号主页链接里。获取路径手机微信打开目标公众号 → 点右上角“...” → 找到“历史消息”或“更多资料” → 复制链接。链接长这样https://mp.weixin.qq.com/mp/profile_ext?actionhome__bizMzA3NDExxxxxxxxxscene124from...把这个链接里的__biz参数值复制出来备用。这是后面所有请求都需要的身份标识。2.2 浏览器扫码一次性拿到 Cookie 和 token在 Chrome 中打开上面复制出来的历史消息链接会跳出一个二维码扫码确认后页面就能正常加载。此时按 F12 打开开发者工具切到 Network 面板勾选 Preserve log再手动刷新页面并往下滚动几次找到profile_ext或appmsglist开头的请求。需要从请求头里复制三样东西完整的 Cookie 值请求头里那一大串查询字符串里的appmsg_token查询字符串里的pass_ticket注意这三个值都跟扫码登录的会话绑定过一段时间可能失效失效后重新扫码一遍即可。提示别把这个会话泄露给任何人。它等同于你的微信网页登录态云端爬虫如果被别人拿去后果跟微信账号被盗差不多。2.3 Python 环境和依赖库本地建议用 Python 3.8 及以上版本。只需要四个依赖尽量装最新版pip install requests beautifulsoup4 lxml html2textrequests 负责发 HTTP 请求beautifulsoup4 和 lxml 负责解析 HTML 正文html2text 负责把正文 HTML 转成 Markdown。如果你想存 JSON 或纯文本html2text 可以不要但我个人强烈建议留一个因为公众号正文的 HTML 结构很乱直接转 Markdown 后扔进笔记软件阅读体验会好很多。3. 核心实现从列表到正文一把梭3.1 拉取文章列表接口的请求与响应结构拿到__biz、Cookie 和 token 之后先用一个 requests.Session 把基础请求头设置好。请求头里必须带Referer: https://mp.weixin.qq.com/否则接口容易返回异常。列表接口真实请求长这样import requests session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Referer: https://mp.weixin.qq.com/, Cookie: 这里粘贴你的完整Cookie }) url https://mp.weixin.qq.com/mp/appmsglist params { action: getmsg, __biz: 这里填__biz参数, f: json, from_msgid: 0, count: 10, is_ok: 1, scene: 124, wxtoken: , appmsg_token: 这里填appmsg_token, pass_ticket: 这里填pass_ticket } resp session.get(url, paramsparams) data resp.json()第一次请求时from_msgid传 0代表从最新一篇文章开始。返回的 JSON 里最关键的是general_msg_list字段。注意它不是一个 JSON 对象而是一个 JSON 字符串必须二次解析这是新手最容易踩的坑import json if data.get(ret) 0: msg_list json.loads(data[general_msg_list]) for item in msg_list[list]: comm item[comm_msg_info] ext item[app_msg_ext_info] msgid comm[id] title ext[title] publish_time comm[datetime] content_url ext.get(content_url, )content_url字段是相对路径请求正文前要拼上https://mp.weixin.qq.com前缀。另外微信返回的content_url里带有符号是正常的转义直接使用即可。3.2 翻页逻辑从最新往最老单向推进列表接口的翻页不是页码式而是游标式。每一页返回 10 条文章拉到页尾时把当前页最后一条的comm_msg_info.id作为下一页的from_msgid。具体流程是第一次请求from_msgid0拿到第一页。从返回结果里取最后一条的id。把from_msgid换成这个 id重新请求。重复直到can_msg_continue返回 0或者返回的列表为空。示例代码from_msgid 0 while True: params[from_msgid] from_msgid resp session.get(url, paramsparams) data resp.json() if data.get(ret) ! 0: print(接口返回异常, data.get(err_msg)) break msg_list json.loads(data[general_msg_list]) items msg_list.get(list, []) if not items: break # 处理当前页文章 for item in items: # 本文省略处理逻辑见下一节 pass last_id items[-1][comm_msg_info][id] if data.get(can_msg_continue) 0 or last_id from_msgid: break from_msgid last_id time.sleep(random.uniform(3, 8))last_id from_msgid这个判断很重要防止某些情况下微信返回了完全相同的列表导致死循环。我在实际调试中就遇到过因为参数格式不对连续几页返回同一批数据差点把请求刷爆。3.3 处理图文消息里的多篇文章很多公众号发文章时不是只发一篇而是一条图文里带“头条 次条”。列表接口返回时app_msg_ext_info是头条文章is_multi为 1 时multi_app_msg_item_list里还会带一串子文章。子文章的字段和头条类似同样有title、content_url、cover等。如果只处理头条漏掉的次条可能比你想象的多得多。我抓过一个科技号两年内 600 篇文章里图文合集里藏的子文章有 40 多篇全是那种“每日快讯”的延伸阅读。所以处理每条数据时必须把multi_app_msg_item_list也遍历一遍。def process_items(items): result [] for item in items: comm item[comm_msg_info] ext item[app_msg_ext_info] result.append(extract_article(comm, ext)) if ext.get(is_multi) and ext.get(multi_app_msg_item_list): for sub in ext[multi_app_msg_item_list]: sub_copy dict(ext) sub_copy.update(sub) result.append(extract_article(comm, sub_copy)) return result注意子文章没有独立的发布时间继承头条的comm_msg_info即可。3.4 正文抓取与内容净化列表接口拿到的是文章摘要和链接要获取完整正文还得顺着content_url请求文章详情页。正文在 HTML 里位于div idjs_content中但也有的页面结构是老版classrich_media_content解析时两个都要兼容。正文 HTML 有三个必须处理的细节不处理就会出现“抓回来一堆代码”或者“图片全是裂图”的问题第一个是图片懒加载。微信正文里img标签的真实地址在>from bs4 import BeautifulSoup def parse_content(html: str): soup BeautifulSoup(html, lxml) content soup.find(div, idjs_content) or soup.find( div, class_rich_media_content ) if not content: return , for tag in content.select(.js_share_source, .rich_media_tool, .qr_code, .rich_media_tool__ft, .poem, .toast): tag.decompose() for img in content.find_all(img): src img.get(data-src) or img.get(src) img[src] src or if src: img[data-src] src for a in content.find_all(a): href a.get(href, ) if href.startswith(//): a[href] https: href elif href.startswith(/) and not href.startswith(//): a[href] https://mp.weixin.qq.com href content_html str(content) content_text content.get_text(\n, stripTrue) return content_text, content_html纯文本用get_text就行但注意get_text会把段落之间的换行吃掉所以用\n作为分隔符。想要更好看的结果可以用html2text.HTML2Text().handle(content_html)转成 Markdown。import html2text h html2text.HTML2Text() h.ignore_images False h.body_width 0 markdown_text h.handle(content_html)4. 把数据存起来并且做到可增量续爬4.1 用 SQLite 保存文章信息所有文章信息统一落库我选择了 SQLite。零配置、单文件、好迁移对这种单机个人抓取任务足够用。建表语句如下CREATE TABLE IF NOT EXISTS articles ( msgid INTEGER PRIMARY KEY, title TEXT, author TEXT, digest TEXT, url TEXT, cover TEXT, publish_time INTEGER, content_text TEXT, content_html TEXT );msgid是列表接口里文章的唯一 id直接设为主键天然去重。content_text和content_html分别存纯文本和 HTML 原文方便后续生成 Markdown 或做全文检索。你也可以再加一个content_md字段在抓取时直接用html2text转换后存入。4.2 断点续爬中断后再也不用从头开始全量爬取几百篇文章时网络抖动、接口反爬、token 过期都有可能中断。如果每次都从头开始抓既浪费时间又增加被风控的风险。正确做法是把抓取进度记录到单独的配置表或本地文件中。最简单的方案是维护一个crawl_state.txt每次成功处理完一批就把当前的from_msgid写进去。下次启动脚本时读取这个文件从上次断点继续。def save_state(value): with open(crawl_state.txt, w, encodingutf-8) as f: f.write(str(value)) def load_state(): try: with open(crawl_state.txt, r, encodingutf-8) as f: return int(f.read().strip()) except (FileNotFoundError, ValueError): return 0配合INSERT OR IGNORE插入数据库即使因为翻页重叠导致数据重复获取数据库也不会出现重复行。这就是最简单可靠的增量续爬方案。4.3 图片下载与防盗链处理如果想把图片也下载到本地让资料库完全离线可用需要在下载图片时额外设置Referer。微信图床mmbiz.qpic.cn有防盗链不带 Referer 很容易返回 403。img_headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://mp.weixin.qq.com/ } def download_image(url, filepath): resp requests.get(url, headersimg_headers, timeout15) if resp.status_code 200: with open(filepath, wb) as f: f.write(resp.content)图片统一按msgid/图片序号命名存放目录建议与数据库文件同级方便后续生成离线电子书时引用本地路径。5. 风控规避与常见问题排查实录5.1 为什么会被风控怎么样把风险降到最低微信对接口的请求频率非常敏感。我在测试时有过惨痛教训脚本不加延迟连续翻页翻到第六七页直接弹出滑块验证整个会话进入观察状态短时间内什么都抓不了。后来总结出一套比较稳的节奏每次翻页之间加 3 到 8 秒的随机延时每抓完 30 篇文章停 30 到 60 秒单次任务总量在 1000 篇文章以内时分几批跑中间间隔几小时请求头里的 User-Agent 用真实浏览器 UA不要用默认 requests 的 UA如果你的需求很大比如上万篇文章更稳妥的做法是拉长总时长甚至分几天完成。公众号历史文章不会跑你的 IP 和账号如果被限制了反而更麻烦。注意一旦遇到滑块验证码立刻停止脚本不要再尝试重试。等待一段时间后重新扫码登录再继续。反复触发滑块会把这个会话的风控等级拉得很高得不偿失。5.2 高频问题速查表现象可能原因排查方向ret返回非 0提示invalid参数缺失、参数过期重新复制appmsg_token、pass_ticket、Cookie连续几页返回完全相同列表from_msgid格式错误或类型不对确认传入的是 int不是字符串翻几页后列表为空但can_msg_continue1触发风控接口静默封禁停止任务降低频率隔段时间再试正文图片全挂图片防盗链给图片请求加Referer: https://mp.weixin.qq.com/解析出来的正文为空页面结构变化在浏览器里查看当前文章 DOM重新定位正文节点老文章链接 404原文章已删除或迁移放弃该链接保留列表元信息5.3 接口路径和参数变了怎么办微信的接口不是永远不变。我最早跑的时候用的是profile_ext?actiongetmsg后来又出现过appmsglist这个新路径再过段时间可能又会加新的参数。遇到接口 404 或者参数报错别急着改代码猜老老实实重新打开历史页面用浏览器 DevTools 抓一次当前最新的实际请求把 URL、参数名、请求头逐一对照更新即可。这套“抓包 → 对照 → 适配”的流程才是这类脚本的核心能力。5.4 老文章链接失效的兜底策略公众号历史上有些文章确实会被删除、设为“已删除”或转为“部分可见”。列表接口里还能看到元信息但点击content_url时可能返回“该内容已被发布者删除”。这时候我的策略是保留列表信息把content_text置为空字符串标注statusdeleted不做强制重试。这种文章占比通常很低不值得为它死磕。6. 合规、边界与长期使用建议6.1 哪些用途危险哪些用途安全我把话说明白这篇文章提供的方法适合个人做内容归档、本地检索、学习研究前提是你对内容的使用符合版权规范。不要做以下事情未授权转载到其他公共平台打包售卖他人公众号的原创内容抓取后用于流量造假、恶意营销构建高并发采集服务对目标接口持续施压使用任何爬虫技术时都要遵守目标平台的用户协议、法律法规以及版权要求。技术本身是中性的但用法有边界。对自己负责也对内容创作者负责。6.2 接口失效后的快速适配思路微信网页端接口每隔一段时间就可能发生变化依赖这套方案长期热更新不现实。我的建议是把关键参数配置化不要硬编码在代码里改起来会舒服很多。config { biz: __biz值, cookie: 完整Cookie, appmsg_token: token, pass_ticket: ticket, }保存成config.json抓包更新后只需要改配置文件脚本主体不用动。如果你要管理多个公众号用字典维护多份配置也很方便。6.3 更好的替代和扩展方向如果只是偶尔想要某几篇文章的离线版本完全没必要写脚本直接在微信里使用“在浏览器打开”再转 PDF 即可。批量场景下这套抓取脚本跑完的数据还可以继续扩展成很多有价值的东西把全量正文导入 Elasticsearch 做全文检索、生成公众号专属 RSS 订阅、定期增量更新并推送摘要到自己的私人笔记。把它当成一个数据管道的第一环价值会比“抓完就完事”大得多。我个人在实际操作中的体会是写这个脚本最耗时间的不是代码而是调试各种边界情况。比如图文合集到底怎么处理、老文章链接失效时怎么兜底、翻页到最后怎么判断结束。这些细节一次一次踩坑之后形成的经验才是这篇内容真正值钱的部分。另外最后再分享一个小技巧每次抓完数据先别着急转 PDF 或 Markdown把原始 JSON 和 HTML 都留一份等哪天想换个姿势重新整理数据时不需要重新访问微信离线就能搞定。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询