尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

微信公众号文章爬虫实战:从requests到反爬处理的完整技术路线

微信公众号文章爬虫实战:从requests到反爬处理的完整技术路线 简介基于Python实现的微信公众号文章爬虫源码专为Python大作业与公众号数据采集场景设计面向需要快速抓取指定公众号文章标题、作者、链接、图标等信息的初学者或开发者。压缩包共8个文件以backstage.py主程序和项目说明.md为核心辅以多个JSON配置、一个SQLite数据库文件以及.gitignore和.suo工程文件整体仅171KB轻量便于部署。程序仅需调用spider(count, offset)一个外部接口即可交互式输入Fiddler抓取的请求头按设定步长与偏移量抓取数据返回包含字典列表的采集结果、当前offset、当前count及成功标志。代码注释详细项目说明清晰且对Fiddler抓包获取请求头的方法做了必要交代能帮助理解请求头构造、数据解析和异常处理思路。目前已有933人学习下载适合作为课程设计、毕业设计或爬虫入门阶段的完整参考项目。1. 公众号文章爬虫这个 Python 大作业难点从来不在 requests微信没有面向开发者的公开文章搜索 API网页端能看到的文章列表从数据流到页面渲染又夹着好几道参数校验于是“基于 Python 实现微信公众号文章爬虫”成了 Python 大作业里最常被选、也最容易翻车的题目。打开那份源码包常见的文件组织是spider.py、parser.py、storage.py外加一份项目说明文档代码行数并不夸张但决定作业能不能交付、说明文档能不能写扎实的关键往往不在循环和正则里而在你对自己“数据从哪里来、参数怎么填、被封了怎么退”的理解深度。这篇文章按一条能落地的技术线展开先讲微信公众号文章抓取的几条常见路径和 URL 关键参数再给可运行的爬虫骨架和正文解析方案接着处理请求频率、签名过期、验证码等异常最后用三个能写进项目说明的技巧收尾。新手能照着一套代码跑通做过几年工程的也能在参数边界和异常兜底上看到实际细节。2. 抓取路径选择搜狗微信搜索和公众号主页 profile 怎么分工在写爬虫之前先要分清微信公众号文章爬虫有两条完全不同的数据入口它们的 URL 结构、请求头要求、可抓取范围和触发反爬的概率都不一样。这个选择会直接影响你要不要维护一个 cookie 池也要决定项目说明文档里怎么描述数据来源。2.1 搜狗微信搜索结果快但抓不到完整历史搜狗微信搜索weixin.sogou.com是目前少数对普通浏览器开放的文章关键词搜索入口请求 URL 大致长这样https://weixin.sogou.com/weixin?type2queryPython爬虫ieutf8其中type2表示搜索文章query是关键词。用requests直接请求这个地址拿到的是搜狗服务端拼出来的搜索列表每条记录包含文章标题、摘要、来自哪个公众号、发布时间以及一个带sn参数的跳转链接。这个入口有几个限制。关键词搜索只能覆盖最近一段时间内的文章没有稳定的历史翻页机制做公众号全量归档不现实搜狗对无 cookie 的新会话限制很严连续搜索十几个关键词就可能弹验证码摘要里不包含正文抓下来之后还要跳到原文链接二次请求。我一般把搜狗方式定位成“关键词舆情采集”或“作业演示的兜底方案”。要抓公众号历史文章下面介绍的 profile 方式才是主路径。2.2 公众号主页 profile 的actiongetmsg是历史文章主入口你在手机微信里点开某个公众号再进入“历史文章”时微信客户端实际是向mp.weixin.qq.com/mp/profile_ext发了一个带参数actiongetmsg的请求。这个接口按offset分批返回公众号已发布的文章列表同一公众号翻到底就是一份按时间倒序排的全量文章索引。抓包后发现最能打动微信服务端的字段是下面几个参数含义变化规律__biz公众号唯一标识一个公众号一个固定值长期不变appmsg_token校验请求来源的会话令牌随登录会话变化过期要重抓cookie微信网页端认证串含多个键值过期后整段替换offset文章列表偏移量从 0 开始每页最多 10 篇每次请求递增__biz是三个值里最值得保存的。拿到一个公众号的__biz后只要手里还有一份没过期的 cookie 和appmsg_token就能持续拉这篇文章列表反过来三样里任何一样失效响应 JSON 里就不再包含正常文章列表字段。2.3 biz、appmsg_token、cookie 怎么抓以及有效期判断常见做法是用微信 PC 客户端的公众号推送缓存来抓取消息 ID但更通用、更可控的方式还是在浏览器里手动抓包用 Chrome 打开任意一篇公众号文章在开发者工具的 Network 面板里过滤profile_ext或/mp/请求从请求 URL 和请求头里分别复制__biz、appmsg_token、Cookie。这几个值不是无限期的。appmsg_token通常跟随浏览器会话几小时到几天不等Cookie中有多个有效期字段过期时间也不统一。判断是否过期的可靠方法不是看时间而是直接发一次请求看返回码。代码里我会把这三个值放到config里每次启动时先发一个offset0的探测请求ret 0才继续跑。2.4 requests 爬虫骨架和翻页逻辑代码把参数拼起来就是一个能跑通的最小骨架import json import requests import time def get_articles(biz: str, appmsg_token: str, cookie: str, max_count: int 50): base_url https://mp.weixin.qq.com/mp/profile_ext headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36, Cookie: cookie, Referer: https://mp.weixin.qq.com, } offset 0 all_list [] while offset max_count: params { __biz: biz, action: getmsg, f: json, offset: offset, count: 10, is_ok: 1, scene: 126, uin: , key: , pass_ticket: , wxtoken: , appmsg_token: appmsg_token, x5: 0, } resp requests.get(base_url, paramsparams, headersheaders, timeout10) data resp.json() if data.get(ret) ! 0: print(请求失败返回码:, data.get(ret)) break msg_list json.loads(data.get(general_msg_list, {})) items msg_list.get(list, []) all_list.extend(items) if not data.get(can_msg_continue): break offset 10 time.sleep(1) return all_list这段代码把参数做成字典交给requests拼接 URL避免拼字符串时漏掉字段。ret 0表示成功general_msg_list本身是字符串要再json.loads一次can_msg_continue为 0 表示没有更多文章。while offset max_count是保护条件防止本地调试时一次把全量文章拉完实际全量抓取时去掉即可但要承担对应风险。3. 正文解析、图片补全和 SQLite 落库从get_articles拿到的是文章元数据和链接正文还在另一组 URL 里。这一章完成从“文章列表”到“可存档文章内容”的转换是项目说明里最方便展示代码量的部分。3.1 正文区域节点和懒加载图片的结构特征文章列表里每项的app_msg_ext_info包含title、digest、cover、datetime和content_url。其中content_url常见形态是这样https://mp.weixin.qq.com/s/xxx?chksmxxxscene126sessionidxxxkeyxxxsubscene0clicktimexxxenteridxxx直接用这个 URL 请求就是文章详情页 HTML。正文集中在div idjs_content容器里。微信开启懒加载后正文里的图片节点通常长这样src为空真实地址在>from bs4 import BeautifulSoup def parse_content(html_text: str): soup BeautifulSoup(html_text, lxml) content_div soup.find(div, idjs_content) for img in content_div.find_all(img): real_src img.get(data-src) if real_src: img[src] real_src img.attrs {src: img.get(src, )} title soup.find(h1, class_rich_media_title).get_text(stripTrue) author soup.find(a, idjs_name).get_text(stripTrue) return { title: title, author: author, content: content_div.get_text(\n, stripTrue), }把>from datetime import datetime def normalize_ts(ts: int) - str: return datetime.fromtimestamp(ts).strftime(%Y-%m-%d %H:%M:%S)标题里可能带空格、换行和 emoji 符号在 Windows 下当文件名会报错清洗函数这样写import re def clean_filename(title: str) - str: title re.sub(r[\\/:*?|\r\n], _, title) return title.strip()[:80]去重指纹不要用整个content_url因为里面的chksm、clicktime、enterid每次拿到都可能不同。正确做法是取 URL path 部分/s/后面的短 ID这个 ID 才是文章唯一标识。3.4 建表 SQL、参数化写入和去重约束为便于大作业展示统计分析单表就够CREATE TABLE IF NOT EXISTS articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, article_id TEXT UNIQUE, title TEXT, author TEXT, publish_time TEXT, content TEXT, source_url TEXT, created_at TEXT DEFAULT (datetime(now, localtime)) );article_id加UNIQUE约束写入时用INSERT OR IGNORE重复批次抓取不会产生重复行import sqlite3 def save_article(conn: sqlite3.Connection, item: dict): sql INSERT OR IGNORE INTO articles (article_id, title, author, publish_time, content, source_url) VALUES (?, ?, ?, ?, ?, ?) conn.execute(sql, ( item[article_id], item[title], item[author], item[publish_time], item[content], item[source_url], ))参数化写法防止 SQL 注入INSERT OR IGNORE让断点续跑时不用做任何条件判断数据库会自动跳过重复行。调完记得conn.commit()否则数据只在内存里。4. 频率控制、签名过期和验证码把爬虫从“能跑”调到“不挂”代码能跑通只是开始微信对mp.weixin.qq.com的访问频控每分钟几十次到上百次不等没有官方保证的阈值所以异常处理必须建立在观察响应特征的基础上。4.1 请求间隔和随机抖动参数怎么调根据经验actiongetmsg接口每批count10连续请求间隔保持 1 到 3 秒一轮抓 200 篇以内是稳妥节奏。时间间隔不要写在循环里写死配置化更好import random import time interval config[request_interval] # 单位秒建议 1.0 以上 time.sleep(interval random.uniform(0, 0.5))加一个 0 到 0.5 秒的随机抖动避免每次请求都落在整数秒边界。很多风控策略会同时统计每秒请求数和请求时间规律性规整的固定间隔反而容易被识别。4.2 响应体的状态信号与处理策略profile_ext接口返回 JSON重点看顶层字段返回码 / 字段含义处理措施ret: 0请求成功正常解析ret: -3cookie 或 token 失效停止抓取提示重新抓包ret: 200013操作频繁被风控拦截长退避等待 5 到 15 分钟general_msg_list为空请求成功但列表为空检查 offset 是否异常响应体不是 JSON返回的是验证码页或错误页停止当前策略人工介入对应代码实现data resp.json() if data.get(ret) -3: raise RuntimeError(cookie/token 已失效请重新抓包) elif data.get(ret) 200013: time.sleep(600) return None elif general_msg_list not in data: print(响应异常可能是验证码页) return None实际项目里建议把-3和200013的状态处理做成回调函数而不是散落在爬虫主逻辑里这样项目说明文档可以把异常表格直接贴出来代码也更好测。4.3 指数退避重试和失败日志记录网络层异常ConnectionError、Timeout与业务层异常要分开处理。网络层写一个带指数退避的重试包装器def request_with_retry(url, params, headers, retries3, base_delay2.0): for attempt in range(retries): try: resp requests.get(url, paramsparams, headersheaders, timeout10) return resp except (requests.ConnectionError, requests.Timeout) as e: delay base_delay * (2 ** attempt) print(f第 {attempt 1} 次请求失败: {e}, {delay:.1f}s 后重试) time.sleep(delay) return None指数退避的核心是base_delay * (2 ** attempt)第一次失败等 2 秒第二次 4 秒第三次 8 秒。超过重试次数后返回None由调用方决定跳过这批还是整个爬虫停掉不要在重试函数内部无限循环。日志至少记录三件事请求的offset、返回的ret值、重试次数排查时可以精确定位是哪一批数据出了问题。5. 三个能写进项目说明文档的收尾技巧代码功能完成后项目说明是最能拉开差距的地方。下面三个技巧既能让文档有内容也能让你的爬虫在演示时显得经过设计。5.1 把断点 offset 落盘支持增量续跑历史文章抓取不会一次跑完公众号每天更新上次抓到offset100下次从 0 开始会浪费大量请求。把 offset 记录在本地文件启动时读取抓完后写回def load_offset(pathoffset.txt): try: return int(open(path).read()) except (FileNotFoundError, ValueError): return 0 def save_offset(path, offset): with open(path, w) as f: f.write(str(offset))这个设计配合INSERT OR IGNORE的去重让增量抓取几乎是零成本的。项目说明里写一句“支持断点续跑”比贴十行重试代码更有说服力。5.2 用 config.json 隔离 cookie 和抓包参数把__biz、cookie、appmsg_token、请求间隔、User-Agent 全部放到config.json代码里只引用配置项。这样 cookie 过期时更换配置文件的几个值就能继续跑爬虫逻辑一行都不用动。配置放 JSON 还有一个好处项目说明文档可以直接放一份示例配置阅读体验比 Python 字典更直观。5.3 用随机抽样对比验证抓取完整性抓完一批文章先做完整性验证再写报告。统计articles表最新一条和最早一条的发布时间跨度手工和公众号主页对比编号是否对得上。再随机抽 5 篇文章拿content_url在浏览器里打开对比本地保存的正文和页面正文的文字长度。如果 5 篇里有 1 篇正文缺失就去检查js_content选择器是否因为微信改版失效把解析模块的选择器改成备用节点重新跑一遍。本文还有配套的精品资源点击获取
返回列表