
简介微信爬虫完整版是一份面向Python初学者的微信平台数据采集与分析项目实例解决从公众号文章、朋友圈等公开内容中获取数据并进行初步挖掘的问题。它围绕爬虫基础、反爬绕过、数据清洗与可视化四个环节展开适合正在学习Requests、BeautifulSoup和数据分析库的开发者参考。压缩包共3个文件、2.45MB包含Wechat.py主程序、bg.jpg配图及simkai.ttf中文字体。其中Python脚本承载核心爬虫逻辑ttf字体可解决matplotlib绘图中文乱码图片则用于界面或笔记展示。目前已有2323人学习下载资源量轻量但结构完整。入手后可得到可直接运行的爬虫脚本其中包含请求构造、HTML解析、UA伪装等关键写法也提供应对验证码、IP限制的思路脚本内还展示了朋友圈发布频率、公众号阅读量分布等基础统计方法配合pandas清洗与matplotlib可视化可快速上手微信数据抓取并进一步延伸到情感分析、关键词统计等方向。1. 微信爬虫完整版从一条搜索链接到本地文章库的全流程微信爬虫完整版这五个字很多人第一反应是抓聊天记录、抓好友列表但我做了几年内容采集后可以明确告诉你真正能被长期复用、稳定出数的微信数据是公众号文章。个人号数据要么违反平台规则要么方案脆弱今天跑通明天就失效而公众号文章有公开检索入口只需要解决搜索、解析、去重和增量这四件事。这篇文章要写的就是一套完整可落地的微信文章采集方案从公开的微信文章检索入口构造请求到列表解析、正文清洗、SQLite去重再到定时增量任务和常见反爬处理。适合正在做公众号竞品分析、选题追踪或语料积累的开发者也适合想自建一个本地微信文章库来给下游NLP或报表喂数据的人。2. 公众号文章抓取的路线选型为什么公开检索入口是最稳的地基在动手写第一行代码之前先花点时间选路线。微信文章的数据来源有好几条常见的是公开检索入口、客户端抓包、小程序接口逆向这三类。很多人一上来就盯着最省事的客户端抓包以为装上证书就能把手机里的文章导出来结果遇到签名校验、证书固定、版本升级后全部失效一个“完整版”活生生做成了“一次性版”。2.1 第三方微信检索入口不用登录也能拿到的公开文章入口目前最能稳定跑通的是浏览器里就能访问的第三方微信文章检索页。它不要求微信登录不要求安装客户端也不依赖某个闭源协议。页面结构相对规整拿到后通过常规HTML解析就能提出列表数据。它主要有两个检索类型type1 搜公众号type2 搜文章。我们做文章采集固定用 type2query 放关键词page 翻页。实际的请求地址格式大概是{search_base}/weixin?type2query关键词page1ieutf8返回的是一个完整HTML页面。注意搜索词必须做URL编码否则中文字符容易被服务端误解。我的习惯是用 requests 的 params 参数自动编码而不是手工拼接字符串这样能少踩一个编码坑。这个入口也有它的限制单页结果数量固定搜索结果里不包含阅读数和点赞数个别关键词会触发验证码。但从长期维护角度看它是三条路线里唯一不需要处理登录态和二进制协议的。2.2 客户端抓包与协议逆向为什么不适合新手的完整项目有些仓库里的“完整版”会让你在手机上装证书、抓包从微信App的HTTPS流量里还原文章详情接口或者直接在电脑上跑一个模拟客户端协议。这类方案在演示环境里看起来特别棒能拿到公众号名称、阅读数、点赞数这些公开检索页面上没有的字段。但它的代价是微信客户端的加密逻辑几个月变一次证书校验、请求头签名、设备指纹、风控行为检测一层套一层。你花两周逆向出来的接口可能过几天就被一次App更新打断。更现实的问题是它对新手不友好。抓包出来的二进制必须反序列化字段映射错一个就全盘错。就算你把接口调通了也会发现每次启动都要处理环境检查、组件更新、设备授权这些额外任务。整套代码的维护成本远高于它多给你的那几个字段。所以我的判断是只有确实需要阅读数、点赞数且你能接受长期维护逆向代码时才值得走这条路线。否则别把精力耗在跟客户端“猫鼠游戏”上。公众号文章的正文、标题、摘要、发布时间公开检索入口基本都能拿到对大多数业务场景已经够用。2.3 我选公开检索入口的三个理由第一不需要维护登录态。这个入口只要带一个常见的浏览器User-Agent就能请求不像其他入口要处理扫码、短时token、会话过期这些事。第二搜索词可控性高。你可以用任意关键词去搜效果上就像一个微信公众号搜索引擎非常适合做“关键词舆情跟踪”。第三页面变化有缓冲。检索页结构虽然也改但改版频率比客户端协议低得多而且就算CSS结构变了你也能通过调整选择器快速跟上。用一张表总结三种路线路线需要登录维护成本能拿到的字段适合场景第三方微信检索入口否低标题、摘要、正文、公众号名、文章链接长期稳定采集客户端抓包是高阅读数、点赞数等额外指标短期定向分析小程序接口逆向是极高文章详情、评论等不建议新手尝试选型不是越复杂越好。如果你的目标是“每天稳定入库一千篇文章”那公开检索入口就是答案如果你想做“一篇文章的深度指标分析”再考虑复杂路线。大多数业务场景先把手头管道跑稳比什么都重要。3. 搭起最小采集管道请求、解析、正文清洗、入库这一章是核心。完整版和Demo版的差距就从这里开始Demo只会打印标题完整版会把每一步都落成函数并且考虑失败和重复。3.1 构造带会话的请求头把爬虫伪装成正常浏览器微信生态背后的风控体系不会对陌生UA直接放行。我第一次尝试时用requests默认UA结果十个请求里八个被丢到验证码页。后来固定了会话模板把浏览器里常见的UA字符串、Referer、Accept-Language都带上情况立刻好转。下面是最小头发模板import os import requests SEARCH_BASE os.getenv( WECHAT_SEARCH_BASE, https://weixin-search.example.com, # 换成你实际使用的检索入口域名 ) session requests.Session() session.headers.update({ 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, Referer: SEARCH_BASE /, Accept-Language: zh-CN,zh;q0.9, })这里最关键是session对象。相比每次请求都新建requests.getsession会把连接池和cookie自动复用。检索入口对cookie非常敏感第一次访问拿到什么cookie后续翻页最好还是同一套session能帮你省掉手动管理cookie的麻烦。Referer设成首页是为了让服务端看起来你是从首页进入搜索的不要把它删掉。接下来写搜索函数def search_articles(keyword, page1): params { type: 2, # 2表示搜文章1表示搜公众号 query: keyword, # 搜索关键词 page: page, # 页码从1开始 ie: utf8, # 输入编码 } resp session.get( f{SEARCH_BASE}/weixin, paramsparams, timeout10, ) resp.raise_for_status() resp.encoding utf-8 return resp.text逻辑说明params里的type2决定本次请求是文章搜索query是关键词page控制翻页。没有把page写在URL里而是交给requests编码这样中文关键词、空格、符号都不会出错。超时设成10秒防止入口响应卡住时整个采集进程被吊死。3.2 解析搜索结果列表BeautifulSoup选择器和字段补全拿到HTML之后先定位到列表容器。公开检索页的典型结构是外层有一个 news-list 容器每个结果是一个 lili 的id以固定前缀sogou_vr_11002601_box开头里面包含文章标题、公众号名、摘要等。用BeautifulSoup写解析函数from bs4 import BeautifulSoup def parse_search_result(html): soup BeautifulSoup(html, lxml) items [] for li in soup.select(li[id^sogou_vr_11002601_box]): title_a li.select_one(h3 a) if not title_a: continue title title_a.get_text(stripTrue) link title_a[href] account account_a li.select_one(.account) if account_a: account account_a.get_text(stripTrue) summary info li.select_one(.txt-info) if info: summary info.get_text(stripTrue) items.append({ title: title, link: link, account: account, summary: summary, }) return items参数说明li[id^...]是CSS属性前缀匹配专门用来圈住搜索结果列表项。select_one(h3 a)是取每个列表项里的文章链接。这里有个小坑有的页面标题里会混入em高亮标签get_text(stripTrue) 会把标签里的文字和正常文字拼在一起这是预期的不用刻意去掉。链接拿到的往往是跳转链接而不是最终文章URL我们在第四章做还原处理。3.3 取出正文并清洗成可存储文本点进文章链接之后正文在div#js_content这个节点里。微信编辑器生成的排版非常混乱段落可能是一层套一层的section、p、span如果直接拿整段HTML存下来后续做NLP会很痛苦。我一般把正文块内的所有文本按块提取逐行拼接def fetch_article_body(url): resp session.get(url, timeout15) resp.encoding utf-8 soup BeautifulSoup(resp.text, lxml) content_div soup.select_one(#js_content) if not content_div: return lines [] for block in content_div.find_all([p, section]): text block.get_text(, stripTrue) if text: lines.append(text) return \n.join(lines)逻辑说明先按p和section标签圈出正文块再把每个块里的纯文本析取出来。get_text(, stripTrue)中第一个参数是标签之间的连接符传空字符串是为了避免段落内部出现多余空格stripTrue 去掉首尾空白。最终得到一个每行一个自然段的纯文本正文。这样入库后无论是搜索、做词频统计还是丢给后续模型都能直接消费。不过要注意这个函数只解决静态正文。部分公众号文章在正文里还有“滑动查看完整内容”“点击展开”这类交互这些内容不会直接放在js_content里抓不到属于正常情况需要额外判断。3.4 落库SQLite唯一索引与INSERT OR IGNORE正文取出后不能每次全量覆盖否则断点续跑会重复插入。我用SQLite做存储一张表搞定所有字段然后用URL的唯一索引做去重import sqlite3 conn sqlite3.connect(wechat_articles.db) conn.execute( CREATE TABLE IF NOT EXISTS articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT, url_hash TEXT UNIQUE, title TEXT, account TEXT, summary TEXT, content TEXT, published_at TEXT DEFAULT , created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.commit()这里的核心不是id而是url_hash。微信文章同一篇可能有不同带参数的入口URL只对 raw URL 做唯一约束不够所以要先用hash指纹。写入时用INSERT OR IGNORE重复hash直接跳过import hashlib def save_article(item): url_hash hashlib.md5(item[url].encode(utf-8)).hexdigest() conn.execute( INSERT OR IGNORE INTO articles (url, url_hash, title, account, summary, content) VALUES (?, ?, ?, ?, ?, ?) , ( item[url], url_hash, item[title], item[account], item[summary], item[content], ), ) conn.commit()参数说明md5在这里不用于加密只用于生成短长度唯一指纹碰撞概率对文章量级可以忽略。字段里专门留了published_at空字段有的列表页能抓到日期有的抓不到先兜底存空字符串后续解析到时间再UPDATE。这里不需要担心id自增溢出SQLite足够。4. 完整版要做增量采集链接还原、定时调度与断点续跑这一章继续补充“完整版”的关键增量能力。如果脚本只能跑一次不叫完整版只是一个教程Demo。让它能每天自动跑才是真正的采集工具。4.1 跳转链接还原入库前必须做的一步结果页里的链接大多不是直接的mp.weixin.qq.com/s/xxx文章地址而是一个带跳转参数的中间链接。如果直接拿这个URL入库会出现两个问题一是重定向后再打开URL和入库时对不上二是中间链接可能会定期清理参数旧链接可能失效。所以每次入库前我先做一次无跟随请求拿到最终Locationdef unwrap_redirect_link(short_url): resp session.get(short_url, allow_redirectsFalse, timeout10) if resp.status_code in (301, 302): location resp.headers.get(Location, ) if location.startswith(http): return location return short_url逻辑说明allow_redirectsFalse 让requests不要自动跟随跳转这样响应头里会保留Location字段Location才是真实公众号文章地址。拿到后只要以它为准做hash和入库后续访问就能直接命中原链接。这里要注意Location有时候是带__biz、mid、idx、sn参数的完整链接也有极少数情况是相对路径前面补全域名即可。4.2 定时增量任务APScheduler 跑每小时或每天的全量扫描增量采集的方案不是每次抓最新而是把关键词列表配置好定时跑一轮搜索。用APScheduler非常省事import time from apscheduler.schedulers.blocking import BlockingScheduler def run_incremental_scrape(): for keyword in [量化交易, Python开发, AI产品]: for page in range(1, 4): html search_articles(keyword, page) items parse_search_result(html) for item in items: real_url unwrap_redirect_link(item[link]) item[url] real_url item[content] fetch_article_body(real_url) if item[content]: save_article(item) time.sleep(3) scheduler BlockingScheduler() scheduler.add_job(run_incremental_scrape, interval, hours6, next_run_timeNone) scheduler.start()参数说明interval表示按固定时间间隔hours6 即每6小时跑一轮。next_run_timeNone表示任务第一次进场时不立即执行而是等到下个周期如果你想让首次启动就立刻跑可以把它改为datetime.now()。每个关键词只抓前三页是我常用的保守设置80%的时效性文章都在前三页再往后会明显加重翻车概率。time.sleep(3)是每篇文章之间的最短间隔宁可慢不要被频控。4.3 断点续跑与失败重试不要让一串坏URL中断整个管道真实采集时总有几篇文章因为被删除、超时、请求异常导致失败。如果让异常直接抛出去整个调度任务会在同一个位置报废。我的习惯是对“单篇文章抓取”加一层重试但严格控制重试次数from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, max10)) def fetch_article_body_with_retry(url): content fetch_article_body(url) if not content: raise ValueError(empty content) return content逻辑说明stop_after_attempt(3)表示最多尝试三次wait_exponential(multiplier1, max10)表示第一次失败等1秒、第二次等2秒、第三次等4秒指数退避而不是死循环快速重发。如果三次都拿不到正文就让它抛出异常由上层捕获或者直接跳过这篇文章不让坏数据影响后续任务。这里的关键是判断content为空时主动抛错否则脚本会把空正文入库将来清洗时反而更难识别。5. 微信爬虫避坑指南验证码、频控、正文截断与重复入库跑通第一版很容易长期稳定才是难点。这里整理我踩得最频繁的四个坑每条按现象、原因、解决写清楚。5.1 验证码弹窗返回的不是列表而是 antispider 页面现象搜索函数返回的HTML里没有预期的列表项而是出现一张图片验证码或者页面标题写着“请输入验证码”。原因请求频率过高、UA特征明显、同一环境在短时间内多次翻页都会触发反爬盾。它是在服务端拦截不是简单的状态码。这类页面通常有自己的专属路径看到它的那一刻就应该意识到节奏太快了。解决先降速。把单次请求间隔从1秒调大到3到5秒翻页时在每页之间加随机时长。另外把验证码出现次数记录下来当连续三次出现验证码时主动停一个小时而不是继续试探。控制最小等待时间的代码可以这样import random import time time.sleep(random.uniform(3, 6))这里用随机数是为了避免每次sleep完全一样形成机械节奏。3到6秒的区间对单机脚本已经足够温和如果还是触发就把区间继续拉大。5.2 搜索词被限制换关键词后突然全部失效现象第一批关键词跑得好好的换了一个比较“营销化”的词比如“赚钱”“代购”瞬间所有请求都返回验证码页面。原因部分搜索词在风控侧有更高的权重。它不会告诉你是哪个词触发只会用验证码兜底。这跟网站自身的运营策略有关你也无法从文档里找到完整黑名单。解决把关键词列表拆成批次批量越细越好。每次提交的关键词数量控制在五个以内每跑完一个关键词停10秒以上。如果反复触发就暂时放弃该批次而不是暴力重试。这批关键词可以等几小时后再试不少情况下会自动恢复。5.3 正文只抓到一句话或空白现象fetch_article_body返回的内容只有“验证”“正在加载”之类的文本或者返回空字符串。原因文章页可能返回了风控页面部分正文使用JS异步加载还有少数文章由于排版原因正文不在js_content里。这三种情况的表现都是正文为空或长度极短。解决先拉取正文页面源码检查是否包含div#js_content。如果不包含说明请求被风控或文章已删除。我一般会把这类链接记录到一个failed_pages表存URL和失败原因下一轮再补抓不直接放弃。补抓时可以把同一URL的请求间隔拉长到几十秒很多临时风控会在等待后自动解除。5.4 重复入库同一篇文章链接带不同query参数现象今天存了一篇文章明天搜到同主题文章发现库里出现两条url不同但真实地址基本一致的记录。原因微信文章链接mp.weixin.qq.com/s?__bizxxxmidxxxidx1snxxx可能同时被拼上chksm等参数手动采集时还可能加上utm_source追踪参数。解决入库前先对URL做参数白名单过滤只保留__biz、mid、idx、sn这四个关键参数其他参数全删掉然后再做hash和去重。from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse def normalize_weixin_url(url): parsed urlparse(url) if not parsed.hostname: return url keep_keys {__biz, mid, idx, sn} query [(k, v) for k, v in parse_qsl(parsed.query) if k in keep_keys] new_query urlencode(query) return urlunparse(parsed._replace(querynew_query))逻辑说明_replace是namedtuple的替换方法能保留原有协议、域名和路径只重写query。这比直接用split处理字符串安全得多。做完这步再算md5重复记录会显著减少。6. 把采集器变成可持续的内容工作流导出、统计与下一个迭代点工具只有跑起来才有价值。这一章分享两个我常用的“后处理”技巧让采集结果真正服务于内容运营。6.1 用SQL直接生成本周公众号热文榜不需要写复杂脚本SQLite命令行就能完成。比如统计某个关键词在本周命中哪些公众号、各命中多少篇SELECT account, count(*) AS article_count, substr(created_at, 1, 10) AS capture_day FROM articles WHERE title LIKE %量化% AND created_at datetime(now, -7 days) GROUP BY account, capture_day ORDER BY article_count DESC;这里的逻辑是title LIKE 做关键词过滤created_at限制在最近7天分母按公众号和抓取日期分组。如果业务上要看长期趋势把时间条件去掉即可。如果你希望文章title里关键词高亮也可以在Python里做但SQL这两行已经能帮运营快速判断哪个号输出最密集。6.2 一键导出JSON给下游NLP任务用如果后续要训练摘要模型或做知识库最好按JSON一次性导出全部正文import json import sqlite3 conn sqlite3.connect(wechat_articles.db) conn.row_factory sqlite3.Row rows conn.execute( SELECT title, account, content, created_at FROM articles WHERE content ! ).fetchall() with open(wechat_corpus.json, w, encodingutf-8) as f: for row in rows: f.write(json.dumps({ title: row[title], account: row[account], content: row[content], captured_at: row[created_at][:19], }, ensure_asciiFalse) \n)保存成JSON Lines格式每行一条记录忽略空正文。这样无论是Pandas、Spark还是微调脚本都能逐行读取。6.3 下一个迭代点图片资源与自动摘要采集做到这一步已经能稳定产出结构化正文但还有两件事值得接着做。第一件是把正文里的图片下载到本地否则文章删掉后图片链接会失效。第二件是给每篇正文跑一次自动摘要然后把摘要和关键词一起存进新字段。这样你的数据库就能从一个“爬虫结果表”变成一个“选题库”后续运营找资料时可以直接搜索摘要而不是翻正文。我自己在这里吃过的亏是某次跑完一周采集没有做图片本地化结果源文章在平台被删除配图全部404导致复盘材料缺了关键截图。后来我在入库阶段就顺手下载图片只要正文里出现img src...就保存到images目录并把路径回写到文章记录。那一版才算真正可以长期依赖。如果你也准备做同类采集先把去重、频控、断点续跑这三件事写牢固再谈加字段。希望帮到你。本文还有配套的精品资源点击获取