
简介一份基于Python实现的美团App爬虫工程代码面向爬虫开发入门者与数据采集工程师聚焦App端数据获取中的URL管理、请求构造、响应解析与存储落地等关键环节同时兼顾robots协议与常见的反爬应对策略。包体共23个文件核心为7个Python脚本覆盖初始化、签名算法、主流请求处理、店铺列表、店铺详情与评论抓取等模块另含6个JSON数据文件、5个XML配置信息以及JS脚本、说明文档和工程配置文件整体仅157KB结构紧凑、模块边界清晰便于快速定位与二次开发。目前已有1565人学习浏览适合希望从基础爬虫过渡到App数据采集场景的初中级Python开发者。通过阅读源码可以理解请求签名生成、User-Agent与请求头模拟、HTML/JSON解析及结果落盘等具体实现JSON与XML文件则展示了数据结构与工程配置方式可帮助读者快速套用到自己的采集任务中。1. 某团App爬虫Zip先弄懂它是脚手架还是成品脚本某团App爬虫Zip严格来说不是一份“解压就能跑”的成品脚本而是一套针对本地生活服务类App的采集脚手架。它把抓包定位、签名生成、验证码触发后的对策、以及门店数据落库的完整链路都收拾好了适合三类人要收集真实样本的在校学生、做竞品价格与评分观察的运营分析岗、以及刚接触接口逆向的Python爬虫新手。需要注意这份资源里没有“免维护”的黑匣子——接口字段会随版本变动请求频率会触发风控真正值钱的是里面沉淀下来的接口分析笔记和踩坑记录。下面按我自己的拆包顺序把它从压缩包一路讲到能稳定增量采集。2. 动手前先拆包接口结构、签名机制与资源目录布局2.1 抓包准备从App入口找到真实数据接口某团这类App界面上看到的数据几乎全部来自服务端接口App本身只是把JSON渲染成卡片。想拿到数据不能用浏览器F12那套思路因为数据不在网页端而在App的加密请求里。我一般的做法是准备一台备用的Android手机装好抓包工具把CA证书装进系统信任区然后滑动门店列表观察请求日志。这份Zip里的分析笔记记录的是某本地生活App某个版本的抓包结果里面筛选出了一批“有效域名”。判断请求是不是数据接口核心标准不是路径长短而是响应体内容出现poiId、shopName、avgPrice这类可读业务字段的就是数据源响应里只有数字ID和一堆埋点参数的是统计上报接口可以直接过滤掉。抓包时还要注意App请求体基本都是form-data格式除了业务参数外固定带上platform、appVersion、timestamp、sig这几个字段。其中sig是关键后面会专门讲。分析笔记里一般会把URL路径的关键片段摘出来方便你后续用关键词搜索抓包日志而不是每次都人工翻几百条记录。提示解压后先看分析笔记文件别急着跑主脚本。不同版本App的接口前缀和字段命名差异很大笔记里记录的字段可能已经和当前线上版本对不上。2.2 签名参数是怎么来的排序拼接、加盐与Zip里的处理思路某团的接口普遍带签名参数。以门店列表为例请求参数里有lat、lng、pageIdx、pageSize等最后加一个sig。签名的生成逻辑业内常见做法是将参数按键名排序值全部转字符串按kvkv拼接最后拼上固定盐值做MD5摘要。Zip里的签名模块封装的就是这个逻辑核心代码长这样import hashlib SECRET_KEY 从抓包请求样本中提取的盐值 # 不同版本App可能不同 def gen_sign(params: dict) - str: # 过滤空值参数避免签名结果不稳定 sorted_items sorted( (k, str(v)) for k, v in params.items() if v not in (None, ) ) raw .join(f{k}{v} for k, v in sorted_items) raw key SECRET_KEY return hashlib.md5(raw.encode(utf-8)).hexdigest()逻辑说明先过滤空值再按键名字典序排序拼成查询串后加盐最后做MD5摘要。参数说明SECRET_KEY就是盐值从同一份抓包样本里提取例子里的key字段名是常见写法具体以抓包结果为标准。这段代码不能直接复制到别的版本上用。新版本App可能把MD5换成了HMAC-SHA256或者加盐位置从尾部挪到中间。遇到请求返回“参数不合法”时我的排查顺序是先确认盐值是否变了再确认排序列是否多了新字段。签名算法没有可视化界面属于典型的“对着抓包结果反推”工作Zip给的是思路不是万能钥匙。2.3 目录拆解压缩包里那几个文件各自管什么事解压后通常能看到下面这样一组文件各自职责很清晰文件/目录作用main.py采集入口负责读配置、启动抓取、调落库sign.py签名模块封装gen_sign这类参数签名逻辑config.yaml请求间隔、并发数、超时时间、重试次数等运行参数interface_notes.md接口抓包笔记记录URL片段、关键字段、坐标系说明requirements.txt第三方依赖清单data/采集结果输出目录脚本默认落库到这里的SQLite文件这份资源里真正有价值的不是那几百行Python代码而是interface_notes.md。字段名和签名规则是花钱都未必买得到的实战记录代码本身反而简单——无非是requests发请求、解析JSON、写入数据库。动手前先建独立虚拟环境我习惯用venv而不是conda因为它更轻量。requirements.txt里列的依赖就那几个requests、pyyaml、pandas。装完之后先空跑一次main.py确认环境没问题再逐步调参数。3. 把最小可用的采集脚本跑起来从环境搭建到落库完整链路3.1 环境准备与依赖安装先用命令行把虚拟环境建起来然后安装依赖。步骤很简单但每次重装环境我都会卡在Python版本上所以建议用3.10以上的版本某些加密库在旧版本上编译容易出问题。python -m venv .venv source .venv/bin/activate # Windows下执行 .venv\Scripts\activate pip install -r requirements.txt逻辑说明第一行创建隔离环境第二行激活第三行安装依赖。参数说明requirements.txt里通常包含requests、pyyaml、pandas、cryptography这几类库如果网络源慢可以加-i参数换镜像源。装完之后验证一下签名模块是否能独立运行直接调用gen_sign传入一组抓包样本里的真实参数对比输出的sig和抓包抓到的sig是否一致。这一步能快速排除环境问题避免把时间浪费在“装好了但跑不通”的迷茫里。3.2 带签名的请求怎么发请求头、超时与失败重试核心请求逻辑不算复杂但请求头是重灾区。某团接口对User-Agent和Referer的校验比较严格缺一个或格式不对都可能直接返回风控提示。Zip里的main.py给了一个较稳的模板import time import requests from sign import gen_sign HEADERS { User-Agent: (Mozilla/5.0 (Linux; Android 12; PEEM00 Build/SP1A.210812.016) AppleWebKit/537.36), Referer: https://app.某团.com/, # 以抓包实际值为准 Content-Type: application/x-www-form-urlencoded, Accept-Encoding: utf-8, } def fetch_shop_list(params: dict) - dict: params[timestamp] int(time.time()) params[sig] gen_sign(params) for attempt in range(3): try: resp requests.post( https://接口域名/api/shop/list, dataparams, headersHEADERS, timeout5, ) result resp.json() if result.get(code) 0: return result except (requests.Timeout, requests.ConnectionError): time.sleep(2 * (attempt 1)) return {}逻辑说明先补timestamp再算签名然后带请求头发POST请求。超时5秒失败后按2秒、4秒、6秒退避重试。参数说明timeout设5秒是兼顾移动网络和WiFi两种场景太低容易误判失败太高会让整个采集任务陷入卡顿。这段代码里有两点值得留意一个是Referer必须和App内真实请求保持一致另一个是data里的参数顺序不影响签名因为gen_sign内部会重新排序。3.3 响应解析与字段清洗某团的响应体结构比其他App规整得多最外层是code和message业务数据在data里。data下面可能是一个列表也可能是带分页信息的大字典。解析时不要硬编码下标而是用字段名取值这样即使字段顺序变化代码还能少改几处。def parse_shop_list(payload: dict) - list: data payload.get(data, {}) raw_list data.get(poiList) or data.get(list) or [] parsed [] for item in raw_list: parsed.append({ shop_id: item.get(poiId), name: item.get(shopName), lat: item.get(latitude), lng: item.get(longitude), rating: item.get(avgScore), price_hint: item.get(avgPrice), raw: item, }) return parsed逻辑说明兼容两种常见的字段容器名poiList和list都能处理。解析后生成统一的字典结构raw保留原始数据用于排查。参数说明rating可能是字符串也可能是浮点数落库前统一做数值转换。清洗这一步容易被忽略尤其是金额和评分字段。某团返回的avgPrice有时是整数有时是“人均¥68”这种带前缀的字符串不处理直接入库后面做数据分析时会让人抓狂。3.4 落库SQLite建表与写入落库部分我直接用SQLite不做复杂设计。单表存门店快照信息每次采集都记录下来靠时间戳区分批次。建表逻辑如下CREATE TABLE IF NOT EXISTS shop_snapshot ( id INTEGER PRIMARY KEY AUTOINCREMENT, shop_id TEXT NOT NULL, name TEXT, lat REAL, lng REAL, rating REAL, price_hint TEXT, captured_at INTEGER, batch_id TEXT ); CREATE INDEX IF NOT EXISTS idx_shop_id ON shop_snapshot(shop_id);逻辑说明shop_id是业务主键captured_at是采集时间戳的毫秒值batch_id用来标识同一次任务。索引建在shop_id上后续按门店去重或增量比对都会用到。参数说明price_hint用TEXT而不是REAL就是为了兼容“人均¥68”这种原始串。写入时用executemany批量插入而不是逐条execute速度能快一个数量级。采集几百家门店的数据量很小SQLite完全扛得住。4. 让采集任务变成工程调度、去重与断点续采4.1 调度方式选择定时增量还是手动触发跑通第一轮之后接着考虑的不应该是“多并发”而是“怎么稳定地跑第二轮”。我见过太多人把采集脚本做成了一次性脚本跑完就扔再过一个月要用时发现字段全变了。调度上我倾向最简单的方式用操作系统的crontab或Windows任务计划程序每天固定时间跑一次而不是在脚本里做复杂的时间循环。原因很简单脚本崩溃时外部调度能帮你隔离问题而脚本内部死循环会拖垮整台机器。crontab写法示例# 每天凌晨2点跑一次增量采集 0 2 * * * cd /path/to/project .venv/bin/python main.py --incremental logs/collect.log 21逻辑说明crontab里先进入项目目录再用虚拟环境的Python执行入口脚本日志追加到collect.log。参数说明--incremental是main.py里的一个命令行开关控制走全量还是增量逻辑具体见4.2。4.2 数据去重按业务主键而不是URL去重爬虫去重最常见的误区是按URL去重但某团这类接口翻页URL几乎都是同一个只是pageIdx不同按URL去重根本拦不住重复数据。正确做法是用业务主键去重对门店数据来说就是shopId。def load_existing_ids(conn) - set: cursor conn.execute(SELECT DISTINCT shop_id FROM shop_snapshot) return {row[0] for row in cursor.fetchall()} existing_ids load_existing_ids(conn) new_items [item for item in parsed_list if item[shop_id] not in existing_ids]逻辑说明从快照表取出所有历史shopId构建集合。新解析结果只保留不在集合里的记录。参数说明set的查找复杂度是O(1)适合几千到几万规模的比对如果门店量到百万级内存集合会吃紧那时该换Bloom Filter但当前场景没必要。去重建议在内存里做而不是在SQL里写NOT IN子查询。SQLite的数据量不大时两种写法都能跑但内存集合更直观也方便后续做更新策略——比如门店评分变了你是保留旧快照还是更新最新值按业务需求选我通常保留历史快照方便回看价格波动。4.3 断点续采记录游标与失败任务清单增量采集最大的坑是“跑一半挂了”。网络抖动、接口临时限流、本地断电任何一个原因都能让任务中断。Zip里的main.py带了一个游标文件机制用JSON记录每个接口的采集进度import json from pathlib import Path PROGRESS_FILE Path(progress.json) def save_progress(page: int, task_name: str) - None: data {} if PROGRESS_FILE.exists(): data json.loads(PROGRESS_FILE.read_text(encodingutf-8)) data[task_name] {last_page: page, updated_at: time.time()} PROGRESS_FILE.write_text(json.dumps(data, ensure_asciiFalse, indent2), encodingutf-8) def load_progress(task_name: str) - int: if not PROGRESS_FILE.exists(): return 0 data json.loads(PROGRESS_FILE.read_text(encodingutf-8)) return data.get(task_name, {}).get(last_page, 0)逻辑说明每次翻页成功后更新游标下次启动时从last_page继续而不是从第1页重来。参数说明task_name用来区分不同数据类型的采集进度比如门店列表和评论列表各自独立记录。游标文件还有一个附加作用失败任务的清单也可以存在文件里。抓包频率高的时候某几页失败很正常不要为了补那几页而重跑全量把失败页码存下来等流量低谷期单独补就够。5. 避坑五个高频问题与排查手段5.1 滑块验证码频繁弹出行为画像被识别了现象脚本连续跑十几分钟后接口突然不再返回业务数据而是返回一段包含滑块验证的HTML或特殊JSON需要手动拖动滑块才能继续。原因这不是针对你签名的风控而是针对请求节奏的行为检测。同一台设备、同一个IP、每秒请求次数稳定得像节拍器一样风控系统一眼就看出是脚本。解决把固定间隔改成随机间隔并模拟真实用户的阅读时间。比如从main.py里把sleep(3)改成sleep(uniform(2.5, 5.5))同时给请求IP切出口。经验值是单IP下每分钟请求控制在20次以内每采集一页随机停顿比固定停顿有效得多。5.2 接口返回“数据异常”但HTTP状态码是200现象随机的几个请求返回的JSON里code是异常码message提示数据异常但status是200重试同样的参数又能拿到正常数据。原因服务端对单次请求做了抽样风控命中的请求即使签名正确也会被拒绝这是加权随机策略不是固定封禁。也可能是同一个IP在短时间内请求频率超过了某个软阈值。解决对这种“偶发失败”不要立刻加重试次数否则会放大频率问题。我的做法是记录失败的参数和返回体统计失败率。如果失败率低于5%正常推进重试一次就好如果超过20%立刻停脚本等30分钟再跑。5.3 门店坐标漂移坐标系转换问题现象脚本采集到的lat和lng放到地图上显示的位置和App里相差几百米城市级别的地图上看不出规律。原因某团App内部使用的坐标是火星坐标系GCJ-02直接扔到高德或百度地图上偏差相对可控但如果拿到WGS-84坐标系里做GPS轨迹比对偏差就非常明显。这不是接口返回错了而是坐标系定义不同。解决在数据清洗阶段做一次坐标系转换。GCJ-02转WGS-84的代码网上有很多轮子Zip的interface_notes.md里也记录了一个转换函数。核心思路是先算偏移量再做二次逼近校正精度到几十米以内就够做业务分析了。5.4 断点续采失效游标存了却没生效现象配置了progress.json重跑时日志显示仍然从第1页开始相当于游标白记了。原因main.py里初始化任务时调用了load_progress但翻页逻辑里调用save_progress的代码路径没走到或者进程异常退出时游标还没写入。最常见的是把save_progress写在了单页请求成功之后但忘记放在解析循环的外层。解决把save_progress的调用放在每页数据成功落库之后而不是收到响应之后。只有落库成功这一页才算真正处理完。另外启动时打印当前游标日志方便肉眼确认读到了进度。5.5 App升级后接口字段变化保持兼容的手段现象某天脚本突然大量返回空数据或者报KeyError检查抓包发现字段名从poiId变成了poi_id接口版本路径也变了。原因App发版时后端接口做了调整旧字段被替换或废弃。这是本地生活类App的常态平均一两个月就来一次。解决解析层封装一层字段映射不要在整个代码库里散用硬编码字段名。维护一个FIELD_MAP字典采集入口调用统一的get_field(item, shop_id)函数来取值。接口变动时只改映射表而不是满文件搜索替换。另一个手段是脚本每次启动时先请求一个已知门店详情的接口做“冒烟验证”确认字段结构没变再开始全量采集。6. 进阶把采集结果变成持续可监控的数据资产6.1 落库后建模从快照表里挖出业务变化采集本身只是第一步数据资产的价值在“第二层”。我在这个项目上挑了三个维度做观察同一门店在不同批次的评分变化、价格区间调整频率、以及新门店出现在列表里的时间点。SQL写起来不复杂一条按门店分组查时间序列就能看出趋势SELECT shop_id, name, captured_at, rating, price_hint FROM shop_snapshot WHERE shop_id 123456 ORDER BY captured_at;在SQLite里保留历史快照的另一个好处是门店下架或改名时你能从数据里追溯变更过程而不是只看到一张“当前状态表”。6.2 采集失败率的小看板我给这套脚本加了一个轻量的监控表格每次跑完把结果写入本地CSV包含采集时间、成功条数、失败条数、平均耗时四个字段。每周看一眼失败率趋势比等接口彻底封死再处理强得多。date,total,success,failed,avg_seconds 2025-01-01,1200,1188,12,86.4 2025-01-02,1188,1180,8,79.2逻辑说明CSV里失败率的异常上升往往比接口报错更早暴露风控收紧。参数说明avg_seconds超过120秒时基本可以判断触发了限流需要降速运行。这套资源我从拆包到稳定增量采集前后调了两周。最难处理的不是签名而是摸清风控的节奏感——它没有一个固定的规则可以照抄只能通过失败记录去逼近边界。从那以后我每次跑新采集任务都会强制走一遍“小批量测试→失败率统计→随机间隔调整”这个流程签名逻辑反倒成了最不用担心的部分。希望这份整理能帮你少走点弯路。本文还有配套的精品资源点击获取