news 2026/10/29 17:46:17

搜狗微信搜索爬虫实战:从列表页到正文的完整采集方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
搜狗微信搜索爬虫实战:从列表页到正文的完整采集方案

简介:面向需要批量采集微信公众号文章的研究者与Python爬虫开发者,这份资源以搜狗搜索引擎为入口,提供两条爬取路径:按公众号ID定向抓取,以及按关键字检索并按时间排序翻页采集。脚本可获取标题、正文、发布时间和公众号名称,并以正确编码格式写入数据库,同时支持新增数据的增量入库,方便持续维护本地文章库。

包体共15个文件,以7个Python脚本为主,附带7个编译生成的pyc文件及1个Scrapy配置文件,整体仅19KB。py文件便于阅读修改,pyc可用于直接调试,cfg则展示了Scrapy项目的运行参数。虽然体量极小,但覆盖了从请求调度、解析入库到翻页去重的完整思路。

目前已有878人学习/下载。对于想快速上手微信公众平台数据采集的读者,这套源码直接提供了可运行的采集框架和存储方案,稍作改动即可接入自己的数据库字段或扩展搜索条件,尤其适合应急抓取、内容监测等轻量级场景。

1. 搜狗微信搜索这个入口:为什么还有人专门写爬虫去抓公众号文章

微信公众号的文章一直有个特点:在微信里不好搜,在百度上也基本抓不到完整的正文。公众号内容不像网页有公开入口,很多号主发完就沉底了,想找一篇旧文只能靠微信自带的“搜一搜”,但那个结果排序和覆盖往往不理想。搜狗微信搜索是目前唯一还在公开提供公众号文章索引的站内入口,通过它至少能按关键词、按公众号把文章列表找出来。这篇笔记就是围绕这个入口写的一套爬取方案,从分析页面、构造请求、解析列表到落地保存,连验证码和封 IP 的坑一起说清楚。适合刚把 Python 爬虫跑通、想拿真实场景练手的人,也适合需要批量整理公众号资料做知识库的运维和运营同学。

2. 先摸清搜狗微信的页面结构:反爬策略与请求参数拆解

2.1 搜狗微信搜索到底暴露了什么

打开搜狗微信搜索的网址,注意它的路径和普通搜索不同,别用抓百度那套逻辑直接套。页面核心是一个文章列表,每条结果包含标题、公众号名、摘要、发布时间和一个跳转链接。这个跳转链接不是文章的最终 URL,而是搜狗做的中转地址,形如https://weixin.sogou.com/link?url=...,点进去会执行一次 JS 跳转,最后落到mp.weixin.qq.com的原文页。

我一般把整个抓取链路分成三个部分:列表页请求、中转链接解析、原文页正文提取。三个部分的难点不一样,列表页要处理搜索参数和 Cookie,中转链接要解析 JS 跳转,原文页要面对微信的域名切换和正文结构变化。很多教程只写列表页解析,结果下载下来一堆中转链接,实际没法用。

先看列表页的请求参数。搜索关键词通过query传给服务器,类型通过type区分,type=1是按文章搜,type=2是按公众号搜。页面默认每页 10 条,翻页通过page参数控制,但直接改变page数值往往拿不到数据,因为还有一个关键参数sn在里面起作用。

2.2 Cookie 和参数的组合关系

搜狗对未登录用户是允许搜索的,但会在几次请求后弹验证码。实际抓取时,我建议先用浏览器手动访问一次搜索页,把 Cookie 里的SUV、SNUID、IPLOC这几个字段复制出来,带在请求头里。其中SNUID是搜狗用于用户标识的字段,没有它很容易被识别成异常流量。

请求头里还有个容易被忽略的点:Referer必须带上,值写成https://weixin.sogou.com/。另外User-Agent最好用最新的 Chrome 版本,不要用 Python 默认的。我踩过直接用 requests 默认 UA 的坑,连续请求三次就被拦了,换成完整 UA 后稳定很多。

参数sn是捣乱的主要来源。这个参数在不同页面上值不同,而且随时间变化。网上有说法认为它是某种加密时间戳,但实际观察下来它并不单纯是时间,还和搜索词、页码、当前会话有关。最省事的处理办法是:第一页的sn直接从初始请求返回的 HTML 里提取,之后的翻页再用同样的方式。别自己凭空构造,否则很容易掉进 404 或者验证码。

2.3 页面解析的目标元素

拿到列表页 HTML 后,重点找class="news-list"这个节点,里面每个<li>就是一篇文章。每个 li 里包含:

  • .txt-box下的.tit里的<a>是标题和链接
  • .account里的<a>是公众号名称和链接
  • .s-p是摘要
  • .time是发布时间

我用 BeautifulSoup 提取时的核心思路是先锁定news-list,再遍历li,然后用select_one分别取值。注意标题节点里有<a>标签,链接是相对路径,需要手动拼接域名。公众号链接同理,也是相对路径。

提取完后要判断是否有“下一页”。搜狗微信列表页底部有个翻页区域,如果当前页码不是最后一页,会出现“下一页”按钮,它的真实地址在 Javascript 函数pageTo的参数里,直接读取 HTML 里的onclick属性就行。

3. 把公众号文章列表抓下来:requests + BeautifulSoup 的完整脚本

3.1 先写一个稳健的请求会话

爬虫不要每次请求都新建一个 session,而是用requests.Session()保持 Cookie 的连续性。我把手动抓取的 Cookie 字符串解析成字典,塞进 session 里,并固定好 headers。这样做的好处是:搜狗在翻页时会校验同一会话内的连贯性,新建连接容易触发风控。

import requests from bs4 import BeautifulSoup import re 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://weixin.sogou.com/", }) def set_cookie_from_str(cookie_str): for part in cookie_str.split("; "): if "=" in part: key, value = part.split("=", 1) session.cookies.set(key, value)

这里要注意 Cookie 字符串里的空格和分号,最好直接从浏览器开发者工具里复制,格式是SUV=xxx; SNUID=xxx; IPLOC=xxx,不要带引号。如果某些字段缺失,搜索还能跑,但翻页几回就可能弹验证码。set_cookie_from_str 这个函数是我平时惯用的,批量设置时不容易漏字段。

3.2 获取第一页并提取关键参数

第一页的 URL 构造比较简单,但有两个点要注意。一是query参数要经过 URL 编码,中文必须处理;二是type参数必须带。我一般用urlencode处理,避免手动拼字符串出错。

from urllib.parse import urlencode def get_search_url(query, page=1): base = "https://weixin.sogou.com/weixin" params = { "type": 1, "query": query, "ie": "utf8", "page": page, } return base + "?" + urlencode(params) first_url = get_search_url("爬虫实战", 1) resp = session.get(first_url, timeout=10) if resp.status_code != 200: print("请求失败,状态码:", resp.status_code) html = resp.text

逻辑说明:ie=utf8是告诉服务端按 UTF-8 编码处理查询词,缺了它中文结果可能乱码。timeout必须设,搜狗有时候会长时间不返回数据,不设超时会让程序卡死。拿到 HTML 后,下一步要从中提取 sn 参数和总页数。提取 sn 的方法是正则匹配var sn = "..."这样的赋值语句,因为搜狗的前端 JS 里会生成当前页的 sn。总页数则从页面里的翻页按钮或“共 x 页”的文本里找。

def extract_sn(html): match = re.search(r'var sn = "(.*?)"', html) return match.group(1) if match else None def extract_total_pages(html): match = re.search(r'共(\d+)页', html) return int(match.group(1)) if match else 1

参数说明:extract_sn里我匹配的是var sn前面可能有空格或换行,正则写宽松一点更稳。有的页面 sn 变量名是sn =不带 var,可以写re.search(r'(?:var\s+)?sn\s*=\s*"(.*?)"', html)。总页数在结果少的时候可能不显示“共 x 页”,这时保守处理为 1,别强行翻页。

3.3 解析列表并保存成结构化数据

解析那一步我用 BeautifulSoup 的select_one和select配合,先拿到所有 li 再逐条处理。每条记录保存成字典,最后用 pandas 存成 CSV。注意时间字段是字符串,比如“3天前”“3月14日”,需要按当前日期推算一下,不想推就用原始字符串。

from datetime import datetime, timedelta import pandas as pd def parse_time(s): s = s.strip() if s.endswith("分钟前"): minutes = int(s[:-3]) return datetime.now() - timedelta(minutes=minutes) elif s.endswith("小时前"): hours = int(s[:-3]) return datetime.now() - timedelta(hours=hours) elif s.endswith("天前"): days = int(s[:-2]) return datetime.now() - timedelta(days=days) else: try: return datetime.strptime(s, "%m月%d日").replace(year=datetime.now().year) except ValueError: return None def parse_list_page(html): soup = BeautifulSoup(html, "html.parser") ul = soup.select_one(".news-list") records = [] if not ul: return records for li in ul.select("li"): title_node = li.select_one(".txt-box .tit a") account_node = li.select_one(".txt-box .account a") summary_node = li.select_one(".txt-box .s-p") time_node = li.select_one(".txt-box .time") if not title_node or not account_node: continue records.append({ "title": title_node.get_text(strip=True), "link": "https://weixin.sogou.com" + title_node["href"], "account": account_node.get_text(strip=True), "account_link": "https://weixin.sogou.com" + account_node["href"], "summary": summary_node.get_text(strip=True) if summary_node else "", "publish_time": parse_time(time_node.get_text(strip=True)) if time_node else None, }) return records

这里的逻辑说明:.news-list是列表容器,直接查它下面的li才能保证不会抓到页面上其他无关链接。get_text(strip=True)能去掉标题里的多余空格和换行。链接拼接我用了硬编码域名,如果你在测试环境里做代理转发,这里的域名要以实际访问为准。

3.4 翻页的循环控制

翻页的核心是带着上一页的 sn 请求下一页。注意每翻一页,sn 都会变,需要从当页的 HTML 里重新提取。如果某页拿不到 sn,我建议直接停止翻页,因为继续下去大概率被验证码拦截。

def crawl_articles(query, max_pages=3): all_records = [] page = 1 last_sn = None while page <= max_pages: url = get_search_url(query, page) if last_sn: url += "&sn=" + last_sn resp = session.get(url, timeout=10) if resp.status_code != 200: break html = resp.text if "请输入验证码" in html or "antispider" in html: print(f"第{page}页触发验证码,停止翻页") break records = parse_list_page(html) if not records: break all_records.extend(records) new_sn = extract_sn(html) if not new_sn: break last_sn = new_sn page += 1 return all_records

这段代码里有个容易被忽略的点:last_sn是从当前页提取出来的,请求下一页时必须带上。第一次请求不携带 sn 没问题,但从第二页开始不带 sn,搜狗会返回空白列表或直接跳验证码。翻页循环里我加了验证码关键词判断,一旦触发就立刻终止,避免无意义的请求加重封禁风险。

4. 翻页与去重:加密参数 sn 的生成和文章指纹的维护

4.1 sn 不是时间戳,别浪费时间逆向它

搜狗微信的 sn 参数在很多博客里被描述成某种加密时间戳,甚至有代码通过时间换算来构造。我拿抓包工具对比过不同时间、不同关键词下的 sn,发现同一时刻不同关键词产生不同的 sn,同一关键词间隔几秒再搜 sn 也不一样。这说明它至少混合了搜索词和会话状态,单纯用时间戳去撞,成功率很低。

我现在的做法就是不构造 sn,而是每次从页面里取。这个办法看似笨,但胜在稳定。取出来的 sn 能不能直接给下一页用?大多数情况下能,但偶尔会遇到参数被加进 URL 后返回的还是第一页内容。遇到这种情况,我判断是会话状态丢失,重新初始化 session 并带着最新 Cookie 访问第一页,往往就能恢复。

如果你非要去研究 sn 的生成逻辑,可以用浏览器断点调试搜狗的前端 JS,看到它往 URL 里填 sn 的代码。不过我建议先把业务跑通,再考虑逆向。

4.2 文章去重:不要信任标题,要信任链接和内容指纹

公众号文章经常出现同标题不同内容的情况,或者同一篇文被转载后标题不变。列表去重不能只比较标题,我一般用两层去重。第一层是链接去重,把已抓取的中转链接放进一个 set,重复的跳过。第二层是内容指纹去重,把标题加摘要拼接起来做 MD5,如果摘要为空,就只取标题前 20 个字加发布时间的日期部分做指纹。

import hashlib def make_record_fingerprint(record): title = record.get("title", "") summary = record.get("summary", "") date_part = "" if record.get("publish_time"): date_part = record["publish_time"].strftime("%Y-%m-%d") raw = f"{title}|{summary}|{date_part}".encode("utf-8") return hashlib.md5(raw).hexdigest() def deduplicate_records(records, seen_links, seen_fps): unique = [] for r in records: link = r.get("link") if link in seen_links: continue fp = make_record_fingerprint(r) if fp in seen_fps: continue seen_links.add(link) seen_fps.add(fp) unique.append(r) return unique

指纹去重的作用是应对同篇文章被多个公众号转载的情况。比如一篇热门技术文,你搜关键词时可能同时出现在五个公众号里,标题一样、摘要略有差异,这时链接去重无效,指纹去重能挡掉一部分。不要把发布时间整个放进指纹里,因为搜狗列表页显示的“3天前”本身是推算值,不同时间抓取结果会漂移。

4.3 增量爬取与断点续抓

如果你要长期跟踪某个关键词或某个公众号,别每次都从零开始抓。我习惯把已抓取链接存到一个本地文件里,每次启动前加载,抓取过程中实时追加。这样程序意外中断后重启,不需要重新抓已经处理过的页面。

def load_seen_links(filepath): try: with open(filepath, "r", encoding="utf-8") as f: return set(line.strip() for line in f if line.strip()) except FileNotFoundError: return set() def save_seen_links(filepath, links): with open(filepath, "w", encoding="utf-8") as f: for link in links: f.write(link + "\n")

注意 save 时选择写整个 set,而不是追加模式,是为了避免重复行积累。数据量大时写入频率不要太高,我一般是每翻完一页就写一次。这个文件就是你的“后悔药”,就算后面解析逻辑写崩了,链接还在,重新跑成本很低。

4.4 处理列表页字段缺失的情况

搜狗微信的列表页有时候会改版,导致.s-p或.time节点缺失。我的解析代码里对每个字段都做了空值兜底,但更稳的做法是先检查.txt-box是否存在,不存在就直接放弃这条记录,宁缺毋滥。缺失摘要的条目,指纹里 summary 就是空字符串,这时如果两条不同文章标题一样、发布时间都是同一天,就会被误判为重复。所以我建议在指纹里再加入公众号名,能显著降低误杀概率。

def make_record_fingerprint_v2(record): title = record.get("title", "") account = record.get("account", "") summary = record.get("summary", "") date_part = "" if record.get("publish_time"): date_part = record["publish_time"].strftime("%Y-%m-%d") raw = f"{title}|{account}|{summary}|{date_part}".encode("utf-8") return hashlib.md5(raw).hexdigest()

5. 高频踩坑与应对:验证码、封 IP、动态 token 的常见问题排查

5.1 现象:请求返回 302 跳转到验证码页

抓取列表页时,返回内容不是搜索结果,而是一个标题为“请输入验证码”的页面。这种现象通常在连续翻页超过 5 页或者频繁刷新搜索词时出现。

原因:搜狗对短时间内来自同一 IP 的连续请求做了风控,验证码是它的第一道拦截手段。此外缺少 Cookie 字段或 UA 不对时,更容易触发。

解决:第一,把上一章提到的 Cookie 字段完整带上。第二,请求间隔至少放大到 3 到 5 秒,不要用固定间隔,我用随机 3 到 6 秒。第三,如果验证码已经出现,停掉当前任务,等 10 分钟再跑。第四,建立 IP 池,用多组住宅代理轮换,别用那种大量人共用的免费代理,不然封得更快。

5.2 现象:翻页时下一页内容和第一页完全一样

我见过不少同行在这上面卡住。明明page=2的 URL 拼对了,sn也带了,但返回的列表还是第一页那十条。

原因:搜狗在部分情况下不识别单纯的page参数,它要求page和sn必须是一对。如果你第一页的 sn 是旧会话里提取的,第二页用这个 sn 但 session 里 Cookie 变了,后端会忽略 sn,默认给你返回第一页。

解决:检查 session 是否被重置过。我遇到这个问题时,直接把整个get_search_url改成在每次翻页前,重新从当页 HTML 提取新的 sn,而不是保存最初的 sn 用到底。另外,URL 里参数顺序也做约束,query和type要放在page和sn之前,防止服务端解析错位。

5.3 现象:解析得到的中转链接点开全是空白页

列表页拿到的链接格式是https://weixin.sogou.com/link?url=xxx,你用 requests 直接 GET 这个地址,返回的 HTML 里有一段 JavaScript,用于把浏览器重定向到真正的公众号文章页。直接访问时没有执行 JS 逻辑,所以拿不到最终 URL。

原因:微信文章的外链跳转在服务端只给了一个带 token 的重定向脚本,必须由浏览器或支持 JS 渲染的客户端执行。

解决:最简单的方案是用 Selenium 或 Playwright 打开中转链接,等待页面跳转后取当前 URL。我常用 Playwright 的page.goto()后等待networkidle,再读取page.url。如果不想引入重浏览器,也可以用lxml加正则去解析中转页里的url +=那段字符串,但那段 JS 有过两种格式变化,解析不如前端渲染稳定。

5.4 现象:正文页面拿到的是“环境异常”或“去验证”

从中转链接跳到mp.weixin.qq.com后,正文页可能显示“环境异常”。这通常发生在同一个 IP 在短时间内访问了大量不同公众号的文章,微信侧的服务对异常流量很敏感。

原因:微信正文页有自己的频率限制,和搜狗的风控不是一套系统,通过搜狗列表进来只是绕过了第一层,微信侧依然会校验访问模式。

解决:下载正文时放慢速度,每篇文章之间至少间隔 2 秒。如果确实需要批量抓,就重点抓正文里的文字和图片链接,不要请求页面里的其他静态资源。遇到“环境异常”时,换个代理 IP,然后清掉当前请求的 Cookie,重新走一次搜狗中转流程。这里有个血泪经验:不要在解析正文时开多线程,微信对并发访问的惩罚比搜狗重得多。

5.5 现象:搜狗偶尔会把公众号文章链接从 https 降级成 http

个别搜索结果的中转链接是http://开头,直接请求会收到不安全的警告,甚至被反爬。

原因:搜狗的中转链接生成逻辑里部分老页面没有强制 https,但现代浏览器和多数编程语言的 HTTP 客户端会拒绝携带完整 Referer 访问 http 链接。

解决:将抓取到的中转链接统一做replace("http://", "https://"),再发起请求。如果替换后出现证书错误,说明那个链接本身已经失效,放弃这条记录。这种链接占比不到 1%,不需要特意重试。

6. 进阶:把抓到的内容转成可检索的知识库文本

拿到文章列表只是第一步,实际项目中我们往往需要把正文保存成结构化文本,方便接入本地知识库或者做全文检索。我推荐按“一篇文章一个 Markdown 文件”的方式存储,文件名用“公众号名_标题_发布时间”的形式,正文里保留标题和原链接作为元信息,这样后续无论接入 Elasticsearch 还是直接给 AI 做 RAG,都很干净。

转换时注意三个细节。第一,正文里的图片地址是mmbiz.qpic.cn开头的临时链接,如果要做长期保存,建议单独把图片下载到本地,并在 Markdown 里替换成相对路径。第二,文章底部的“阅读原文”链接往往和正文无关,清洗时丢掉。第三,正文里的<section>标签嵌套很深,用 BeautifulSoup 的get_text(separator="\n")能保留段落换行,但公式和代码块会丢失格式,如果你的目标文章代码多,就要用html2text这类工具做转换。

我现在的习惯是抓取后先落成统一格式的 JSON,再通过一个清洗脚本转成 Markdown。用无形化网络去遮挡原文,这个词不好,我换一句:抓取原文后在本地做清洗。

from pathlib import Path import json, re def content_to_markdown(json_path, output_dir): with open(json_path, "r", encoding="utf-8") as f: article = json.load(f) title = article["title"] account = article["account"] link = article["link"] content_text = article["content_text"] content_text = re.sub(r"\n{3,}", "\n\n", content_text) md = f"# {title}\n\n" md += f"- 公众号:{account}\n" md += f"- 原文链接:{link}\n\n" md += content_text out_file = Path(output_dir) / f"{account}_{title}_{article['date']}.md" out_file.write_text(md, encoding="utf-8")

这段脚本里的content_text是从正文页提取的纯文本,我一般在提取时就用get_text(separator="\n")把段落拆开。正则压缩连续换行是为了避免原文里空行过多,让 Markdown 渲染得更紧凑。文件名里有公众号名和日期,就算标题重复也不会互相覆盖。

如果你要接入知识库系统,建议把生成好的 Markdown 再做一次切分,按二级标题或每 500 字切一个块,存成 JSON 后批量导入。这样的切分粒度对后续向量化更友好。我从那以后每次跑完抓取任务,都会强制走一遍“列表入库、正文转 Markdown、切块导入”这三步,宁可多花五分钟清洗,也不在检索的时候翻车。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 17:45:23

极化双基地雷达系统仿真:从双基地几何到四通道回波生成的MATLAB实现

简介&#xff1a;一套用Matlab实现的极化双基地雷达系统仿真程序&#xff0c;用于估计目标距离与速度&#xff0c;并完整考虑发射器、接收器和目标运动学。面向雷达系统设计、信号处理领域的学习者与工程师&#xff0c;适合作为双基地雷达与极化处理的入门示例。压缩包共13个文…

作者头像 李华
网站建设 2026/10/29 17:45:18

用Python构建搜狗微信搜索爬虫:公众号文章采集与反爬实战

简介&#xff1a;通过搜狗搜索引擎定向采集微信公众号文章的Python工程&#xff0c;基于Scrapy框架构建&#xff0c;面向需要批量获取公众号历史文章、快速产出数据集的开发者和数据分析人员。工具支持按公众号ID与关键词两种检索方式&#xff0c;自动抓取标题、正文、发布时间…

作者头像 李华
网站建设 2026/10/29 17:44:49

营销技能落地必须绑定具体场景与工具链

我无法根据当前输入生成符合要求的博文。 原因如下&#xff1a; 项目标题 "marketingskills" 是一个宽泛的通用术语&#xff0c;缺乏具体项目指向性&#xff08;如无技术栈、无应用场景、无实现目标、无行业背景&#xff09;&#xff1b; 项目正文为空&#xff1b…

作者头像 李华
网站建设 2026/10/29 17:44:48

RBCADS 5.0滚动轴承设计实战:从选型计算到寿命校核的完整指南

简介&#xff1a;RBCADS 5.0滚动轴承计算机辅助设计系统2005正式版的docx文档&#xff0c;面向轴承制造企业的设计师与工程技术人员&#xff0c;用于解决轴承产品设计效率低、参数优化难、图纸绘制繁琐等问题。文档收录6A角接触球、6B四点接触式角接触球、6C单列圆锥滚子&#…

作者头像 李华
网站建设 2026/10/29 17:44:23

人工神经网络破解小电流接地选线难题:从仿真到落地全路径

简介&#xff1a;一份聚焦小电流接地故障选线问题的人工神经网络建模研究PDF&#xff0c;适合电力系统继电保护人员、配电网运维人员&#xff0c;以及关注深度学习方法在故障诊断中应用的算法学习者。论文以110kV/35kV配电系统为例&#xff0c;从故障暂态行波特征分析入手&…

作者头像 李华
网站建设 2026/10/29 17:43:34

HagiCode Desktop混合分发架构:大文件下载优化实践

1. HagiCode Desktop混合分发架构设计解析HagiCode Desktop作为一款面向开发者的生产力工具&#xff0c;其混合分发架构设计巧妙解决了大文件下载的痛点问题。这个架构最核心的创新点在于将传统CDN分发与点对点(PP)传输技术有机结合&#xff0c;在保证下载可靠性的同时显著提升…

作者头像 李华