简介一份面向计算机专业课程设计与毕业设计场景的闲鱼商品爬虫及可视化项目基于FastAPI与Vue实现xianyu二手平台数据抓取、接口服务与前端展示覆盖数据采集、后端服务、前端可视化等环节适合教师、学生用于课程设计、期末大作业或Python爬虫实战演练。压缩包共28个文件包含11个py脚本负责爬虫与后端接口4个json文件保存配置与示例数据3个md说明文档2个js及1个vue、1个html组成前端展示另有1个exe、图标与图片等辅助文件整体约9.33MB目录结构清晰便于按模块查找。已有222人学习下载。项目中附有依赖清单、前端构建配置和说明文档能够帮助读者快速还原环境并理解从数据采集、解析入库到API交互、可视化呈现的完整链路此版本经严格测试稳定运行适合作为毕业设计、课程设计或大作业的改造蓝本与实操参考。1. 闲鱼商品爬虫是什么从一份zip包到可用的数据管道闲鱼商品爬虫本质上就是绕过闲鱼前端页面的手动浏览用程序化的方式把商品标题、价格、成色、卖家信息、发布时间、地理位置这些字段批量抓取下来再落进数据库做二次加工。xianyu平台数据抓取这个需求在二手电商场景里远比想象中高频——盯竞品价格、做闲鱼关键词监控、无货源选品看趋势甚至判断某个品类最近是不是爆了都依赖这套数据。这份“最新开发.zip”的标题里带上了平台名和抓取方向说明它不是零散脚本而是把登录态、请求构造、数据解析和存储串起来的一整套采集方案解压后通常是一个带依赖文件的Python工程。适合谁如果你正在做电商数据分析、想监控某个关键词下的商品波动或者打算批量比对二手行情这套东西能省下大量手动刷新页面的时间。但先说句实在话闲鱼的反爬强度和淘宝系一脉相承签名、风控、验证码一样不少这不是装个requests库就能挂机跑的项目。后面每一章我都按自己实际踩过的路子来讲方案怎么选、参数怎么调、哪些坑不值得再踩一遍。2. 闲鱼数据抓取的技术方案选型接口直连、浏览器模拟还是抓包逆向2.1 闲鱼的数据特征与抓取难点闲鱼是典型的C2C二手交易平台商品生命周期极短同一个关键词下的列表几个小时就会变一轮。这决定了爬虫必须走实时请求不能像抓静态网站那样一次性拉全量。数据本身是动态加载的页面首屏只是框架商品列表靠异步接口返回JSON所以直取HTML的方案基本不可行。难点集中在三块。第一闲鱼没有公开的开放API所有数据接口都挂在淘宝系的mtop网关后面请求头里要带appKey、sign、t等参数sign是动态生成的签名直接构造请求会被网关返回“非法请求”。第二搜索接口依赖登录态未登录状态下能拿到的结果不全翻页深度有限而且部分字段会脱敏。第三平台对频率的容忍度很低单IP高频请求很快触发滑块验证严重时直接封设备指纹。2.2 三种常见抓取方案的对比与现实选择以我自己的经验做闲鱼数据抓取不外乎三条路你可以按时效和成本来选第一版实现。方案原理优点缺点适合场景接口直连逆向搜索接口构造签名和请求头请求开销小、速度快、带宽占用低签名容易失效需要逆向维护长期稳定跑、数据量大浏览器模拟Selenium/Playwright驱动浏览器绕过大部分JS签名开发快耗内存、速度慢、容易被检测自动化特征短期采集、小批量验证抓包中间人代理mitmproxy/Charles拦截APP请求转发入库拿到的字段最全包含客户端特有参数必须维护一个在线代理且依赖手机或模拟器数据字段复杂、需要全量字段接口直连是zip包里最常见的做法也是唯一能支撑“关键词监控”这种高频任务的方案。但直连的前提是解决签名。常见做法是先用抓包工具比如mitmproxy从闲鱼APP或网页端拦一次真实搜索请求把完整URL、请求头、请求体拷出来分析找到sign参数的生成位置。如果zip包里已经有逆向好的签名模块那直接调用就行如果没有就得从APP的JavaScript或so库里去定位加密逻辑这部分工作量最大但一次搞定后能用很久。2.3 请求签名与登录态闲鱼反爬的底层逻辑闲鱼的搜索接口走的是mtop.com.taobao.idle.widget.internal.fullbreadcrumb和mtop.taobao.idle.search.widget.search等链路核心参数包括data搜索条件JSON包含keyword、page、pageSize、sortType等sign由请求参数和固定密钥经过HmacSHA256或MD5变体生成t毫秒时间戳appKey客户端标识x-sys、x-features客户端环境特征签名一旦失效最典型的现象是接口返回RET_SYS_USER_ERROR或者直接FAIL_SYS_TOKEN_EMPTY。这时候不要急着改代码先确认几个点手机或网页端当前能否正常搜索、抓包拿到的签名是否过期、本地时钟是否偏差过大。签名对时间戳很敏感本地时间和服务器时间差超过5分钟基本必挂所以运行爬虫的机器必须开启时间同步。登录态同样关键。闲鱼APP端和网页端的登录体系不一样网页端扫码登录后拿到cookieAPP端则是token。zip包里如果用的是Playwright一般走扫码登录更稳因为cookie可以持久化复用如果用的是纯requests/httpx你得自己管理cookie和token的刷新逻辑。提示一开始就同时维护APP端和网页端两套登录态是一种常见的翻车姿势。选一个你抓包最方便的设备作为主入口把另一套的代码暂时注释掉减少无关变量。3. 跑通闲鱼爬虫zip包解压、依赖安装与登录态管理3.1 解压zip包与伪加密问题标题里的zip是发布者常用的打包格式但解压这件事本身就有坑。闲鱼爬虫这类在圈内流转的zip包经常被人加上伪加密标识来防白嫖——文件头部的加密标志位置1但实际数据没有加密。你用普通解压软件打开会提示输入密码体验上就是“这包是不是坏的”。先试试直接解压unzip xianyu-crawler-latest.zip -d xianyu-crawler如果报错encrypted或要求输密码别急着放弃确认是不是伪加密zipinfo -v xianyu-crawler-latest.zip | grep -i encryption看到encryption: none但解压时仍要密码就是伪加密。处理办法是用Python的zipfile库把加密标志清除后重打包import zipfile # 读取原始zip文件信息伪造解压时不校验密码 with zipfile.ZipFile(xianyu-crawler-latest.zip, r) as zin: with zipfile.ZipFile(xianyu-crawler-fixed.zip, w) as zout: for item in zin.infolist(): # 伪加密的本质位标志第0位置1但数据未加密 # 直接篡改标志位绕过解压检查 item.flag_bits ~0x1 zout.writestr(item, zin.read(item.filename))这段代码逐个读取zip包内的文件条目把通用位标志里的加密位第0位去掉再原样写入新zip。虽然原文件数据本身没加密但zipfile初始化时因为标志位会传入一个空密码占位这样才能正常读取。逻辑说明flag_bits是zip文件对每个文件单独记录的属性0x1代表“已加密”置0后解压工具会把它当普通文件处理。如果zip包里文件本身就做了真加密那只能找发布者要密码。圈内默认规矩是查看压缩包注释、文件名里的联系信息或作者在简介留的提示不要费劲去爆破AES-256的zip密码爆破在消费级硬件上不现实。3.2 Python环境与依赖安装不要直接pip install -r requirements.txt解压完成后第一件事不是跑而是建独立的虚拟环境。闲鱼爬虫依赖的库通常包括requests、httpx、pycryptodome、sqlalchemy、playwright这些库单装任何一个都不难但版本组合很讲究。比如pycryptodome和cryptography之间有时会冲突playwright又会拖一个浏览器内核装错版本很可能让签名模块直接没法import。cd xianyu-crawler python3 -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple playwright install chromium虚拟环境把依赖隔离在项目目录里不污染系统Python。playwright install chromium是浏览器模拟方案必须的一步很多人漏了它结果跑到一半报Executable doesnt exist。如果你确定走接口直连不需要这行可以跳过省掉300MB的浏览器下载。注意如果requirements.txt里锁了很老的版本号比如requests2.18不要盲目升级。签名模块经常依赖旧版的加密库行为随意升版本反而会破坏已有的加密输出。3.3 登录态获取从扫码到cookie持久化的完整步骤登录是闲鱼爬虫最关键的环节。常见做法是用Playwright打开浏览器人工扫码完成登录然后把cookie保存到本地文件之后所有接口请求都带上这份cookie。import json from playwright.sync_api import sync_playwright def get_login_cookie(save_pathcookies.json): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) # 必须显示浏览器窗口扫码需要 context browser.new_context( user_agentMozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1 ) page context.new_page() page.goto(https://www.goofish.com/, timeout60000) input(请在弹出的浏览器中扫码登录登录完成后按回车键继续...) cookies context.cookies() with open(save_path, w, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse) browser.close()这段代码里的UA用了iPhone Safari的字符串因为闲鱼网页版对桌面UA可能强制跳转APP下载页而移动端UA能正常渲染商品列表。扫码之后的cookie里通常包含unb、cookie2、_tb_token_这几个关键字段后续请求直接复用。cookie的保质期视账号活跃度而定低频使用一般能撑一周高频请求可能几小时就失效。建议每次启动任务都先验证cookie有效性写一个轻量的探测请求返回200就继续返回302或登录跳转就重新扫码。把登录脚本单独拆出来作为一个模块不要在抓取主流程里内联否则后续改登录逻辑会牵扯整个项目。3.4 首次运行前的配置检查清单在跑主脚本前有几项配置一定要人工确认不然会白白浪费很多次请求机会。第一本机时间必须开启自动同步。第二确认当前网络出口IP没有被闲鱼风控过——如果这个IP之前注册过大量小号或频繁登录退出初始风控等级就很高。第三检查config文件里的关键词列表是否为UTF-8编码Windows下用记事本另存的GBK编码会让关键词参数直接乱码。4. 闲鱼商品搜索接口抓取与SQLAlchemy数据落库4.1 构造搜索请求从抓包数据到可运行代码拿到登录cookie后下一步是把搜索接口从抓包数据变成真实请求。这里以网页版搜索接口为例抓包后你会看到类似https://www.goofish.com/...的异步请求返回的JSON里包含商品列表。核心代码结构如下import httpx import time import hashlib import json def make_sign(params: dict, secret: str) - str: 模拟闲鱼搜索接口签名逻辑 实际secret和拼接顺序以抓包结果为准这里只给出框架 keys sorted(params.keys()) raw .join(f{k}{params[k]} for k in keys) raw secret return hashlib.md5(raw.encode(utf-8)).hexdigest() def search_products(keyword: str, page: int 1, cookie: str , secret: str ): params { keyword: keyword, page: page, pageSize: 20, sortType: default, t: int(time.time() * 1000) } params[sign] make_sign(params, secret) headers { cookie: cookie, referer: https://www.goofish.com/, user-agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) } resp httpx.get(https://www.goofish.com/h5/mtop.taobao.idle.search.widget.search/1.0/, paramsparams, headersheaders, timeout10) return resp.json()代码里的make_sign只演示了最简单的排序拼接实际项目的签名算法大概率不是这一种。你从zip包里拿到的签名模块常常是一段几百行的Python或一个编译好的so文件直接调用即可。这里的关键是理解签名对参数顺序的敏感性——增删任何一个请求参数签名都要重新计算否则返回错误。接口地址和参数名也一样以抓包结果为准。不同版本的闲鱼客户端接口路径会有微调zip包里的代码如果跑不通优先排查接口版本而不是怀疑签名。4.2 用SQLAlchemy把抓取结果落到MySQL表结构设计与模型代码数据抓下来只是第一步存储才是后续分析的地基。SQLAlchemy是Python生态里最成熟的ORM之一配合MySQL或SQLite都能用。闲鱼商品数据的特点是字段不固定、详情页内容随时下架所以表设计要留足扩展空间。from sqlalchemy import create_engine, Column, String, Float, BigInteger, DateTime, Text, Integer from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime Base declarative_base() class IdleProduct(Base): __tablename__ idle_products id Column(BigInteger, primary_keyTrue, autoincrementTrue) item_id Column(String(32), uniqueTrue, indexTrue) # 闲鱼商品ID去重基准 keyword Column(String(64), indexTrue) # 搜索关键词 title Column(String(256)) price Column(Float) province Column(String(32)) city Column(String(32)) seller_nick Column(String(128)) raw_data Column(Text) # 原始JSON字段变化时兜底 created_at Column(DateTime, defaultdatetime.now) updated_at Column(DateTime, defaultdatetime.now, onupdatedatetime.now) engine create_engine(mysqlpymysql://root:passwordlocalhost:3306/xianyu_db?charsetutf8mb4) Base.metadata.create_all(engine) Session sessionmaker(bindengine)这段模型里最关键的是item_id的唯一索引。闲鱼商品ID在不同搜索关键词下会重复出现用它做唯一键才能保证同一件商品不会被插入两条记录。raw_data字段很多人觉得多余但抓取过程中经常遇到结构解析失败的情况保留原始JSON能让你事后不用重新抓一遍就能排查问题。注意SQLAlchemy 2.0之后declarative_base的导入路径有变化老项目里常见的是from sqlalchemy.ext.declarative import declarative_base新版本更推荐DeclarativeBase子类方式。zip包里如果自带模型文件优先用原项目的写法避免升级迁移带来额外工作量。4.3 增量更新与去重策略不要把同一批数据重复入库商品爬虫跑起来后最容易出现的问题是重复入库。同一件商品可能出现在多个关键词的搜索结果里同一个关键词翻页时也可能有重叠。处理思路是“先查再插有则更新无则新增”。def save_products(session, products: list, keyword: str): for item in products: exists session.query(IdleProduct).filter( IdleProduct.item_id item[itemId] ).first() if exists: # 只更新价格和更新时间保留首次抓取时间 exists.price item[price] exists.updated_at datetime.now() else: row IdleProduct( item_iditem[itemId], keywordkeyword, titleitem[title], priceitem[price], provinceitem.get(province, ), cityitem.get(city, ), seller_nickitem.get(sellerNick, ), raw_datajson.dumps(item, ensure_asciiFalse) ) session.add(row) session.commit()这段逻辑的关键在于区分“新增”和“更新”。价格、地址、卖家昵称都可能变化尤其是价格——卖家改价、包邮变不包邮这些字段对价格监控来说非常重要。更新时不要覆盖created_at否则无法分析商品首次出现的时间点。批量抓取时建议用session.bulk_save_objects或分批commit一次塞太多记录到session里内存占用会快速上涨跑几万条数据后明显卡顿。经验值是每500条左右commit一次平衡速度和内存。5. 闲鱼爬虫避坑指南风控封号、伪加密与数据脏乱的5个常见问题5.1 请求频率过高压根触发滑块验证现象连续抓取几十页后请求返回的JSON里不再有商品列表而是出现一个滑块验证页的HTML跳转或者接口返回FAIL_SYS_USER_VALIDATE。原因短时间大量搜索请求触发了闲鱼的安全策略系统判定当前账号或IP存在异常访问行为要求人工验证。这是闲鱼防盗刷的基础机制和登录态是否有效无关。解决把每次搜索后的sleep从固定的1秒改成随机2-5秒并在翻页时加入随机间隔。更彻底的方法是引入IP轮换和账号池。我一般会在代理配置里同时准备两到三个代理出口按请求轮询切换但每切换一次代理cookie中的IP绑定字段可能失效需要重新登录。所以优先级是先降频、再换账号、最后换IP不要一上来就上昂贵的基础设施。5.2 凌晨批量任务跑到一半账号被限制登录现象定时任务在凌晨1点启动跑到第200个关键词时突然所有请求返回登录态失效重新扫码登录时提示“当前账号存在异常行为”。原因闲鱼对凌晨时段的批量请求风控比白天更严格因为正常用户不太可能在凌晨连续执行几百次搜索。高频操作触发了账号层级的限制连带登录都被暂时封禁。解决把批量任务的时间窗口限制在8点到23点之间且每小时请求量控制在合理范围。另一个经验是如果一个账号被抓到立即停止该账号的所有请求切换备用账号继续跑让被限制的账号冷却24小时再观察。不要在同一台设备上登录多个账号设备指纹被标记后更容易让一批账号连带遭殃。5.3 特定关键词搜索返回的数据量远低于正常水平现象搜索“iphone15”能拿到80条商品搜索“苹果手机壳”只返回10条但网页上手动搜索明明有几百条。原因闲鱼的搜索接口做了个性化推荐和商品过滤部分商品标签与搜索词匹配度不够或关键词本身被系统限流。某些高竞争关键词卖家大量重复发布也会触发列表截断。解决把关键词拆细再聚合。比如“苹果手机壳”拆成“苹果12手机壳”“苹果13手机壳”“苹果14手机壳”分别抓取后按item_id去重。同时观察返回JSON里的total字段如果远小于页面显示数说明当前接口版本或登录账号的权限不够换一个权重高的账号试试。5.4 价格字段出现字符串拼接噪音数据入库后完全没法算均值现象Excel里看抓下来的价格列出现 12.5包邮、8888.00元这样的混杂格式Float类型字段直接报错插入失败。原因闲鱼页面上价格和运费、标签混在一起展示接口返回的价格字段有时是纯数字有时带货币符号和文字后缀。zip包里的解析正则如果写得太宽松就会把脏数据原样入库。解决入库存前先做字段清洗统一用decimal类型存储价格import re def clean_price(raw_price): if not raw_price: return None # 提取数字和小数点去掉货币符号、包邮等文字 cleaned re.search(r\d\.?\d*, str(raw_price).replace(,, )) return float(cleaned.group()) if cleaned else None这段正则从任意字符串中提取第一个合法的十进制数只保留数字和小数点。同时入库前用try-except包住价格转换逻辑遇到清洗后为None的记录打印原始字段并跳过避免单条脏数据令整个批量任务中断。5.5 zip包里自带的依赖版本太老签名模块也无法兼容新环境现象按requirements.txt装完依赖后运行主脚本立刻报ModuleNotFoundError: No module named Crypto或者ImportError: cannot import name md5 from hashlib。原因pycryptodome库在pip安装后旧版本import的是Crypto新版本是cryptography两者并存时会互相覆盖。zip包作者在自己的环境里跑得通但换一台机器后包管理器解析了同一目录下更老的版本导致模块名冲突。解决先卸载可能冲突的加密库再单独安装pycryptodome并降级版本pip uninstall crypto cryptography pycryptodome -y pip install pycryptodome3.18.0如果项目还用到了CDN加密或token解析再逐个补齐。总体原则是不要整个重装依赖先精确到报错模块一个一个解决。每次改完依赖后跑一遍项目自带的test文件或main函数确认导入无误再推进。6. 从单次抓取升级到闲鱼关键词监控的落地技巧6.1 定时调度与队列让抓取任务像水龙头一样可控单次抓取只需要跑一个脚本但关键词监控要求的是周期性执行而且不同关键词的抓取优先级可以动态调整。常见做法是用APScheduler做定时调度把关键词列表放到优先级队列里用worker并发消费。from apscheduler.schedulers.blocking import BlockingScheduler from queue import PriorityQueue import threading keyword_queue PriorityQueue() def push_keywords(): 每小时把监控关键词重新入队模拟持续关注 keywords [苹果手机, 相机, switch, 乐高] priority 0 # 数字越小优先级越高 for kw in keywords: keyword_queue.put((priority, kw)) def job_worker(): while True: _, keyword keyword_queue.get() # 抓取逻辑失败时重新入队并降低优先级 try: crawl(keyword) except Exception as e: keyword_queue.put((5, keyword)) # 降低优先级延后重试 finally: keyword_queue.task_done() if __name__ __main__: scheduler BlockingScheduler() scheduler.add_job(push_keywords, interval, hours1) worker_thread threading.Thread(targetjob_worker, daemonTrue) worker_thread.start() scheduler.start()这个方案的维度是调度器负责“什么时间触发关键词入库”worker负责“按什么顺序抓取”。失败的任务降级到队尾避免一个出问题的关键词阻塞后面所有正常关键词。实际运行中我一般把优先级和关键词热度绑定比如最近24小时价格波动大的品类自动提升优先级。6.2 数据变化检测不止是看价格变没变关键词监控的最终产出是“变化”。这些变化包括价格变动、商品上下架、新上架的商品、卖家改名等。最简单有效的做法是把每次抓取的快照存进单独的表用两条SQL做对比-- 找出新增商品 SELECT item_id FROM idle_products_snapshot_today WHERE item_id NOT IN (SELECT item_id FROM idle_products_snapshot_yesterday); -- 找出价格下降超过10%的商品 SELECT item_id, yesterday.price as old_price, today.price as new_price FROM idle_products_snapshot_today today JOIN idle_products_snapshot_yesterday yesterday ON today.item_id yesterday.item_id WHERE today.price yesterday.price * 0.9;这两条SQL的价值在于把“监控”从抓数据变成了查差异。闲鱼上的二手价格波动频繁尤其电子数码类一天降10%很正常抓到这批降价商品往往就是捡漏或调价策略的依据。6.3 自动重试与冷却机制把封号损失降到最低最后一个环节是给整条链路加上“后悔药”。爬虫跑一个月脸黑的时候会遇到封号、接口升级、签名失效这些事不可能每次都人工介入。我通常在配置里维护一个冷却列表某账号检测到风控就自动放入冷却池至少6小时不再分配抓取任务。cool_down {} def safe_crawl(account, keyword): if account in cool_down and time.time() cool_down[account]: return try: crawl_with_account(account, keyword) except RiskControlError: cool_down[account] time.time() 6 * 3600 # 冷却6小时 switch_to_backup_account()这里的关键在于把“风控异常”和“网络超时”区分开。网络超时重试一两次就好风控异常必须立即停手否则继续请求只会加深刻痕让封禁从临时变成永久。这套路径走下来从解压zip包、处理伪加密到完成登录、抓取、落库和监控你已经具备一个完整的闲鱼商品爬虫的最小可用系统。最后说一个我自己养成的习惯每次上线前先用最少数量的关键词试跑10分钟观察日志确认无报错再放开全量。这个习惯帮我避免过很多次“一觉醒来发现凌晨3点就全挂了”的惨案。希望这个方向的经验能帮到你少走一段弯路。本文还有配套的精品资源点击获取
