饿了么商家爬虫实战:接口分析、反爬破解与工具打包
简介本资源是一款面向数据分析初学者与市场研究从业者的饿了么平台商家信息自动化采集工具聚焦解决外卖行业竞品分析、消费行为研究及本地生活服务数据获取等实际问题。压缩包共19个文件含5个核心Python爬虫脚本如eleme_spider.py、main-spider.py、2个编译缓存文件、10张界面与流程示意图png、1份PDF使用指南及1份Markdown说明文档整体3.93MB结构清晰便于快速部署与调试。资源已获37人学习下载适合希望掌握真实电商平台反爬应对策略如Cookie分割、动态请求模拟、HTML解析与结构化数据存储JSON/CSV的实践者。用户可直接运行主爬虫程序获取商家名称、地址、菜品详情、价格、评分及评论等完整字段并通过txt_to_excel.py完成格式转换配套图文指南与代码注释降低了学习门槛是理解Web爬虫工程化落地的典型小项目范例。 前阵子我搭了个小工具用爬虫批量抓取饿了么的商家信息最后打包成zip发给朋友用。这玩意儿做完之后我发现外卖平台爬到能用是一回事爬到不出事、不封号、不漏数据完全又是另一回事。今天把整个项目的思路、细节和踩过的坑都整理一遍给想做类似工具的朋友一个参考。这个工具解决的痛点很直接比如你想做某个区域的市场调研、看看周边商圈有哪些外卖商家、或者给商品信息做个对照表手动一家家搜效率实在太低了。爬虫的价值就在于把这种重复性操作变成一个自动化流程一次跑完几百家店铺的店名、评分、月售、起送价、配送费、人均、品类、地址这些字段导出成表格后面怎么用就看你自己了。适合谁来读这套东西呢主要是刚入门或者卡在半路上的Python爬虫学习者还有一些需要做本地生活数据采集的产品、运营、调研类岗位的朋友。你不需要懂很深的前端知识但得会用requests、会看一些基本的JSON结构剩下的咱们一步步来。1. 项目整体设计与思路拆解1.1 为什么直接抓页面很难必须走后端接口很多人第一次做爬虫下意识会去爬网页HTML打开饿了么的页面右键查看源代码然后打算从里面匹配数据。这个思路放在十年前还行现在基本属于踩坑第一步。饿了么的页面是前端动态渲染的店铺列表、评分、销量这些数据全部通过JavaScript异步加载页面源代码里只有一堆框架标签你要的数据根本不在里面。就算你会用Selenium模拟浏览器效率和稳定性也是大问题——浏览器要不断启动、页面要等加载、反爬检测还盯着你看。所以这个项目从设计一开始就定了基调直接拿后端接口。页面展示的数据一定来自某个API找到这个接口、模拟它的请求参数、解析JSON返回这才是爬虫项目能跑得稳的核心。1.2 技术方案选型requests还是Scrapy选型这件事我一开始纠结过。Scrapy是专业级的爬虫框架支持异步、中间件、分布式听起来高大上但对付饿了么这种需要反复调参、调试签名、验证Cookie的场景反而显得笨重。我的选择是requests配合pandas原因有三个。第一饿了么的接口逻辑相对固定不存在海量URL需要调度一个循环就能搞定所有分页请求用不上Scrapy的调度器。第二调试接口时requests的代码路径特别清晰哪里出了问题一看便知Scrapy的中间件链路反而容易让人绕晕。第三数据最终要存成表格requests拿到JSON以后直接用pandas转DataFrame一条代码的事情Scrapy还得再配pipelines和item处理器。如果你以后要爬的数据源是那种成千上万级页面、需要持久化存储和分布式采集的再考虑升级到Scrapy不迟。现阶段这个小工具用requests是最合适的。实际上我试过用Scrapy写了一个版本后来发现改请求头、调参数、测试封禁策略的时候来回改代码的效率实在太低了果断换回requests。工具没有高下之分只有合不合适。1.3 数据字段设计采集什么才是最核心的爬虫项目最容易犯的一个错是“什么都想爬”最后爬下来一堆没用的字段处理起来还费劲。这个项目在设计字段的时候做了明确取舍。商家本身的信息是最值钱的店铺名称、评分、月售、人均价格、起送价、配送费。这些字段直接决定用户看到的是不是一个“能用的数据”。平台整体的排序位置也值得记录因为同一个搜索词下排名靠前的店铺和排名靠后的店铺商业价值差异极大。地理位置这块我也加了经纬度和geohash字段这样后期可以做商圈聚合分析知道哪个位置附近商户密度高。不过注意这些字段在接口返回里面是加密过的后面会讲到怎么处理。我不建议采集用户评价这种个人信息相关的数据一方面数据量大另一方面涉及隐私合规问题做工具的朋友要自己把握好边界。2. 核心细节接口分析与请求构造2.1 纯文本抓包的核心思路这个项目的起点是抓包分析。你打开饿了么的网页版或者小程序搜索一个关键词打开浏览器的开发者工具切到Network面板刷新页面你会看到一堆请求。关键是在这里面找到那个返回店铺列表数据的接口。如何快速定位呢直接在Network面板里搜索“店铺名”或者其他特征字段比如你搜“麦当劳”在响应内容里能搜到这个字符串的就是数据接口。再把响应内容格式化一看如果结构清晰、层级分明说明这个接口没做混淆直接用就行。饿了么的接口返回数据是比较标准的JSON结构里面有个name字段、score字段、month_sales字段等等。这种接口相比抖音、快手那种全字段混淆的已经算很友好的了。2.2 请求头构造Referer和User-Agent的坑找到接口之后直接拿Python的requests去请求大概率会得到一个错误页或者空白返回。原因在于饿了么对请求头做了严格校验。其中最重要的两个参数是Referer和User-Agent。Referer必须是指定来源不能为空否则服务端判定为异常请求。User-Agent也不能用Python默认的标识要伪装成真实浏览器的标识。另外还有一个容易被忽略的就是Cookie。浏览器里访问过的页面会有会话信息比如定位城市、登录态等。第一版工具的时候我只复制了一个Cookie字符串结果能爬到数据但是被限流的概率很高。后来发现需要额外加上一个标识用户身份的参数这个参数在后面的请求中可以保持不变但是Cookie本身最好每次运行前更新一次。请求头构造是不是越全越好呢也不一定。我之前把浏览器里的所有Header字段全部复制进代码反而触发了风控因为正常浏览器每次请求的Header顺序都不一样你一概而论反而显得机械。请求头只要包含关键字段即可切忌画蛇添足。2.3 签名参数与加密逻辑的破解思路饿了么的接口有个比较核心的反爬机制就是很多参数是动态加密生成的。我在分析的时候发现如果你只构造常规参数接口可以正常返回但当你请求频率稍微高一点服务器就会拒绝响应。后来我对比了生成的URL和浏览器中的URL发现差别在于多了几个加密相关的参数。其中某些参数是根据固定算法生成的比如当前时间戳和版本号拼接后再做MD5处理。这类参数好在算法固定就是时间戳加上一个盐值再走一遍哈希运算。具体写法是先在抓包里找到这几个参数名称然后在源码里全局搜索初始化代码找到生成这些参数的逻辑。多数情况下它们是JavaScript代码里的一段方法。你需要把这个逻辑翻译成Python替换原来的参数拼接。我在这部分花的时间其实不长因为参考了网上不少同行的经验。说实话逆向这部分在整个项目难度里只能算中等。真正的反爬大头在字体反爬和请求频率控制上面后面详细说。2.4 Cookie过期与身份状态管理爬虫工具最大的痛点不是写不出来而是跑两天就不好使了。Cookie过期是我在实际使用中遇到最频繁的问题。饿了么的Cookie有效期通常在几小时到一天之间。如果你直接把Cookie写死在代码里工具的寿命就是以你手动复制那次为起点到Cookie失效为止。解决办法有两个。第一个是半自动化方案每次运行前手动从浏览器复制新的Cookie粘贴到配置文件里适合使用频率不高的场景。第二个是模拟登录用账号密码去请求登录接口拿到新的Cookie自动写入适合需要长期跑的脚本。第二个方案实现复杂度高不少但如果工具是给别人用的就必须做这一步。我在最终的zip版本里用了第一种方案因为大部分使用场景是“今天跑一次出个报告”而不是7乘24小时挂在服务器上。任何工具都要匹配实际使用的场景不要为了技术而技术。3. 实操过程与核心环节实现3.1 环境准备与依赖清单项目基于Python 3.8用到的核心库就三个requests负责网络请求pandas负责数据存储json负责解析返回数据。如果你还需要反爬相关的字体处理再额外加一个fontTools库但基础功能是不需要它的。创建虚拟环境、安装依赖这几步就不赘述了。需要注意的版本坑是requests不要追新2.25之前的版本在有些系统上对HTTPS的适配反而更好。当然这只是我个人的经验如果你是新环境直接用最新版问题也不大。3.2 搜索请求核心代码实现下面是整个工具中最核心的一段代码功能是根据城市和搜索关键词获取饿了么商家的列表数据。import requests import time import hashlib import json def get_shop_list(city_code, keyword, page1, cookie_str): # 构造基础URL url https://www.ele.me/restapi/v4/member_cart/mget_delivery_carts # 生成签名参数 timestamp str(int(time.time())) sign_content fcity_code{city_code}keyword{keyword}page{page}timestamp{timestamp} sign hashlib.md5(sign_content.encode()).hexdigest() headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.ele.me/shop/, Cookie: cookie_str, Accept: application/json, text/plain, */* } params { city_code: city_code, keyword: keyword, page: page, limit: 20, sign: sign, timestamp: timestamp } response requests.get(url, headersheaders, paramsparams, timeout10) return response.json()这段代码是经过多次调试后稳定运行的版本。注意我生成的签名参数,不同时间段的算法可能有变化,如果你拿到代码发现接口返回需要额外参数,请通过抓包比对URL参数差异,定位新增的参数生成逻辑。3.3 数据解析与清洗流程接口返回的JSON结构比较嵌套,店铺主体数据在data下面,每家店铺是一个字典,包含restaurant_info、restaurant等不同层级的字段。第一次解析的时候容易懵,建议用在线JSON格式化工具先看一遍结构,再写代码提取。我习惯先定义一个数据清洗函数,把嵌套字段拉平输出成行数据。下面这段函数演示了如何从接口返回中提取有效字段:import pandas as pd def parse_shop_data(raw_json): 从原始JSON中提取店铺核心字段 rows [] shop_list raw_json.get(data, {}).get(carts, []) for shop in shop_list: info shop.get(restaurant, {}) row { name: info.get(name), score: info.get(rating, 0), month_sales: info.get(month_sales, 0), per_capita: info.get(flavor_score, 0), delivery_fee: shop.get(delivery_fee, 0), 起送价: shop.get(min_order_price, 0), geohash: info.get(geohash, ) } rows.append(row) return rows def save_to_csv(rows, filenameeleme_shops.csv): df pd.DataFrame(rows) df.to_csv(filename, indexFalse, encodingutf-8-sig)这里有一个重要细节:导出的CSV如果直接用utf-8编码,用Excel打开时会乱码。必须用utf-8-sig,这个格式会加上BOM头,Excel才能正确识别中文。还有一个细节是字段值有些是None,有些是空字符串,清洗时统一处理成0或者空字符串,避免后续分析时报错。3.4 分页与全量抓取逻辑单页能拿到的店铺数量有限,要抓全一个关键词下的所有商家,必须处理分页逻辑。接口的分页参数通常有两个:offset和limit。limit是每页数量,一般最大到20到30。offset是偏移量,翻页的时候按limit累加即可。但我在这里遇到了一个隐藏较深的坑——单纯按偏移量翻页,爬到第二页之后就出现了大量重复店铺。排查后发现是因为接口内部有“智能排序”的逻辑,翻页时前几家的排序权重会发生变化,导致数据错位。解决办法是改用接口返回的rankInfo字段作为下一次请求的游标,而不是自己计算偏移量。不同接口的游标机制不一样,有的用页面最后一家店铺的ID,有的用排序值,你需要看返回数据里有没有类似标记。如果接口不支持游标,还有一个妥协方案:翻页时按关键词拆分,比如搜索“烤肉”分“烤肉饭”“烤肉店”“烤肉外卖”等多个子关键词分别采集,最后去重。这种思路虽然土,但在应对搜索推荐算法的时候特别管用。def crawl_all_shops(city_code, keyword, cookie_str, max_pages10): all_rows [] offset 0 limit 20 while offset max_pages * limit: raw get_shop_list(city_code, keyword, pageoffset // limit 1, cookie_strcookie_str) rows parse_shop_data(raw) if not rows: break all_rows.extend(rows) # 关键游标更新 offset limit time.sleep(2) # 控制请求频率 return all_rowsmax_pages参数限制最大翻页深度,防止关键词搜索有几千页时程序无脑跑下去。time.sleep(2)控制每次请求间隔,既是道德要求,也是保命要求。3.5 打包成可执行文件的细节项目跑通之后,我把脚本用PyInstaller打包成了可执行文件,方便不需要Python环境的用户直接运行。打包命令很简单:pyinstaller -F -w eleme_crawler.py-F表示打包成单文件,-w表示不显示控制台窗口。但是有个坑:打包后的程序如果设计成需要在命令行传入参数,-w参数会让你看不到任何输出,用户会误以为程序没启动。所以我最终的方案是做了个简单的命令行交互,程序启动后提示用户输入城市、关键词和执行模式,而不是硬编码参数。这样打包的时候不加-w参数,让用户看到提示信息,体验会好很多。打包后生成的dist目录下的exe文件,连同config.ini、README.txt一起打包进zip,就成了你看到的那个“饿了么商家信息爬取工具.zip”了。4. 常见问题与反爬避坑实录4.1 字体反爬评分和销量变成了神秘符号这是爬饿了么时最大的拦路虎,也是网上提问最多的问题。现象是用代码爬回来的数据里,评分、销量这些数字字段全变成了#xe602;之类的自定义字符编码,肉眼根本看不懂。原理是前端页面把数字渲染成了自定义字体,字体文件里数字对应的字形被重新映射了。爬虫拿到的是字符编码,但浏览器用特定字体渲染后才显示正确数字。解决方案有两种。第一种是下载页面字体文件,解析字体映射关系,把自定义字符还原成标准数字。这个方案需要用到fontTools库,有一定代码量。第二种更简单,寻找接口返回的明文数字字段,很多接口数据其实在原始JSON里是有明文字段的,只是前端展示时用了字体混淆,你需要确认自己爬到的到底是哪个字段。我在实际项目中发现,饿了么网页版的接口返回的字数大多已经是明文数字,字体反爬主要防的是直接解析HTML的场景。所以如果你用的也是接口方案,遇到乱码的概率不大。但如果你要扩展去爬美团,那就要做好字体反爬的心理准备了。4.2 频繁请求被封IP怎么办写爬虫就绕不开封IP这个话题。我实测下来,饿了么对单IP的请求频率限制比较敏感,如果每秒超过两次请求,很快就会出现验证码或者请求失败。应对方案按优先级排序:控制请求频率,每次请求间隔1到3秒,是成本最低、效果最好的方案使用代理IP池,通过requests的proxies参数动态切换出口IP设置自动化重试机制,遇到429或403状态码时,等待一段时间后重试proxies { http: http://127.0.0.1:7890, https: http://127.0.0.1:7890 } response requests.get(url, headersheaders, paramsparams, proxiesproxies, timeout10)自制代理池的方案是把端口、IP维护在一个文本文件里,用的时候随机抽取。但从性价比角度说,我建议平时自用的话,只做频率控制就足够了,别过度设计。4.3 数据字段为空的排查方法跑出来的数据里,偶尔会出现某些店铺的评分、人均价格全部为零或者为空的情况。这不是代码写错了,而是商家没有完善自己的资料。有些小店确实没有评分数据,有些品牌店的人均价格字段在接口返回里就是空的。遇到这种情况不用纠结,保持空值导出就行。我在清洗函数里加了条件判断,如果字段值为None或空字符串,统一写入0。这样后期做分析的时候数据类型不会出错。但有一种情况要警惕:如果某个字段全部为空,而不是个别为空,那就说明请求参数或者解析路径错了。之前有过一次,所有店铺的配送费都为空,排查后发现接口把字段名从delivery_fee改成了deliver_fee,改动一个单词,全量数据就都取不到了。遇到这种问题,去重新翻一遍接口返回的原始JSON,对比字段名,比对着代码猜要快得多。4.4 Cookie失效的紧急处理工具在运行到一半的时候跳出来“session过期”的错误提示,是最让人恼火的。已经跑了三个小时的数据,说断就断了。我的处理方案是在主循环里做异常检测,遇到登录失效的错误码之后,立刻停止爬取,把当前进度保存到本地文件,然后弹窗提示用户手动更新Cookie。下次运行的时候,先读取本地进度文件,跳过已经抓取的数据继续跑。try: raw get_shop_list(city_code, keyword, pagepage, cookie_strcookie_str) except requests.exceptions.HTTPError as e: if 401 in str(e): save_progress(progress_file, page) print(Cookie已失效请更新后继续运行) break这种做法比每次都从头跑一遍要友好得多,也是用了几次之后被逼出来的优化。4.5 如何应对接口字段更新的问题接口字段的变更,其实是外卖平台爬虫最大的不稳定因素。我维护这个工具的半年里,接口至少经历了三次大的改版——字段名迁移、返回结构嵌套调整、新增必填参数。应对这个问题的关键不是写更复杂的解析代码,而是保持一份“接口变更日志”。每发现代码失效,先抓包看接口返回的JSON结构长什么样,记录下来新旧字段的对应关系,再改代码。另外,写解析函数的时候尽量增加容错性,比如用dict.get(field, default_value)而不是直接dict[field],这样即使字段缺失,程序也只会返回默认值,而不是直接崩溃。# 不推荐的做法 name shop[restaurant][name] # 推荐的做法 name shop.get(restaurant, {}).get(name, )两行代码的差别,决定了工具在接口微调时能不能扛过去。5. 工具选型与项目边界思考5.1 为什么选择requests而不是httpx最近很多人推荐httpx,说它支持HTTP/2和异步。我在新项目里确实会用它,但在这个工具里坚持用requests,核心原因是稳定大于性能。饿了么接口目前还是HTTP/1.1协议,requests完全够用。异步并发确实能提高效率,但外卖平台对并发请求的封禁力度非常大,拿到高并发的能力反而不容易用。再者requests经过了十年的社区验证,各种代理、重试、会话管理方案都很成熟,网上遇到问题一搜就有答案,对新手友好得多。如果你以后想做的项目是抓取那种几千页、完全不反爬的静态网站,再考虑用httpx做异步并发也不迟。5.2 爬虫工具的边界与合规建议讲完技术,必须提醒一句合规问题。爬虫本身是中性工具,但使用要注意边界。我个人从实际操作中总结出来的几条参考:工具仅用于学习、调研和个人合法用途,不应该批量抓取带有用户隐私属性的数据并用于牟利。采集公开的商家名称、评分、营业时间等基础信息,获取的难度和数量要控制在合理范围内。平台有用户协议和Robots协议,虽然协议本身不具有强制力,但作为从业者最好自律。如果只是做一次性的市场调研、竞品对比,问题基本不大。要是你拿这个工具每天全量抓取平台的商家数据然后对外售卖,风险就得自己掂量了。如果你做的工具要给别人用,建议在代码里加上频率限制、数据量限制、免责声明,这既是保护自己,也是在帮使用者规避风险。5.3 后续扩展思路这个工具做完以后,我发现它还能扩展出挺多玩法的。第一个是定时巡检。搭配系统计划任务,每天定时抓取指定商圈的数据,跟踪商家上架下架、改价、活动变化,做成一个轻量版的商家监控系统。第二个是商圈热度分析。抓多个地标周边几百米内的商户数据,按品类、评分、销量做聚合,就能粗略看出不同商圈的商业结构差异。对于选址、开店调研来说,这套数据比拍脑袋靠谱得多。第三个是商品维度扩展。从商家到商品,抓取商家的菜单和商品价格,更新频率可以更低,一周跑一次就行,用来做外卖行业的品类价格观察。顺着这些方向,一个小工具会慢慢长成一个完整的数据采集、处理、分析闭环,能做出来的东西就远不止一个zip那么简单了。6. 常见问题速查表问题症状排查思路请求时被拒绝返回403或验证码页面检查User-Agent和Referer是否完整,降低请求频率数据全是乱码字符评分、销量显示为特殊符号确认使用的是接口返回字段还是HTML页面解析,如为HTML需处理字体映射Cookie失效中途出现未登录提示手动复制最新Cookie,或实现自动登录逻辑翻页重复数据第二页和第一页数据大量重合改用rankInfo游标而非简单offset翻页部分字段为空个别店铺配送费等字段为零确认字段名是否改动,检查原始JSON结构打包后无法运行双击exe没反应使用命令行运行查看报错,检查打包参数是否误加了-w编码乱码CSV用Excel打开中文乱码保存时使用utf-8-sig编码做这个项目最大的感受是,爬虫的工作量其实不在“爬”这一步,而是在“稳定”这两个字上。代码写出来只是开始,怎么让它在各种反爬策略下还能稳定运行,才是真正考验工程能力的地方。以后如果你也遇到类似的需求,不妨控制好范围、理清接口、注意频率,一步步来。还有一个经验想分享:工具不能只停留在能跑的阶段,一定要让自己用起来顺手。我最初那个版本,每次运行都要改代码里的城市名和关键词,后来改成了命令行交互,体验天差地别。给工具做一点使用体验上的优化,不只是给用户看的,更是给三个月后的自己看的。本文还有配套的精品资源点击获取
