简介:这份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 四个核心参数:图文搜索与公众号搜索的区别
第一次写这个采集器时,我习惯在浏览器里手动搜一个词,然后把地址栏的参数逐个删掉、逐个验证,最后发现真正影响结果的只有少数几个。现在固定用下面这套参数,既干净又够用。
| 参数 | 取值 | 作用 |
|---|---|---|
| type | 2 | 文章搜索,返回图文结果;type=1 是公众号搜索 |
| query | URL 编码后的关键词 | 搜索词,中文必须做百分号编码 |
| ie | utf8 | 输入编码,固定 utf8 |
| s_from | input | 来源标记,保持 input 即可 |
| page | 从 2 开始 | 翻页参数,第 1 页不传 |
type=2 返回的是文章列表,每条对应一篇文章;type=1 返回的是公众号名片,字段结构完全不同。爬文章就固定 type=2,不用犹豫。query 用 requests 的 params 传参时会自动做百分号编码,中文不需要手动处理;如果自己拼 URL,务必用 urllib.parse.quote(keyword) 包一层,否则服务端拿到的参数是乱的,返回结果直接为空。
ie=utf8 是服务端要求的前端编码参数,列表页内容本身是 UTF-8。requests 偶尔会按响应头里的旧编码去猜,导致标题乱码,所以解析前要强制 resp.encoding = "utf-8"。这是这个项目里最常见的乱码来源,先记下,后面的脚本里会看到对应写法。
2.2 列表页解析对象:标题、公众号名、摘要、相对时间都在哪个节点里
搜狗文章搜索的 DOM 结构这些年改过好几版,但大致骨架没变:结果是 ul.news-list 下的 li 节点,每个 li 承载一篇文章。解析代码要基于当前实际页面来写,如果发现选择器失效,按 4.5 节的思路先留现场再改,不要盲猜。
| 字段 | 选择器 | 说明 |
|---|---|---|
| 文章标题 | h3 a | get_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?src=11×tamp=...&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/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", "Referer": "https://weixin.sogou.com/", } session = requests.Session() session.headers.update(HEADERS) # 先访问一次搜狗微信搜索首页,让服务端把 SNUID 等基础 Cookie 种下来 # 跳过这一步直接发搜索请求,触发验证码的概率会高很多 session.get("https://weixin.sogou.com/", timeout=10) time.sleep(1)这段逻辑的核心是 requests.Session。Session 会自动持有并回传 Cookie,先 GET 首页的目的是让服务端下发基础 Cookie,模拟真实用户先打开搜索页、再输入关键词的行为。HEADERS 里的 Referer 是给后面的中转跳转用的,统一放这里省事,搜索请求本身带不带它影响不大。
参数说明:timeout=10 是连接超时,搜狗偶尔响应慢,后续请求可以放宽到 15;time.sleep(1) 是访问首页和第一次搜索之间的间隔,别小看这一秒,它决定了第一轮请求的节奏,也直接关系到你会不会在第一页就吃到验证码。
3.2 搜索、解析列表、解析跳转链接:三个函数各司其职
采集主流程拆成三个函数:search_articles 负责发请求拿 HTML,parse_list 负责从列表页提取字段,resolve_article_url 负责把搜狗中转链接解析成真实文章地址。
def search_articles(keyword, page=1): """按关键词搜索公众号文章,返回列表页 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", params=params, timeout=15) 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(strip=True) 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(strip=True) if account else "", "summary": summary.get_text(strip=True) if summary else "", "relative_time": time_el.get_text(strip=True) if time_el else "", "sogou_link": link, }) return items def resolve_article_url(sogou_link): """把搜狗中转链接解析成最终的 mp.weixin.qq.com 文章地址""" resp = session.get(sogou_link, timeout=15) # 大部分低频请求会直接 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 的参数说明:type=2 固定在函数里,keyword 交给 requests 的 params 做百分号编码,不需要手动 quote;page 从第 2 页开始传,第 1 页不传 page 是搜狗的习惯,传了 page=1 也不会报错,结果一致。
parse_list 的说明:CSS 选择器对应 2.2 小节的表格;get_text(strip=True) 会去掉标题和摘要里的首尾空白;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?src=11×tamp=...&ver=...&signature=... 链接,当天能打开,第二天再访问返回 404 或提示链接已失效。
原因:搜狗搜索页给出的跳转目标是带签名参数的临时地址,服务端会校验 timestamp,超过有效期就作废。这是搜狗故意设计的一道门槛,目的是让采集者不能只存链接、按需再抓正文。
解决:拿到解析后的文章 URL 时立刻请求正文页,把标题、正文、公众号的发布时间落库。库里只保留 article_url 字段做记录,不依赖它做长期访问。实际操作里我会在拿到正文后执行一条 UPDATE articles SET article_url = ?, fetched = 1 WHERE sogou_link = ?,让正文抓取成为解析跳转链接后的同一步,而不是隔天补抓。
4.3 翻到第 10 页就没了:单关键词结果截断与深翻页风险
现象:page=11 及以后,返回页面依然是 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_pages=5): 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 分钟。这套配置不是最快的,但它是能连续跑几周不被封的配置。采集这种事,跑得久比跑得快重要,希望帮到你。
本文还有配套的精品资源,点击获取