微信小程序爬虫实战:从抓包到Python GUI工具开发
2026/9/16 22:24:40 网站建设 项目流程

很多人一听到爬虫,就觉得得跟反爬、加密、验证码斗智斗勇,其实真不是所有项目都那么硬核。今天拿“随机目的地旅行”这个微信小程序来练手,它的功能很简单——每次给你扔一个陌生的目的地,帮你在没灵感的时候找个地方去转转。我们要做的事也很直接:把小程序背后的接口找出来,然后用 Python 复现成自己能调用的函数,最后包一个带按钮的 GUI 工具,点一下就能刷出一条随机地址,顺手还能复制下来。

这个案例特别适合基础阶段的 Python 爬虫学习者,因为它把“抓包分析 → requests 模拟 → JSON 解析 → GUI 封装”这条最核心的链路完整走了一遍,难度不大,但每个环节都能体会到真实爬虫项目的思维方式。你不需要懂复杂的 JavaScript 逆向,也不需要去解小程序包,只要会装工具、会看网络面板、会写最基础的 Python,就能把它跑起来。下面我把整个实操过程拆开讲,包括我踩过的坑和调试经验。

1. 动手前想清楚:小程序爬虫和普通网页爬虫差在哪

1.1 这个案例能学到什么

先说技术点。这个项目麻雀虽小,但五脏俱全,涉及的东西主要有四块:一是抓包分析,搞清楚小程序在什么时候调用了哪个接口;二是 HTTP 请求模拟,用 Python 的 requests 库把接口复现出来;三是数据解析,把返回的 JSON 里我们关心的字段提取出来;四是桌面界面封装,用 tkinter 做一个可以点按钮操作的窗口程序。

为什么选微信小程序而不是普通网页?一个很现实的理由是,现在越来越多的服务只放在小程序里,网页端反而没有。你以后遇到“这个数据只有小程序里有”的情况,今天这套玩法就完全能复用。而且“随机目的地旅行”这种工具类小程序,接口设计非常简单,返回的数据也很干净,没有复杂的加密参数,对新手特别友好。我故意没有选那种需要破解签名、逆向 JS 的目标,因为那是进阶内容,基础篇先把这条干干净净的路走通更重要。

1.2 方案选型:为什么要走“抓包→复现接口”这条路

有人可能会问,直接去下载小程序包反编译,看它源码里的接口地址,这不更快吗?技术上确实可行,但我不推荐,尤其是基础阶段。一方面反编译别人的小程序包有合规风险,另一方面对新手来说,解析一大堆编译后的 js 文件,很容易被绕晕。

正确且稳妥的做法是抓包。原理很简单:小程序在运行的时候,本质上还是通过 HTTP/HTTPS 请求从服务器拿数据。我们只需要在中间加一个代理工具,把它发出的网络请求“偷看”一遍,就能拿到接口地址、参数和返回结果。这个过程完全不需要去碰小程序的代码,也不涉及任何修改和破解,只是“观察”网络流量而已。

我用的是 Charles,免费版已经够用了。你如果更习惯 Fiddler 也没问题,思路完全一样。抓包这条路的好处是:它逼着你去理解一次完整的 HTTP 请求长什么样——URL、Headers、Query 参数、请求体、响应体,这些才是爬虫真正要面对的东西。我见过很多新手一上来就去背反爬技巧,但对“一个请求是怎么发出去、又是怎么回来的”毫无概念,那后面肯定走不远。

2. 抓包分析:把“随机目的地”的接口揪出来

2.1 抓包工具准备与代理配置

Charles 的安装就不多说了,官网下对应系统的版本,一路下一步就行。关键是后面的代理配置,我第一次用的时候就在这卡了快半小时。

先说最简单的方案:用电脑版微信打开小程序,然后让 Charles 接管电脑的 HTTP 代理。Charles 默认会开启一个代理端口,通常是 8888,你打开Proxy → Proxy Settings能看到。要让系统流量走这个代理,Charles 会自动帮你配置系统代理,但更稳妥的做法是在 Windows 的“代理”设置里手动把127.0.0.1:8888填进去。

然后是 HTTPS 包的解密。不处理这一步的话,你会看到请求列表里全是带锁的https://条目,点开也看不到内容。Charles 的解决方法是安装它自己的 SSL 证书。菜单栏进Help → SSL Proxying → Install Charles Root Certificate,把根证书装到系统“受信任的根证书颁发机构”里。装完以后,还要在Proxy → SSL Proxying Settings里勾选启用,并在 Host 和 Port 里各填一个*,表示解密所有 HTTPS 流量。

这里有个很关键的细节:微信 PC 版对网络要求比较敏感,如果代理配置正确但还是打不开小程序,先确认 Charles 的“SSL Proxying”有没有打开,再看微信是不是处于某个特殊的登录态。实在不行可以换手机抓包,让手机和电脑连同一个 Wi-Fi,手机代理指向电脑 IP 的 8888 端口,然后在手机端也装一次 Charles 证书。不过基础篇用 PC 微信一般就够,手机那套配置更适合进阶。

2.2 定位核心接口:点一次按钮,盯住变化

配置好代理后,打开微信 PC 版,进入“随机目的地旅行”小程序。注意,这时候不要急着点功能按钮,先让 Charles 静默观察一会儿,熟悉一下这个小程序启动时加载了哪些请求。这些请求大多跟页面渲染、用户登录有关,不是我们想要的。

等页面加载完,在 Charles 底部的查找框里输入过滤条件,比如destinationrandomtravel这些关键词,然后回到小程序里点击“随机生成”按钮。我的习惯是每点一次操作,就马上看 Charles 里新增了哪些请求。如果你预先设置好过滤词,几轮操作下来,核心接口就会浮出水面。

我第一次抓的时候,接口名里有random这个词,特别好认。你实际操作时,重点找这些特征:请求方式一般是 GET,URL 里带有跟“目的地”“旅行”“随机”相关的英文单词,响应体是一段 JSON。找到了就点开看它的 Headers 和 JSON 响应,接下来要做的事就是把它记录下来。需要记住的信息包括完整 URL、请求方式和所有 Header 字段,尤其是 User-Agent、Referer 和可能存在的自定义 Header。

2.3 读懂返回的 JSON 结构

我抓到并做了脱敏处理的接口,大概是长这样的(不同版本接口可能略有差异,结构作为参考):

GET https://api.example-travel.com/v1/destination/random?type=city&count=1

关键 Headers 如下:

  • User-Agent: 微信 PC 版浏览器的 UA,很长一串,里面带有MicroMessenger字段,这表明请求来自微信客户端
  • Referer:https://servicewechat.com/wx1234567890abcd/1/page-frame.html,这是微信小程序的固定套路,很多接口会校验这个字段

响应长这样:

{ "code": 0, "msg": "success", "data": { "id": "d20240512", "city": "喀什古城", "country": "中国", "region": "新疆", "tag": "人文历史", "budget": "3000-5000", "season": "秋季", "reason": "千年老城,自带胶片感滤镜,适合慢慢逛" } }

看到这种结构就可以放心了:字段非常规整,没有嵌套太深,没有加密,纯 JSON。我们的目标就是从data里把regioncitycountrytagbudgetseasonreason这几个字段提取出来,组成一句通顺的“旅行灵感”。

这里提醒一个新手很容易忽略的点:抓包时看到的请求参数和返回结构,可能跟另一个账号或另一个版本下看到的不完全一样,这很正常。我们学习的是方法,不是背接口。你只要抓住“找一个包含随机信息的 GET 请求 → 观察返回 JSON → 提取目标字段”这个套路,换个小程序照样能做。

3. Python 复现接口:从一次请求到一个可用的类

3.1 环境准备与依赖安装

写 Python 代码之前,先把环境理一理。我的建议是给这个项目单独建一个虚拟环境,免得以后包版本打架。命令很简单:

python -m venv venv

Windows 下激活环境:

venv\Scripts\activate

接下来安装依赖。这个项目基础阶段只需要一个 requests,GUI 用的 tkinter 是 Python 自带的,不用额外装。

pip install requests

如果你还没装 Python,直接去官网下最新稳定版,安装时记得把“Add Python to PATH”勾上。环境弄好后,可以用python --version验证一下。

3.2 用 requests 模拟第一次请求

先写一个最简单的版本,目的只有一个:确认我们能用 Python 请求到这个接口,并且能拿到 JSON。

import requests url = "https://api.example-travel.com/v1/destination/random" 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 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI WindowsWechat", "Referer": "https://servicewechat.com/wx1234567890abcd/1/page-frame.html", } params = {"type": "city", "count": 1} resp = requests.get(url, headers=headers, params=params, timeout=10) print(resp.status_code) print(resp.text)

这里有几个地方要解释一下。headers里最重要的是Referer,我实际测试时发现,把 Referer 去掉或改成别的,接口会直接返回 403。这其实是小程序的常见校验手段,它只允许来自servicewechat.com域名的请求访问。另外User-Agent尽量保留微信相关的字段,有的接口会对 UA 做识别。

timeout=10这个参数是必须加的,不加的话,接口一旦无响应,你的脚本就会无限期挂在那里。尤其后面要做 GUI,请求卡住会导致界面完全没反应。

运行这段代码,如果能看到返回的 JSON,说明请求已经成功了。下一步就是解析和处理数据。

3.3 把功能封装成“随机地址生成器”

一个合格的爬虫脚本不应该只是把resp.text打印出来,而是要变成可复用的函数或类。我习惯把接口请求封装成一个专门的类,这样后面接 GUI、接批量生成、接数据存储都方便。

import requests import random class RandomDestination: def __init__(self): self.base_url = "https://api.example-travel.com/v1/destination/random" self.session = requests.Session() self.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 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI WindowsWechat", "Referer": "https://servicewechat.com/wx1234567890abcd/1/page-frame.html", }) # 用来去重和记录历史 self.history = [] def fetch_one(self): """请求接口,解析出目的地信息""" try: resp = self.session.get( self.base_url, params={"type": "city", "count": 1}, timeout=10 ) resp.raise_for_status() data = resp.json().get("data", {}) return { "region": data.get("region", ""), "city": data.get("city", ""), "country": data.get("country", ""), "tag": data.get("tag", ""), "budget": data.get("budget", ""), "season": data.get("season", ""), "reason": data.get("reason", ""), } except Exception as e: print("请求失败:", e) return None def generate(self, count=1): """批量生成 count 条不重复的目的地""" results = [] while len(results) < count: item = self.fetch_one() if not item: continue if item["city"] not in self.history: self.history.append(item["city"]) results.append(item) return results def format_one(self, item): """把字典格式化成适合展示的文本""" return ( f"目的地:{item['region']} · {item['city']}({item['country']})\n" f"标签:{item['tag']} | 预算:{item['budget']} | 适合季节:{item['season']}\n" f"推荐理由:{item['reason']}" ) if __name__ == "__main__": gen = RandomDestination() items = gen.generate(3) for item in items: print(gen.format_one(item)) print("-" * 40)

这个类做了几件有用的事:用requests.Session保持连接和复用 headers;把抓到的数据整理成统一的 dict;用history列表实现简单的去重,避免连续生成重复城市;format_one方法负责把字典变成人话。

说一下为什么用 Session 而不用每次请求都重新加 headers。Session 会自动保存连接信息和 Cookie,小程序接口有时候会先下发一个校验 Cookie,后续请求要带上去才能通过。用 Session 能避免很多奇怪的坑。

批量生成那里我加了个 while 循环,实际使用时注意控制次数,别一次性生成几百个,容易触发频率限制。我在本地测试时,连续请求 30 次左右是没问题的,再高就出现限流了。

4. 加上 GUI:让脚本像个正经工具

4.1 界面需求梳理

脚本功能搞定之后,就该考虑体验了。如果只是自己用,命令行完全够;但如果你想发给朋友用,或者自己就是喜欢点按钮的感觉,那就得做个界面。

我设计的交互很简单,四个区域:顶部一块展示文本区域,用来显示生成的目的地;底部一排按钮,包括“生成一个目的地”“批量生成 10 条”“清空记录”“复制当前内容”。所有操作都用 tkinter 完成,不需要额外装库。

为什么要用多线程?这点必须提前讲清楚。如果直接在按钮点击事件里发 HTTP 请求,请求在网络等待的这段时间里,tkinter 的主循环会被卡住,表现就是窗口变白、按钮点了没反应,严重的话系统直接提示“程序未响应”。这不是代码逻辑错,而是网络请求阻塞了界面主线程。解决办法是让请求在后台线程里跑,完成后把结果通过队列传给主线程刷新界面。

4.2 tkinter 界面实现:完整示例代码

下面是我写的一个精简但可用的 GUI 版本。我尽量保留了必要的结构,但去掉了跟核心逻辑无关的花哨美化。

import tkinter as tk from tkinter import scrolledtext import threading import queue from random_destination import RandomDestination class TravelGUI: def __init__(self, root): self.root = root self.root.title("随机目的地旅行生成器") self.root.geometry("680x520") self.gen = RandomDestination() self.msg_queue = queue.Queue() # 线程和主界面通信用的队列 self.current_item = None # 展示区域 self.text_area = scrolledtext.ScrolledText( root, wrap=tk.WORD, font=("Microsoft YaHei", 11) ) self.text_area.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 按钮区域 btn_frame = tk.Frame(root) btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) tk.Button(btn_frame, text="生成一个", command=self.start_fetch_one).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="批量生成10个", command=self.start_fetch_ten).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="复制", command=self.copy_current).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="清空", command=self.clear_text).pack(side=tk.RIGHT, padx=5) # 每 100ms 检查一次队列 self.root.after(100, self.process_queue) def start_fetch_one(self): self.text_area.insert(tk.END, "正在生成,请稍候...\n") threading.Thread(target=self._fetch_one_worker, daemon=True).start() def start_fetch_ten(self): self.text_area.insert(tk.END, "正在批量生成 10 个目的地,请稍候...\n") threading.Thread(target=self._fetch_ten_worker, daemon=True).start() def _fetch_one_worker(self): item = self.gen.fetch_one() self.msg_queue.put(("one", item)) def _fetch_ten_worker(self): items = self.gen.generate(10) self.msg_queue.put(("ten", items)) def process_queue(self): """从队列里取结果,刷新到界面""" try: while True: msg_type, payload = self.msg_queue.get_nowait() if msg_type == "one": self.current_item = payload if payload: self.text_area.delete(1.0, tk.END) self.text_area.insert(tk.END, self.gen.format_one(payload)) else: self.text_area.insert(tk.END, "生成失败,请检查网络或稍后重试。\n") elif msg_type == "ten": lines = [] for item in payload: lines.append(self.gen.format_one(item)) lines.append("-" * 40) self.text_area.delete(1.0, tk.END) self.text_area.insert(tk.END, "\n".join(lines)) except queue.Empty: pass self.root.after(100, self.process_queue) def copy_current(self): content = self.text_area.get(1.0, tk.END).strip() if content: self.root.clipboard_clear() self.root.clipboard_append(content) self.text_area.insert(tk.END, "\n内容已复制到剪贴板。\n") def clear_text(self): self.text_area.delete(1.0, tk.END) if __name__ == "__main__": root = tk.Tk() app = TravelGUI(root) root.mainloop()

这段代码的关键在三处。第一,每个按钮对应的业务逻辑都放到threading.Thread里执行,后台线程跑完把结果丢进queue.Queue;第二,主界面的process_queue方法每隔 100 毫秒检查一次队列,拿到结果再更新文本框,这样既不卡界面,又避免多线程直接操作 UI 组件的混乱;第三,daemon=True保证关闭窗口时后台线程自动退出,不会残留进程。

4.3 体验优化与后续扩展空间

这个 GUI 版本做出来后,已经能应付日常使用了。但我实际用下来还是有些小问题值得优化。比如批量生成 10 条时,如果中间某次请求超时,整个生成流程会卡在循环里,可能要好几十秒才回来。更好的办法是为每次请求设置独立超时,并且把失败次数统计出来,超过 3 次就直接结束并提示用户。

另一个体验点是“复制”功能。我原先只复制当前这一条,后来发现朋友更想要整页内容都复制走,于是干脆把text_area里的全部内容都塞进剪贴板。这个改动很小,但用起来顺手很多。

再往下扩展的话,可以考虑用loguru记录请求日志,或者把每次生成的目的地存到 SQLite 里,时间久了能统计出哪个城市最常出现。这些都是很好的练习方向。

5. 常见问题与排查技巧实录

5.1 抓包阶段:看不到小程序请求、数据全是乱码

抓包抓不到请求,绝大多数情况出在代理配置上。检查顺序是这样的:先确认 Charles 的代理端口有没有被占用,再确认系统代理有没有指向 127.0.0.1:8888,最后确认 Windows 防火墙是否放行了 Charles。如果你用的是手机抓包,还要额外确认手机和电脑在同一个局域网。

另一种情况是流量能看到,但内容是一堆十六进制或者根本点不开。这八成是 HTTPS 解密没生效。回到SSL Proxying Settings里,确认*:*规则存在,并且根证书已经安装到“受信任的根证书颁发机构”。我自己经历过一次最坑的情况:证书装了两遍,第一遍装到了“个人”证书区域,结果完全失效,后来重新装到“受信任的根证书颁发机构”才正常。

5.2 requests 请求阶段:403、超时、数据取不到

如果 Python 请求返回 403,优先怀疑 Headers。微信小程序接口对 Referer 的校验非常严格,必须带上servicewechat.com域名的 Referer。其次看 User-Agent,建议直接复制抓包数据里的 UA,不要自己拼一个。

还有一种情况是接口返回 200 了,但resp.json()报错,提示 JSON 解析失败。这时候先打印resp.text看看,很可能是返回了空串、HTML 页面或者错误提示。我在调试时遇到过接口在请求参数缺少某个字段时返回纯文本的情况,用resp.json()直接崩,后来改成先判断resp.text是否以{开头再做解析,问题就解决了。

超时问题也常见。小程序接口偶尔会慢,尤其晚上高峰期。timeout=10如果经常被触发,可以做一次重试机制,比如失败后睡 2 秒再试一次,但别无限重试。

5.3 GUI 阶段:窗口卡死、中文乱码

窗口卡死的原因我上面已经提过,就是请求阻塞了主线程。如果你看到“程序未响应”,别怀疑是 tkinter 不行,用threading + queue重新组织一下就好。这里提醒一个小坑:线程里不能直接改文本框内容,必须通过queue转发到主线程修改,否则偶尔会出现奇怪的崩溃。

中文乱码一般出现在 Windows 控制台或者部分文本控件里。解决方法是统一用 UTF-8 编码。在文件开头加# -*- coding: utf-8 -*-或者直接用 Python 3,一般不会再出问题。tkinter 本身对中文支持没问题,但字体最好是“Microsoft YaHei”,默认字体在中文符号上不太美观。

5.4 频率控制与合规提醒:别把小程序的接口薅秃了

最后这点我必须强调。这个项目用来学习没问题,真机测试时我连续请求 30 多次也没事,但千万不要去搞什么并发循环,一下子请求几百上千次。小程序的服务器通常有频控策略,轻则限流报错,重则封号和机器 IP。我见过有人为了做“全网最全目的地库”,拿脚本刷了一整晚,第二天微信账号直接被限制登录了。

合规方面也说一下。爬虫本身是中性的工具,但使用场景要克制。这个案例中我爬取的是公开的随机信息,不涉及隐私,也不涉及核心数据资产。如果你以后盯上别的目标,一定要先判断数据是否公开、是否允许抓取、是否会造成服务器负担。用于学习没问题,拿去商用或者恶意采集就得慎重考虑了。想在实际项目里稳定爬取微信小程序接口,还需要考虑合规授权和更完善的限速方案,这些不是基础篇能讲完的,但意识要从第一天就有。

我个人做完这个项目的最大感受是:爬虫的学习曲线其实没那么陡,真正难的是把“看请求、发请求、处理数据、落地展示”这条链路跑通。你只要完整地走一遍,后面的并发、分布式、验证码处理这些进阶内容,才谈得上有意义。下一步可以试着把这个脚本改造成命令行工具,加一个参数控制数量和输出格式,或者把批量生成的数据保存成 CSV 文件,练练文件操作。总之,先把基础链路吃透,后面随便加点东西都是新技能。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询