
简介这份Python爬虫资源依托搜狗搜索引擎定向抓取微信公众号文章面向需要批量采集公众号历史内容、搭建自有文章库的开发者与数据分析人员也适合爬虫入门与中级学习者作为练手项目适用于个人公众号监控、内容聚合分析等场景。资源支持按公众号ID和关键字两种方式抓取能提取文章标题、正文、发布时间及公众号名称并以正确编码格式存储至数据库同时实现新增数据自动增量入库避免重复抓取与数据冗余。关键字模式下还按时间排序、支持翻页爬取完整覆盖常见采集流程。压缩包共15个文件以7个Python脚本、7个pyc编译文件为主另含Scrapy配置文件py为源码便于查看逻辑pyc可用于直接运行cfg定义爬虫工程配置整体仅19KB轻量易读。已有878人学习下载使用前需留意搜狗反爬策略与页面结构变化。1. 通过搜狗搜索引擎爬取微信公众号文章先把这个入口值不值得做说清楚公众号文章没有开放搜索接口站外搜索引擎也收录不全想按关键词爬取微信公众号文章搜狗搜索引擎是目前最值得认真研究的公开入口既能搜图文也能按公众号名称搜号。对做内容监控、竞品分析、舆情采集的开发者来说通过搜狗搜索引擎爬取微信公众号文章这套流程一旦跑通等于拿到一个不需要申请任何官方权限的数据来源。它适合会用 Python、想长期拿公众号文章数据但不想碰官方接口权限的人。但这入口不是白给的反爬验证码、链接时效、翻页上限三座山拦在中间。下文从接口拆解讲到最小脚本和落库最后用一整章写踩坑记录。2. 拆解 weixin.sogou.com 的搜索接口URL 参数、列表结构与跳转链接搜狗微信搜索是独立域名 weixin.sogou.com不走普通网页搜索。爬虫请求统一打到 /weixin 路径通过 GET 参数拼搜索条件。写代码之前先把参数和页面结构看清楚后面调参时才知道自己在改什么而不是对着一个黑盒瞎试。2.1 type、query、ie、page 四个核心参数图文搜索与公众号搜索的区别第一次写这个采集器时我习惯在浏览器里手动搜一个词然后把地址栏的参数逐个删掉、逐个验证最后发现真正影响结果的只有少数几个。现在固定用下面这套参数既干净又够用。参数取值作用type2文章搜索返回图文结果type1 是公众号搜索queryURL 编码后的关键词搜索词中文必须做百分号编码ieutf8输入编码固定 utf8s_frominput来源标记保持 input 即可page从 2 开始翻页参数第 1 页不传type2 返回的是文章列表每条对应一篇文章type1 返回的是公众号名片字段结构完全不同。爬文章就固定 type2不用犹豫。query 用 requests 的 params 传参时会自动做百分号编码中文不需要手动处理如果自己拼 URL务必用 urllib.parse.quote(keyword) 包一层否则服务端拿到的参数是乱的返回结果直接为空。ieutf8 是服务端要求的前端编码参数列表页内容本身是 UTF-8。requests 偶尔会按响应头里的旧编码去猜导致标题乱码所以解析前要强制 resp.encoding utf-8。这是这个项目里最常见的乱码来源先记下后面的脚本里会看到对应写法。2.2 列表页解析对象标题、公众号名、摘要、相对时间都在哪个节点里搜狗文章搜索的 DOM 结构这些年改过好几版但大致骨架没变结果是 ul.news-list 下的 li 节点每个 li 承载一篇文章。解析代码要基于当前实际页面来写如果发现选择器失效按 4.5 节的思路先留现场再改不要盲猜。字段选择器说明文章标题h3 aget_text 取标题href 是搜狗中转链接公众号名称.account认证号名称旁带图标取文本时 strip 即可摘要.txt-info正文前 80 字左右的截断发布时间span.s2列表页只给3天前昨天这类相对时间解析时每个字段都要做空值兜底li 节点里偶尔会夹带广告位或结构不完整的异常节点写 if not h3: continue 跳过不要假设每个 li 都长一个样。另外列表页底部的相关搜索词块也是重要资源它是搜狗根据当前关键词聚合出来的关联词可以直接收进关键词表做下一轮扩展比空想新词高效得多。2.3 跳转链接的真相搜狗 /link 中转与 mp.weixin.qq.com 临时 URL列表页里标题的 href 不是文章真实地址而是形如 /link?url... 的搜狗中转链接。直接把这个地址存库没有意义必须再请求一次让它跳到最终的 mp.weixin.qq.com/s 文章页。这里的行为并不完全一致我实际观察到的有两种。正常低频请求时服务端直接 302 跳转requests 默认开启 allow_redirects跟随跳转就能拿到最终 URL请求一频繁服务端会返回一个 JS 延时跳转页需要从 HTML 里把目标地址抠出来。还有一部分跳转要求 Referer 必须是搜狗搜索页否则返回异常所以请求头里的 Referer 要固定在 https://weixin.sogou.com/。最终拿到的文章 URL 形如 mp.weixin.qq.com/s?src11timestamp...ver...signature...这是带时效签名的临时链接几小时到一天内就会失效。理清这一点很重要第四章专门讲链接过期怎么处理。搜狗这套行为没有任何公开文档全是黑匣子式试出来的所以把先种 Cookie、带对 Referer、拿到链接立刻抓正文这三件事养成习惯能少踩很多坑。3. 用 requests BeautifulSoup 跑通最小采集脚本从搜索词到 SQLite 落库这一章给一套可以直接复制改用的最小脚本。选 requests BeautifulSoup 是因为依赖少、出错好排查Scrapy 那种框架在这个场景偏重等真的需要分布式调度和中间件时再迁移不迟。脚本分三块请求预处理、搜索与解析、落库判重。3.1 请求头与 Cookie 预处理先访问一次首页把基础 Cookie 拿全搜狗的反爬判断很大程度依赖 Cookie 链所以第一步不是发搜索请求而是先让 Session 拿到服务端下发的基础 Cookie。import requests import re import time import sqlite3 from urllib.parse import urljoin from bs4 import BeautifulSoup HEADERS { User-Agent: (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36), Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, Referer: https://weixin.sogou.com/, } session requests.Session() session.headers.update(HEADERS) # 先访问一次搜狗微信搜索首页让服务端把 SNUID 等基础 Cookie 种下来 # 跳过这一步直接发搜索请求触发验证码的概率会高很多 session.get(https://weixin.sogou.com/, timeout10) time.sleep(1)这段逻辑的核心是 requests.Session。Session 会自动持有并回传 Cookie先 GET 首页的目的是让服务端下发基础 Cookie模拟真实用户先打开搜索页、再输入关键词的行为。HEADERS 里的 Referer 是给后面的中转跳转用的统一放这里省事搜索请求本身带不带它影响不大。参数说明timeout10 是连接超时搜狗偶尔响应慢后续请求可以放宽到 15time.sleep(1) 是访问首页和第一次搜索之间的间隔别小看这一秒它决定了第一轮请求的节奏也直接关系到你会不会在第一页就吃到验证码。3.2 搜索、解析列表、解析跳转链接三个函数各司其职采集主流程拆成三个函数search_articles 负责发请求拿 HTMLparse_list 负责从列表页提取字段resolve_article_url 负责把搜狗中转链接解析成真实文章地址。def search_articles(keyword, page1): 按关键词搜索公众号文章返回列表页 HTML params { type: 2, # 2 表示文章搜索1 是公众号搜索 query: keyword, ie: utf8, s_from: input, _sug_: n, _sug_type_: , page: page, # 第 2 页起才需要这个参数 } resp session.get(https://weixin.sogou.com/weixin, paramsparams, timeout15) resp.raise_for_status() resp.encoding utf-8 # 列表页是 UTF-8避免 requests 猜错编码 return resp.text def parse_list(html): 解析列表页提取标题、公众号名、摘要、相对时间、中转链接 soup BeautifulSoup(html, html.parser) items [] for li in soup.select(ul.news-list li): h3 li.select_one(h3 a) if not h3: # 广告位或异常节点没有标题直接跳过 continue title h3.get_text(stripTrue) link urljoin(https://weixin.sogou.com/, h3.get(href, )) account li.select_one(.account) summary li.select_one(.txt-info) time_el li.select_one(span.s2) items.append({ title: title, account: account.get_text(stripTrue) if account else , summary: summary.get_text(stripTrue) if summary else , relative_time: time_el.get_text(stripTrue) if time_el else , sogou_link: link, }) return items def resolve_article_url(sogou_link): 把搜狗中转链接解析成最终的 mp.weixin.qq.com 文章地址 resp session.get(sogou_link, timeout15) # 大部分低频请求会直接 302 到文章页拿 resp.url 即可 if resp.url.startswith(https://mp.weixin.qq.com/): return resp.url # 高频请求会收到 JS 延时跳转页需要从 HTML 里抠出目标地址 m re.search( r(?:URL|location\.href|window\.location\.replace)\s*[:]\s*[\]([^\]), resp.text ) if m: target m.group(1) return target if target.startswith(http) else urljoin(resp.url, target) return search_articles 的参数说明type2 固定在函数里keyword 交给 requests 的 params 做百分号编码不需要手动 quotepage 从第 2 页开始传第 1 页不传 page 是搜狗的习惯传了 page1 也不会报错结果一致。parse_list 的说明CSS 选择器对应 2.2 小节的表格get_text(stripTrue) 会去掉标题和摘要里的首尾空白urljoin 把 /link?url... 补成完整地址避免后面请求时拼错域名。resolve_article_url 的说明先判断最终 URL 是不是 mp.weixin.qq.com是就直接返回如果收到的是跳转页用正则匹配三种常见的跳转写法分别是 meta refresh 的 URL、location.href、window.location.replace。这里超时不要设太短跳转链路里搜狗和目标服务器各响应一次15 秒比较合适。3.3 SQLite 落库与判重文章表的最小字段设计公众号文章量级对个人采集器来说SQLite 完全够用还省掉部署数据库服务的麻烦。表结构按先收录、后抓正文的节奏设计。def save_items(items, keyword): conn sqlite3.connect(wechat_articles.db) conn.execute( CREATE TABLE IF NOT EXISTS articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, keyword TEXT, title TEXT, account TEXT, summary TEXT, article_url TEXT, sogou_link TEXT UNIQUE, relative_time TEXT, fetched INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime(now, localtime)) ) ) conn.executemany( INSERT OR IGNORE INTO articles (keyword, title, account, summary, sogou_link, relative_time) VALUES (?, ?, ?, ?, ?, ?) , [ (keyword, item[title], item[account], item[summary], item[sogou_link], item[relative_time]) for item in items ]) conn.commit() conn.close() if __name__ __main__: keywords [数据采集, 爬虫工程, 信息抽取] for kw in keywords: for page in range(1, 6): # 单关键词最多翻 5 页见 4.3 html search_articles(kw, page) items parse_list(html) save_items(items, kw) print(f{kw} 第 {page} 页共 {len(items)} 条) time.sleep(10 page * 2) # 页数越深间隔拉得越长落库的关键是 sogou_link 上的 UNIQUE 约束配 INSERT OR IGNORE。同一关键词多轮采集、不同关键词之间重复收录都会因为中转链接相同而被自动跳过这是最简单有效的判重手段。article_url 字段先留空等第四章讲了链接时效问题后你会明白为什么不能把真实文章地址当成唯一键。主循环里两个参数值得说。page 停在 5是因为搜狗对深翻页的容忍度很低硬翻到第 10 页的收益和风险不成正比time.sleep(10 page * 2) 让第 1 页间隔 12 秒、第 5 页间隔 20 秒整体节奏比固定 sleep 更接近真人浏览触发验证码的概率会明显下降。4. 搜狗搜索反爬避坑指南验证码、链接过期、翻页上限与相对时间搜狗的反爬不算最强但足够把新手放倒。下面把最常踩的五个坑按现象、原因、解决三步写清楚排查时直接对号入座。4.1 返回 200 却是验证码页请求频率过高和 Cookie 缺失现象search_articles 返回的 HTML 里没有 ul.news-list页面标题变成请输入验证码有时候是一个 iframe 嵌入的滑动验证组件。原因两个因素叠加。一是 Session 里没有拿到搜狗下发的有效基础 Cookie二是同一出口 IP 的请求频率超过了搜狗的心理阈值。搜狗不会直接拒绝请求而是用一个 200 状态的验证码页替换正常结果让程序以为自己成功了其实什么都没拿到。解决第一先按 3.1 节访问首页种 Cookie第二把单页间隔从秒级拉到 10 秒以上第三在代码里显式检测验证码页。判断逻辑很简单def is_captcha_page(html): 验证码页的特征没有结果列表出现验证码关键词 return news-list not in html and (验证码 in html or antispider in html)脚本每次拿到 HTML 先跑这个函数是验证码页就立刻停止当前关键词的翻页进入冷却。冷却多久我一般直接停 30 分钟宁可少跑一轮不要赌搜狗的封禁阈值。4.2 文章链接第二天打不开搜狗临时 URL 有时效现象数据库里存了一堆 mp.weixin.qq.com/s?src11timestamp...ver...signature... 链接当天能打开第二天再访问返回 404 或提示链接已失效。原因搜狗搜索页给出的跳转目标是带签名参数的临时地址服务端会校验 timestamp超过有效期就作废。这是搜狗故意设计的一道门槛目的是让采集者不能只存链接、按需再抓正文。解决拿到解析后的文章 URL 时立刻请求正文页把标题、正文、公众号的发布时间落库。库里只保留 article_url 字段做记录不依赖它做长期访问。实际操作里我会在拿到正文后执行一条 UPDATE articles SET article_url ?, fetched 1 WHERE sogou_link ?让正文抓取成为解析跳转链接后的同一步而不是隔天补抓。4.3 翻到第 10 页就没了单关键词结果截断与深翻页风险现象page11 及以后返回页面依然是 200但解析出的 items 始终为空偶尔还会直接触发验证码。原因搜狗对公众号文章搜索做了结果截断一个关键词能翻到的有效页数就在 10 页左右同时深翻页本身就是反爬的高危动作翻得越深验证码出现得越快。解决不硬翻页。我一般单关键词最多翻 5 页拿 50 条左右结果需要更多数据时用关键词拆分比如把爬虫拆成爬虫 工程爬虫 反爬爬虫 采集三个词分别搜再去掉重复。列表页底部的相关搜索词块是现成的扩展词源定期收下来扩充关键词表比单纯翻页划算得多。4.4 3天前不是时间字段相对时间要换算现象列表页 span.s2 里的文本是3天前昨天刚刚直接入库后没法按日期筛选排序也乱了。原因搜狗刻意在列表页不展示绝对时间只给相对时间增加采集方的数据处理成本。解决解析时做一次粗换算把刚刚当当天、昨天减 1 天、x天前减 x 天换算结果存 relative_time 字段。def normalize_relative_time(text): 把搜狗列表页的相对时间换算成日期字符串 if not text: return if 刚刚 in text: return time.strftime(%Y-%m-%d) if 昨天 in text: return time.strftime(%Y-%m-%d, time.localtime(time.time() - 86400)) m re.search(r(\d)天前, text) if m: days int(m.group(1)) return time.strftime(%Y-%m-%d, time.localtime(time.time() - days * 86400)) return 需要精确到分钟的绝对时间只能进正文页公众号正文里的发布时间通常在标题下方才是完整日期。列表页的相对时间适合做粗粒度过滤正文页的绝对时间适合做精确归档两者各司其职。4.5 解析结果突然全空DOM 改版和请求头缺失现象昨天还跑得好好的脚本今天 items 全是空列表页面 HTML 里也找不到 ul.news-list。原因两种可能。一是搜狗调整了列表页的 DOM 结构二是中转跳转时 Referer 没带过去被服务端判定为异常请求。个人经验里 DOM 改版更常见搜狗每隔一段时间就会微调 class 命名。解决先把返回的 HTML 存成文件用浏览器打开确认是正常列表还是验证码页。是列表页但选择器失效就重新查看 li 节点的 class 和层级更新 parse_list 里的选择器是跳转问题核对 HEADERS 里的 Referer 是否为 https://weixin.sogou.com/。改版这种事没有后悔药只能靠报错时第一时间留现场别急着改代码先存 HTML 再分析。5. 进阶技巧给采集器加验证码自愈逻辑让它能长期无人值守入门版脚本能跑通但离能长期跑还差一个关键能力遇到验证码会自己停下来冷却后自动恢复。具体做法是把主循环改造成带冷却状态的自愈循环。def run_with_cooldown(keywords, max_pages5): cooldown 0 # 冷却剩余秒数0 表示正常 for kw in keywords: for page in range(1, max_pages 1): if cooldown 0: print(f触发反爬冷却 {cooldown // 60} 分钟后继续) time.sleep(cooldown) cooldown 0 try: html search_articles(kw, page) except requests.RequestException: time.sleep(60) # 网络异常等一分钟重试 continue if is_captcha_page(html): cooldown 30 * 60 # 验证码出现停 30 分钟 break items parse_list(html) save_items(items, kw) print(f{kw} 第 {page} 页入库 {len(items)} 条) time.sleep(12)自愈循环的核心是 cooldown 变量遇到验证码不报错退出而是把冷却时间拉满 30 分钟放弃当前关键词剩余页数换下一个关键词重新开始。配合 4.1 的 is_captcha_page 检测采集器就能在无人值守的情况下自己调节节奏而不是半夜跑挂。除了自愈还有一个值得养成的习惯正文去重。公众号领域转载率极高同一个内容换几个标题反复出现如果只按链接去重会存进大量重复正文。建议抓正文时对正文文本做一次 SHA-1 哈希单独存一个 content_hash 唯一字段转载文直接跳过。给自己留一张小的正文去重表比事后再清洗省事得多。我自己的运行习惯是单关键词只取前 5 页单页间隔不低于 12 秒每个关键词每天只跑一轮验证码一出现立刻停 30 分钟。这套配置不是最快的但它是能连续跑几周不被封的配置。采集这种事跑得久比跑得快重要希望帮到你。本文还有配套的精品资源点击获取