Python爬取搜狗微信搜索:公众号数据采集与反爬实战指南

发布时间:2026/10/27 18:23:06
Python爬取搜狗微信搜索:公众号数据采集与反爬实战指南

简介:面向微信公众号数据采集入门者与爬虫开发者,这套资源围绕搜狗微信接口实现公众号基础信息与历史文章抓取,覆盖接口调用、参数处理、返回解析与异常规避等关键环节,可帮助读者快速搭建一个轻量级采集原型。包内共三个文件,包含两个以Python编写的脚本与一份Markdown说明文档:脚本分别承担公众号信息查询与文章列表抓取功能,文档对运行环境、请求头设置、翻页逻辑及常见问题做了简要说明;整体压缩包仅2KB,结构精简,适合直接阅读和二次修改。目前已有1719人学习下载。借助这套脚本,读者能直观理解搜狗微信搜索的数据交互方式,掌握从链接构造到结果抽取的完整思路,并据此扩展自定义字段、增量抓取或数据清洗等实用功能,减少从零摸索的时间,适合作为爬虫入门练习或内部工具的基础版本。

1. 从搜狗微信搜索说起:接口能拿到什么,值得怎么用

搜狗微信搜索(weixin.sogou.com)可能是目前唯一一个不需要你有一堆公众号后台权限,就能“按公众号名称搜到账号信息,按关键词搜到文章列表”的公开入口。很多做公众号数据分析、竞品监控、内容归档的人,都会在这一层去抓公开数据:公众号的微信号、认证主体、最近文章,以及每篇文章的标题、链接、发布时间、阅读数(部分可见)。它不是官方开放平台,没有稳定的授权流程,但好在它不需要开发者资质,也没有调用次数配额——只要你的请求行为足够克制,就能稳定拿数据。

这篇文章面向两类人:一类是做内容运营,想定期把同行公众号的新文章抓下来做选题分析;另一类是搞知识库沉淀,想把关注列表里的公众号文章自动存到本地。我会按“请求流程 → 信息抓取 → 文章正文提取 → 避坑 → 进阶”的顺序,把搜狗微信接口这条链路完整拆开。这里不依赖任何第三方封装库,直接用 requests 就能跑通,代码里的参数和坑,都是能照抄的。

2. 搜狗微信的搜索机制:不是正式接口,但要按接口的规矩来

2.1 搜狗微信搜索的页面结构与请求流程

搜狗微信的搜索入口分为两类:按公众号搜索和按文章搜索。公众号搜索的 URL 通常是这种格式:

https://weixin.sogou.com/weixin?type=1&query=关键词&ie=utf8&s_from=input&_sug_=n&_sug_type_=

type=1 表示搜公众号,type=2 表示搜文章。query 是关键词,其他的一堆下划线参数是搜狗自己生成的客户端标识,少传两个通常不影响结果,但传了能降低被拦的概率。

整个请求流程是:先用一个普通浏览器 User-Agent 打开这个地址,搜狗会返回一个 HTML 页面,里面嵌着搜索结果列表,每个结果包含公众号名称、微信号、认证信息、最近更新的一篇文章标题和时间。你需要的“公众号信息”,其实就藏在这份 HTML 里,不需要再调其他二次接口。

比较麻烦的是,搜狗在搜索结果页做了两件事:一是给返回的 HTML 里部分字段加了加密和转义,二是对高频请求会返回一个验证码页面。这两个坑后面第 5 章会详细讲,这里先记住一件事:搜狗的这个“接口”,表面是网页,本质上是一个动态参数较少、但反爬检查较多的 HTTP 接口。所以你在写代码时,要按接口的思路去处理:控制请求头、控制 Cookie、控制频率。

2.2 为什么搜狗会成为事实上的公众号数据入口

微信官方对公众号信息的获取限制得很严。公众号后台可以导出 CSV,但只能拿到自己号的数据;开放平台接口需要应用审核,而且要用户授权。也就是说,如果你没有号主的配合,想去查任意一个公众号的认证主体、简介、最近文章,正规开放接口是拿不到的。

搜狗微信搜索做了个折中:它跟微信有内容合作,有权抓取公众号的公开信息,并在自己的搜索页面展示。对普通从业者来说,这就是一个不对个人开放、但对爬虫“半开放”的数据入口。你不需要登录搜狗账号,也不需要接任何 SDK,只要会模拟浏览器行为,就能拿到数据。这也是为什么市面上大量公众号数据工具都基于搜狗微信搜索来做,因为它是门槛最低、不需要企业资质的一条路。

但要注意,搜狗毕竟是搜索公司,不是专门给爬虫提供 API 的。它做了很多“墙体”来防爬:比如验证码、Cookie 校验、请求频率限制、结果页链接动态加密。这些都会在后续代码里一并处理。

2.3 黑匣子一样的 Cookie 与 SNUID 机制

第一次请求搜狗微信时,服务器会下发两个关键 Cookie:SNUID 和 SUID。这两个值会在后续请求中作为身份标识。SNUID 尤其重要,它是由访问时间、IP 和随机数生成的,搜狗会用它来判断你是不是同一个客户端在连续请求。

我一般会建议这样处理:第一次请求不管返回什么,先把 HTTP 响应里的 Set-Cookie 全部存下来,之后每次都带上。注意别自己伪造 SNUID,因为它跟 IP 有关联,你伪造一个别人的,容易被判定为异常。用 requests.Session 会自动管理这些,但有一个细节要手动配置:会话中要固定 User-Agent 和 Accept-Language,不然搜狗会认为你不是同一个浏览器在访问。

如果你第 1 次请求就遇到了验证码,先别急着调代码,把浏览器手动打开一次 weixin.sogou.com,完成一次搜索,然后再把浏览器里的 Cookie 复制出来,放到自己代码的 headers 里。这是最快且最稳的过法,比硬调参数靠谱得多。

3. 用 requests 获取公众号信息列表:最小可跑通的代码

3.1 最基本的搜索请求:带上必要参数和 UA

下面这段代码是获取公众号信息最核心的请求。它不处理翻页,只负责拿到第一页搜索结果:

import requests from urllib.parse import quote 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-Language": "zh-CN,zh;q=0.9", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Referer": "https://weixin.sogou.com/", } session = requests.Session() session.headers.update(headers) keyword = "科技美学" url = "https://weixin.sogou.com/weixin?type=1&query={}&ie=utf8".format(quote(keyword)) resp = session.get(url, timeout=10) print(resp.status_code) print(resp.text[:500])

这段代码的逻辑很直接:构建一个带 UA 和 Referer 的请求头,用 Session 保持 Cookie,然后请求 type=1 的公众号搜索页。这里 quote(keyword) 是为了把中文关键词做 URL 编码,不然搜狗直接返回 400。打印出来的 resp.text 如果包含“搜狗安全验证”,说明被验证码卡住了;如果包含公众号名称,就说明正常返回了结果页。

参数说明:type=1 是公众号搜索,type=2 是文章搜索,这个参数不能错。ie=utf8 是搜狗要求固定带的,不传也能返回结果,但传了更稳。timeout 建议 10 秒以上,搜狗响应有时候会慢,5 秒容易误报超时。Referer 第一次请求可以先不带,但遇到部分异常响应时补上会有帮助。

3.2 解析 HTML 里的公众号信息字段

搜狗回复的 HTML 结构比较固定,每个公众号搜索结果都在 class 为 wx-box 的 div 里。下面是提取关键字段的代码:

from bs4 import BeautifulSoup import re def parse_weixin_account_list(html): soup = BeautifulSoup(html, "html.parser") results = [] for item in soup.select(".wx-box"): # 公众号名称 name_tag = item.select_one(".wx-name") name = name_tag.get_text(strip=True) if name_tag else "" # 微信号 account_tag = item.select_one(".info span:last-child") account = account_tag.get_text(strip=True) if account_tag else "" # 清理掉 "微信号:" 前缀 account = re.sub(r"^微信号:", "", account) # 认证主体 / 简介 desc_tag = item.select_one(".txt-info") desc = desc_tag.get_text(" ", strip=True) if desc_tag else "" # 最近文章标题 article_tag = item.select_one(".s-p a") article_url = article_tag.get("href") if article_tag else "" article_title = article_tag.get_text(strip=True) if article_tag else "" results.append({ "name": name, "account": account, "desc": desc, "latest_article_title": article_title, "latest_article_url": article_url, }) return results # 使用 data = parse_weixin_account_list(resp.text) for d in data: print(d["name"], d["account"], d["desc"][:50])

解析逻辑分四步:先定位所有 wx-box 节点,再分别提取名称、微信号、简介和最近文章。这里有两个易踩的坑:一是微信号在 html 里可能被拆成了多个 span,要用 “:last-child” 拿到完整内容;二是搜狗返回的 html 中部分字段可能有实体转义,比如把 & 写成 &,BeautifulSoup 会自动反转,但如果你用正则去匹配,就要注意这些转义。

参数说明:select(".wx-box") 用的是 CSS 选择器,速度比逐层 find 快。.info span:last-child 表示取 info 类节点下的最后一个 span,通常这一项就是微信号。这里没有把头像和二维码链接加进来,如果需要头像,可以额外取 .wx-avatar img 的 src 属性,但那是一个相对路径,需要拼接上 https://weixin.sogou.com 前缀。

3.3 翻页与参数边界

搜狗公众号搜索结果默认只显示前 10 页,每页最多 10 条。翻页参数是 page,URL 里加 &page=2 就是第二页。翻页逻辑就是把上面的 URL 循环 10 次,每次改 page 值。但这里有个现实边界:公众号搜索翻到第 3 页之后,很容易触发验证码。这不是代码问题,是搜狗的反爬策略,它在用户角度也是这么限制的。

所以我一般不建议把公众号搜索翻页太深。如果目标是把某个行业所有公众号都抓下来,更稳的方式是用行业关键词做多词搜索,每个词只抓前 2 页,最后做去重合并。因为不同关键词会召回同一批公众号,合并后覆盖率不一定比深翻第 8 页低,但触发验证码的概率会小很多。这是个很实用的策略,属于用搜索词的数量换取翻页深度。

翻页请求改成这样:

for page in range(1, 3): # 只翻前两页 page_url = url + "&page={}".format(page) resp = session.get(page_url, timeout=10) time.sleep(3) # 每页之间至少间隔 3 秒 if "搜狗安全验证" in resp.text: print("第 {} 页触发验证码,停止翻页".format(page)) break data = parse_weixin_account_list(resp.text)

这段代码加了 sleep(3) 来控制频率,同时用验证码关键词做一个提前中断。如果你用这段代码跑,发现前两页都能正常返回,那说明当前的 IP 和 Cookie 状态是健康的;如果第一页就触发验证码,优先去换浏览器 Cookie,而不是加大 sleep 时间。

4. 从公众号到文章列表:抓取文章信息和正文的关键步骤

4.1 搜狗文章搜索与公众号文章页的两种路由

搜狗微信里找公众号文章有两条路:一条是通过公众号搜索结果里的“最近文章”链接,直接进到文章页;另一条是通过 type=2 的文章搜索,按关键词搜文章,再从文章链接回到公众号。两条路的最终目标都是 article 详情页,但文章搜索返回的是一个中间跳转页,链接形如:

https://weixin.sogou.com/link?url=xxxxxx

这个 link 地址点击后会经过一次 302 跳转,最终落到 mp.weixin.qq.com 的正文页。你直接用 requests 去 get 这个 link 地址,会把跳转后的最终 URL 拿到,一般用 allow_redirects=True 就能自动跟过去。

公众号搜索结果里的“最近文章”,同样也是一个 link 跳转地址。所以后续代码里你只需要写一个函数:接收任意 link 链接,返回最终的文章页 HTML 和最终 URL。这样可以统一处理两种来源。

4.2 提取文章列表:参数与返回结构

搜狗文章搜索的典型 URL 是:

https://weixin.sogou.com/weixin?type=2&query=人工智能&ie=utf8

返回的每一篇文章都在 li 标签里,结构里有标题、公众号名称、发布时间、摘要。提取代码:

def parse_article_list(html): soup = BeautifulSoup(html, "html.parser") articles = [] for li in soup.select("ul.news-list > li"): title_tag = li.select_one("h3 a") if not title_tag: continue title = title_tag.get_text(strip=True) link = title_tag.get("href") account_tag = li.select_one(".account") account_name = account_tag.get_text(strip=True) if account_tag else "" time_tag = li.select_one(".s2") publish_time = time_tag.get_text(strip=True) if time_tag else "" # 摘要通常在 .txt-info 里,但有时会被截断 summary_tag = li.select_one(".txt-info") summary = summary_tag.get_text(" ", strip=True) if summary_tag else "" articles.append({ "title": title, "link": link, "account": account_name, "publish_time": publish_time, "summary": summary, }) return articles

这里的重点在于:文章标题在 h3 里的 a 标签,公众号名在 .account 类里,发布时间在 .s2 类里。搜狗对发布时间有三种格式:xx分钟前、xx小时前、具体日期。如果你要做时间排序,需要把这些格式统一转换,只保留“昨天”“前天”这种会丢掉准确日期的字段,所以我建议直接在摘要里找日期,或者在点进文章正文之后再取发布时间。

参数说明:链接 link 不是完整 URL,而是 /link?url= 开头的相对路径。要拼上 https://weixin.sogou.com 前缀再访问。另外 .s2 字段查出来可能包含“2017年03月20日”这类格式,要把“年”“月”去掉后转成标准时间,才能进数据库。

4.3 文章正文提取:用正文页的元信息而非 pyquery 硬啃

搜狗跳转后的文章正文页是 mp.weixin.qq.com 的页面。这个页面里,正文内容在 id 为 js_content 的 div 里,标题在 h1 或 meta 里,作者在 id 为 js_name 的 span 里。提取代码:

def parse_article_content(final_resp): soup = BeautifulSoup(final_resp.text, "html.parser") # 标题先从 og:title 拿,拿不到再用 h1 title_meta = soup.find("meta", attrs={"property": "og:title"}) title = title_meta.get("content", "").strip() if title_meta else "" if not title: h1 = soup.select_one("h1") title = h1.get_text(strip=True) if h1 else "" # 公众号名 account_meta = soup.find("meta", attrs={"property": "og:article:author"}) account = account_meta.get("content", "").strip() if account_meta else "" # 正文:js_content div content_div = soup.select_one("#js_content") content_text = content_div.get_text("\n", strip=True) if content_div else "" return { "title": title, "account": account, "content_text": content_text, "final_url": final_resp.url, }

正文提取看起来简单,但有两个常见翻车点:第一,部分公众号开启了“图文素材保护”,正文会被拆进多个 div.wx-temp 或做了懒加载,直接取 js_content 可能只有开头一段;第二,正文里的图片在 HTML 里是>import sqlite3 from datetime import datetime conn = sqlite3.connect("weixin_articles.db") conn.execute("""CREATE TABLE IF NOT EXISTS articles ( title TEXT PRIMARY KEY, account TEXT, publish_time TEXT, final_url TEXT, content TEXT, created_at TEXT )""") def save_article(article): cur = conn.execute("SELECT 1 FROM articles WHERE title=?", (article["title"],)) if cur.fetchone(): return False # 已存在,跳过 conn.execute( "INSERT INTO articles (title, account, publish_time, final_url, content, created_at) VALUES (?,?,?,?,?,?)", (article["title"], article["account"], article["publish_time"], article["final_url"], article["content_text"], datetime.now().isoformat()) ) conn.commit() return True

这里用 title 做主键去重。虽然理论上不同文章可能重名,但这样最简单,后面如果发现误删,可以再把 (account, publish_time) 也纳入判断。存储正文用 TEXT 字段就够了,不需要上 PostgreSQL,单机爬 1 万篇文章也就几百 MB。

6.2 把文章转成知识库格式:从 HTML 到 Markdown

正文存纯文本之后,搜索是能搜了,但对知识库来说,没有标题层级和图片标记,阅读体验会差很多。我通常会加一步:把提取出来的 js_content HTML 直接转成 Markdown。这个转换可以用 html2text 库,也可以自己用 BeautifulSoup 按标签转。核心是把 p 转成空行分隔,把 h2/h3 转成 ## / ###,把 img 转成。

def html_to_markdown(content_html): soup = BeautifulSoup(content_html, "html.parser") lines = [] for block in soup.find_all(["p", "h2", "h3", "img", "section"]): if block.name == "h2": lines.append("\n## " + block.get_text(strip=True)) elif block.name == "h3": lines.append("\n### " + block.get_text(strip=True)) elif block.name == "img": src = block.get("data-src") or block.get("src", "") alt = block.get("alt", "") lines.append(f"![{alt}]({src})") else: text = block.get_text(" ", strip=True) if text: lines.append("\n" + text) return "\n".join(lines)

这里的要点有两个:第一,所有 img 标签的 src 属性值优先取 style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />