简介:这是一套面向数据分析初学者与Python开发者的数据采集与可视化实践工具包,聚焦二手交易平台闲鱼的市场洞察需求,解决商品价格分布、地域热度、类目结构及用户评价等核心业务问题。资源共13个文件,含8个Python脚本(涵盖爬虫解析、MongoDB写入、统计分析与图表生成)、2个文本说明文档(含配置指引与使用提示)、1个Word附赠资料、1个Markdown README及1个Git忽略配置,整体仅45KB,轻量易部署。已有53人学习下载,适合希望快速构建垂直领域数据管道的学习者。用户可直接运行主程序完成JSON数据解析、MongoDB结构化存储,并调用内置分析模块生成多维统计报表与可视化图表;配套说明文档清晰标注环境依赖与字段映射逻辑,目录按crawl→database→analysis→visualization分层组织,便于理解数据流转全过程。
1. 项目缘起:从“淘二手”到“看市场”的转变
几年前,我还在频繁使用闲鱼处理闲置物品,那时候更多是作为一个普通用户,关注的是怎么把东西快点卖出去、怎么找到便宜的好货。后来,因为工作需要研究一些消费电子产品的二手行情,我开始有意识地去批量查看闲鱼上的商品信息。手动翻页、肉眼比对价格、凭感觉判断哪个城市货多……效率低不说,得出的结论也极其片面。我意识到,闲鱼这个巨大的非标品交易市场,其公开的搜索结果页面里,其实蕴藏着大量关于价格趋势、地域分布、品类热度的结构化信息,只是它们被包裹在看似杂乱的商品列表里。
于是,一个想法诞生了:能不能写个工具,自动抓取闲鱼搜索结果的原始数据,把它解析、清洗、存起来,然后像分析一份商业数据报告一样,去分析这个市场?这个工具的核心目标,不是替代闲鱼官方的数据分析(如果有的话),而是为像我这样的个人研究者、小商家、或是某个垂直领域的爱好者,提供一个低成本、可定制、能深入挖掘市场细节的“望远镜”。它要做的,就是从海量的商品JSON数据中,提炼出价格分布、卖家地理位置、商品类别构成、评价口碑等多维度的信息,并通过图表直观地展示出来,帮助我们做出更理性的决策——无论是定价、选品,还是寻找货源。
2. 核心架构设计:数据从页面到洞察的旅程
整个工具的设计思路,遵循着数据处理的经典管道:采集 → 解析 → 存储 → 分析 → 可视化。但每个环节,针对闲鱼这个特定场景,都有其独特的考量和实现细节。
2.1 数据采集层:与闲鱼接口的“对话”
闲鱼并没有公开的官方数据API供我们直接调用。因此,数据采集的核心在于模拟浏览器行为,向闲鱼服务器发起搜索请求,并获取其返回的JSON数据包。这里的关键是找到正确的请求接口和参数。
经过对闲鱼网页版和移动端网络请求的分析,我发现其搜索结果是通过异步加载的Ajax请求获取的。核心的请求URL(这里以示例形式说明原理)通常包含搜索关键词、分页参数、排序方式等。例如,一个搜索“iPhone 13”的请求,其关键参数可能包括q(关键词)、s(排序,如default按默认、price-asc按价格升序)、page(页码)等。
注意:直接、高频地请求闲鱼服务器可能触发反爬机制,如IP封锁、请求频率限制或验证码。在实际操作中,必须加入合理的延时(如每次请求间隔2-5秒),并考虑使用代理IP池来分散请求源。尊重平台规则,仅用于个人学习和市场分析,避免对服务器造成压力。
采集脚本(以Python的requests库为例)的核心逻辑如下:
- 构造请求头(Headers),特别是
User-Agent,需要模拟成真实的浏览器或移动端设备。 - 组装请求参数,构建完整的URL。
- 发送GET请求,并处理可能的异常(如网络超时、状态码非200)。
- 接收返回的响应内容,其主体通常是一个JSON字符串。
import requests import time import random def fetch_xianyu_search_json(keyword, page=1, sort='default'): """ 模拟请求闲鱼搜索接口,获取JSON数据 """ base_url = "https://s.2.taobao.com/list/list.htm" # 示例地址,实际地址需分析 params = { 'q': keyword, 'sort': sort, 'page': page, # ... 其他必要参数 } headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept': 'application/json, text/plain, */*', 'Referer': 'https://2.taobao.com/', } try: # 添加随机延时,模拟人工操作 time.sleep(random.uniform(2, 5)) response = requests.get(base_url, params=params, headers=headers, timeout=10) response.raise_for_status() # 检查HTTP错误 # 假设返回的是JSON data = response.json() return data except requests.exceptions.RequestException as e: print(f"请求失败: {e}") return None except ValueError as e: print(f"JSON解析失败: {e}") return None2.2 数据解析与清洗:从原始JSON到规整字段
闲鱼返回的JSON数据结构嵌套较深,且字段名可能不够直观。解析的目标是将其“扁平化”,提取出我们关心的核心字段,并处理脏数据。
一个典型的商品条目可能包含以下信息(字段名为示例):
itemId: 商品ID(唯一标识)title: 商品标题price: 价格(单位:分或元,需统一)location: 卖家所在地(如“广东 深圳”)category: 商品类目(如“手机/数码/电脑办公 > 手机”)sellerInfo: 卖家信息(昵称、信用等)detailUrl: 商品详情页链接mainPic: 主图URLviews/favCount: 浏览数/收藏数(如有)description: 商品描述片段tags: 商品标签(如“包邮”、“全新”)
解析清洗步骤:
- 遍历提取:遍历JSON数据中的商品列表(如
data['items']),逐个提取上述字段。 - 单位统一:将价格统一转换为“元”为单位的浮点数。
- 地理位置拆分:将“广东 深圳”这样的字符串拆分为
province(省)和city(市)两个字段,便于后续按省、市聚合分析。 - 类目路径解析:将完整的类目路径字符串进行拆分,可能提取出一级类目、二级类目。
- 处理缺失值:对于价格、地理位置等关键字段缺失的记录,可以选择记录日志后跳过,或根据业务逻辑填充默认值(如用中位数填充价格)。
- 去重:根据
itemId对抓取到的商品进行去重,避免同一商品因分页或刷新被重复记录。
import json import re def parse_item(raw_item): """ 解析单个商品原始数据 """ item = {} # 提取基础字段 item['item_id'] = raw_item.get('itemId') item['title'] = raw_item.get('title', '').strip() # 解析价格 price_str = raw_item.get('price', '0') # 可能包含“元”、“¥”等字符,需清理 price_num = re.sub(r'[^\d.]', '', price_str) try: item['price'] = float(price_num) except ValueError: item['price'] = 0.0 # 解析失败,设为0或None # 解析地理位置 location = raw_item.get('location', '') loc_parts = location.split() item['province'] = loc_parts[0] if len(loc_parts) > 0 else '未知' item['city'] = loc_parts[1] if len(loc_parts) > 1 else item['province'] # 如果只有省,城市同省 # 解析类目 category_full = raw_item.get('category', '') cat_parts = category_full.split('>') item['category_l1'] = cat_parts[0].strip() if len(cat_parts) > 0 else '其他' item['category_l2'] = cat_parts[1].strip() if len(cat_parts) > 1 else item['category_l1'] # 其他字段 item['seller_nick'] = raw_item.get('sellerInfo', {}).get('nick', '') item['detail_url'] = raw_item.get('detailUrl', '') # ... 解析其他字段 return item2.3 数据存储选型:为什么是MongoDB?
面对闲鱼商品这种半结构化、字段可能变化、且嵌套层次不一的数据,传统的关系型数据库(如MySQL)在存储时会有些吃力。你需要预先设计严格的表结构,如果JSON中某个字段缺失或新增,就需要修改表结构或处理空值。
MongoDB作为一个文档型数据库,在这里展现出了巨大优势:
- 模式灵活(Schema-less):每条商品记录可以直接作为一个BSON文档(类似JSON)存入集合(Collection)。字段可以动态增减,无需提前定义所有列。这完美适配了从网页解析出的、结构可能微调的JSON数据。
- 原生JSON支持:MongoDB的文档存储格式BSON是JSON的二进制形式,写入和查询都非常高效自然。我们解析清洗后的Python字典,几乎可以直接插入。
- 强大的聚合框架:后续的多维度分析,如按省份统计商品数量、计算价格区间分布等,正是MongoDB聚合管道(Aggregation Pipeline)的用武之地。其
$group,$match,$project,$sort等操作符能轻松完成复杂的数据分组和计算。 - 地理位置查询:虽然本项目当前只解析了省市级文本,但MongoDB原生支持地理空间索引和查询。如果我们未来能获取到更精确的经纬度,可以轻松实现“附近X公里内的商品”这类查询。
因此,选择MongoDB作为数据存储层,是基于数据特性和分析需求的双重考虑,它能极大地简化开发流程并提升分析效率。
2.4 数据入库操作:连接与写入
使用Python操作MongoDB,主要依赖pymongo库。核心步骤包括连接数据库、选择数据库和集合、插入文档。
from pymongo import MongoClient from pymongo.errors import DuplicateKeyError, BulkWriteError def save_to_mongodb(items_list, db_name='xianyu_analysis', collection_name='items'): """ 将解析后的商品列表存入MongoDB """ # 1. 连接MongoDB,默认连接本地27017端口 client = MongoClient('localhost', 27017) # 如果需要认证: MongoClient('localhost', 27017, username='user', password='pass', authSource='admin') # 2. 选择数据库和集合(如果不存在会自动创建) db = client[db_name] collection = db[collection_name] # 3. 为item_id创建唯一索引,防止重复插入(可选,但建议) # collection.create_index([('item_id', 1)], unique=True) # 4. 批量插入数据,使用insert_many效率更高 if items_list: try: # 如果已创建唯一索引,重复的item_id会导致插入失败,可以使用`ordered=False`继续插入其他 result = collection.insert_many(items_list, ordered=False) print(f"成功插入 {len(result.inserted_ids)} 条文档。") except BulkWriteError as e: # 处理批量写入错误,如重复键错误 print(f"批量插入发生错误,部分数据可能已插入。错误详情: {e.details}") except Exception as e: print(f"插入数据时发生未知错误: {e}") else: print("待插入的数据列表为空。") # 5. 关闭连接(在长时间运行的程序中,可以保持连接) client.close()实操心得:在实际运行中,建议将采集、解析、存储三个步骤串联在一个循环中,每解析完一页数据就立即存入数据库,而不是全部抓完再存。这样做的好处是,即使中途程序因网络等问题中断,已抓取的数据也已保存,下次可以从断点续抓。另外,务必做好异常处理和日志记录,便于排查是采集失败、解析出错还是入库问题。
3. 多维度分析实战:MongoDB聚合管道的威力
数据存入MongoDB后,真正的“分析”才刚刚开始。我们不再需要将数据导出到其他工具,直接在MongoDB里使用聚合管道就能完成大部分统计工作。下面以几个典型分析场景为例。
3.1 价格分布分析:你的商品在哪个区间?
了解一个品类商品的价格集中区间,对于卖家定价和买家砍价都至关重要。我们可以将价格划分为若干个区间(如0-500, 500-1000, 1000-2000, 2000+),统计每个区间的商品数量。
from pymongo import MongoClient def analyze_price_distribution(keyword, collection): """ 分析指定关键词商品的价格区间分布 """ pipeline = [ { '$match': { 'title': {'$regex': keyword, '$options': 'i'}, # 按关键词过滤,i表示不区分大小写 'price': {'$gt': 0} # 过滤掉价格为0或无效的数据 } }, { '$bucket': { # $bucket操作符是进行分桶统计的利器 'groupBy': '$price', 'boundaries': [0, 500, 1000, 1500, 2000, 3000, 5000, 10000, float('inf')], # 定义价格区间边界 'default': 'Other', # 超出边界的归入Other,这里用不上因为inf包含了所有 'output': { 'count': {'$sum': 1}, 'avgPrice': {'$avg': '$price'}, 'minPrice': {'$min': '$price'}, 'maxPrice': {'$max': '$price'} } } }, { '$sort': {'_id': 1} # 按价格区间升序排序 } ] results = list(collection.aggregate(pipeline)) print(f"关键词 '{keyword}' 的价格分布:") for bucket in results: lower_bound = bucket['_id'] # 由于我们用了inf,最后一个桶的_id是10000,需要特殊处理显示 upper_bound = price_bounds[price_bounds.index(lower_bound) + 1] if lower_bound != 10000 else '以上' print(f" 价格区间 {lower_bound}-{upper_bound}元: {bucket['count']} 件商品, 平均价 {bucket['avgPrice']:.2f}元") return results这个分析能快速告诉你,大部分“iPhone 13”的闲鱼成交价集中在2000-3000元,还是3000-4000元?低价位(如1000元以下)的商品是真实捡漏还是问题机?为你的决策提供数据支撑。
3.2 地理位置特征:货源地与消费地
二手商品的地域分布往往能反映产地的集中度(如华强北的电子产品)、消费水平(一线城市高端商品更多)或物流便利性。我们可以按省份或城市统计商品数量。
def analyze_geo_distribution(collection, group_by='province', top_n=10): """ 分析商品的地理分布,可按省或市分组 """ pipeline = [ { '$match': { group_by: {'$ne': '未知', '$exists': True} # 过滤掉地理位置未知的数据 } }, { '$group': { '_id': f'${group_by}', # 按省份或城市分组 'itemCount': {'$sum': 1}, 'avgPrice': {'$avg': '$price'} } }, { '$sort': {'itemCount': -1} # 按商品数量降序排序 }, { '$limit': top_n # 只取前N名 } ] results = list(collection.aggregate(pipeline)) print(f"商品数量最多的前{top_n}个{group_by}:") for geo in results: print(f" {geo['_id']}: {geo['itemCount']} 件商品, 平均价格 {geo['avgPrice']:.2f}元") return results通过这个分析,你可能会发现某个品牌的二手相机大量出自某个城市(可能是产业带),或者某个型号的手机在一线城市的平均售价显著高于二三线城市。这对于寻找货源或判断商品流通性很有帮助。
3.3 类别占比分析:市场在流行什么?
闲鱼的类目树很深,分析一级或二级类目的占比,可以宏观把握当前市场的热点。例如,在“手机”这个大类下,是“苹果”多还是“华为”多?在“数码”大类下,“相机”和“游戏机”哪个更活跃?
def analyze_category_ratio(collection, category_level='category_l1'): """ 分析商品类目占比 """ pipeline = [ { '$match': { category_level: {'$exists': True, '$ne': ''} } }, { '$group': { '_id': f'${category_level}', 'count': {'$sum': 1} } }, { '$sort': {'count': -1} } ] results = list(collection.aggregate(pipeline)) total = sum(item['count'] for item in results) print("商品类目分布:") for cat in results: percentage = (cat['count'] / total) * 100 print(f" {cat['_id']}: {cat['count']} 件 ({percentage:.2f}%)") return results, total3.4 评价与卖家信用初探
虽然从搜索结果页直接获取详细的评价文本和卖家信用分比较困难(通常需要进入详情页),但有时JSON数据中会包含卖家的信用等级标识(如“芝麻信用极好”)或商品标签(如“包邮”、“安心购”)。我们可以对这些标签进行简单的频次统计,侧面反映市场的服务水平和可信度。
def analyze_tags(collection): """ 分析商品标签的分布(如果数据中包含tags字段) """ # 假设tags字段是一个数组,如 ['包邮', '芝麻信用极好'] pipeline = [ { '$match': { 'tags': {'$exists': True, '$ne': []} } }, { '$unwind': '$tags' # 将tags数组拆分成多条记录,每条一个标签 }, { '$group': { '_id': '$tags', 'count': {'$sum': 1} } }, { '$sort': {'count': -1} } ] results = list(collection.aggregate(pipeline)) print("常见商品标签:") for tag in results[:15]: # 显示前15个 print(f" {tag['_id']}: {tag['count']} 次") return results4. 可视化展示:让数据自己说话
分析出的数据是冰冷的数字,图表才能直观地讲述故事。Python的Matplotlib和Seaborn库是进行静态可视化的绝佳选择,而Pyecharts或Plotly则可以生成交互性更强的网页图表。这里以Matplotlib为例,展示如何将上述分析结果可视化。
4.1 价格分布直方图
直方图是展示价格分布最直观的方式。
import matplotlib.pyplot as plt import numpy as np def plot_price_histogram(price_data, keyword, bins=20): """ 绘制价格分布直方图 price_data: 从MongoDB查询出的价格列表 """ plt.figure(figsize=(10, 6)) # 过滤掉极端高价,避免长尾影响视图(可根据实际情况调整) filtered_prices = [p for p in price_data if p < 10000] n, bins, patches = plt.hist(filtered_prices, bins=bins, edgecolor='black', alpha=0.7) plt.xlabel('价格 (元)') plt.ylabel('商品数量') plt.title(f'闲鱼商品 "{keyword}" 价格分布直方图') plt.grid(axis='y', alpha=0.75) # 在柱子上方标注数量 for i in range(len(patches)): plt.text(patches[i].get_x() + patches[i].get_width() / 2, patches[i].get_height() + 0.5, str(int(n[i])), ha='center', va='bottom', fontsize=9) plt.tight_layout() plt.savefig(f'price_distribution_{keyword}.png', dpi=300) plt.show() # 使用示例:先从数据库查询价格列表 client = MongoClient('localhost', 27017) db = client['xianyu_analysis'] collection = db['items'] prices = [doc['price'] for doc in collection.find({'title': {'$regex': 'iPhone 13', '$options': 'i'}, 'price': {'$gt': 0}}, {'price': 1})] plot_price_histogram(prices, 'iPhone 13')4.2 地理位置商品数量柱状图
用柱状图展示商品数量最多的前10个省份或城市。
def plot_geo_barchart(geo_results, group_by='省份'): """ 绘制地理位置商品数量柱状图 geo_results: analyze_geo_distribution函数返回的结果 """ locations = [item['_id'] for item in geo_results] counts = [item['itemCount'] for item in geo_results] plt.figure(figsize=(12, 6)) bars = plt.bar(locations, counts, color='skyblue', edgecolor='grey') plt.xlabel(group_by) plt.ylabel('商品数量') plt.title(f'闲鱼商品数量 Top {len(locations)} {group_by}') plt.xticks(rotation=45, ha='right') # 旋转x轴标签,防止重叠 # 在柱顶显示数量 for bar, count in zip(bars, counts): plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, str(count), ha='center', va='bottom', fontsize=9) plt.tight_layout() plt.savefig(f'geo_distribution_top{len(locations)}.png', dpi=300) plt.show()4.3 类目占比饼图
饼图适合展示类目的构成比例。
def plot_category_pie(category_results, top_n=8): """ 绘制类目占比饼图 category_results: analyze_category_ratio函数返回的结果 top_n: 显示前N个类目,其余合并为“其他” """ top_categories = category_results[:top_n] other_count = sum(item['count'] for item in category_results[top_n:]) labels = [item['_id'] for item in top_categories] sizes = [item['count'] for item in top_categories] if other_count > 0: labels.append('其他') sizes.append(other_count) plt.figure(figsize=(8, 8)) plt.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=140, textprops={'fontsize': 10}) plt.axis('equal') # 保证饼图是正圆 plt.title('闲鱼商品类目占比') plt.tight_layout() plt.savefig('category_pie_chart.png', dpi=300) plt.show()4.4 集成与自动化报告
我们可以将上述分析函数和绘图函数整合到一个脚本中,针对某个关键词(如“索尼微单”)自动运行全套分析,并生成一个包含多张图表的HTML报告。使用Jinja2模板引擎可以很方便地实现这一点。报告可以包含数据摘要、关键发现和所有可视化图表,方便分享和存档。
5. 踩坑实录与进阶思考
在开发和运行这个工具的过程中,我遇到了不少预料之中和预料之外的“坑”。
5.1 反爬策略与请求优化
闲鱼对爬虫的防御不算最严,但绝非没有。除了之前提到的请求频率控制,还有几点需要注意:
- 请求头(Headers):务必模拟得足够像。除了
User-Agent,Referer、Accept-Language等字段也最好带上。有时缺少Cookie中的某个关键值也会导致返回空数据或错误页。 - 参数加密:某些接口的关键参数可能是加密或经过编码的。你需要仔细分析网页加载时的JavaScript代码,找到参数生成逻辑,并用Python复现。这可能是整个采集过程中技术难度最高的部分。
- IP代理池:对于大规模、长时间的采集,使用付费或自建的代理IP池几乎是必须的。免费的代理IP往往不稳定,速度慢,且容易被目标网站封禁。
- 验证码处理:如果触发了验证码,简单的方案是暂停一段时间或更换IP。复杂的方案需要引入图像识别或打码平台,成本会急剧上升。最佳实践是控制请求节奏,尽量避免触发验证码。
5.2 数据质量与清洗的挑战
闲鱼是C2C平台,商品信息由用户填写,数据质量参差不齐。
- 价格异常值:存在大量“1元”、“999999元”这种引流或非真实售价。在分析前必须进行过滤,例如只分析价格在某个合理区间(如10元到50000元)的商品。
- 标题与类目不符:用户可能把手机挂在“服装”类目下。单纯依赖平台类目进行分析可能不准,可以结合标题关键词进行辅助判断。
- 地理位置模糊:很多卖家只填写到省份,甚至填写“海外”。对于需要精确城市分析的需求,这部分数据价值有限。
- 商品状态:很难从搜索结果页直接区分商品是“已售出”还是“在售”。这会影响对真实市场库存的判断。一种间接方法是观察商品的发布时间和浏览量,但并非绝对准确。
5.3 MongoDB性能与查询优化
当数据量达到数十万甚至上百万条时,查询和聚合操作的性能变得重要。
- 索引是王道:在经常用于查询(
$match)和分组($group)的字段上建立索引,能极大提升速度。例如,在title(用于关键词搜索)、price、province、category_l1等字段上创建索引。collection.create_index([('title', 'text')]) # 文本索引,支持$text搜索 collection.create_index([('price', 1)]) collection.create_index([('province', 1), ('city', 1)]) - 聚合管道优化:尽量在管道早期使用
$match和$project操作符,减少流入后续阶段的数据量。例如,先过滤出需要的字段和记录,再进行昂贵的$group或$sort操作。 - 内存限制:MongoDB的聚合管道默认有100MB的内存限制。对于超大的数据集进行复杂聚合时,可能会报错。可以通过设置
allowDiskUse: True选项来允许使用磁盘临时文件,但会牺牲速度。
5.4 分析的局限性与扩展方向
这个工具提供的是一个基于公开搜索结果的、静态的、截面数据分析。它有几个天然的局限:
- 无法获取成交价:我们看到的是卖家的标价,而非最终成交价。真实的市场价格可能围绕标价有较大波动。
- 无法跟踪动态:数据是某个时间点的快照,无法反映价格随时间的变化趋势。要实现趋势分析,需要定期(如每天)抓取相同关键词的数据,形成时间序列。
- 信息深度有限:详情页中的宝贝描述、卖家历史评价、私聊记录等更丰富的信息无法通过简单搜索获取。
可能的扩展方向:
- 定时任务与历史数据:使用
APScheduler或Celery搭建定时爬虫,积累历史数据,从而分析价格走势、品类热度周期(如开学季的电子产品、换季时的服装)。 - 情感分析与文本挖掘:如果能够获取商品描述和评价文本,可以利用
Jieba分词和snownlp等库进行情感分析,了解商品的口碑和常见问题。 - 关联规则分析:分析经常被一起浏览或收藏的商品组合,发现潜在的“搭配销售”机会。
- 构建简单预警系统:监控特定商品(如你关注的某个型号)的价格,当出现低于设定阈值的新商品时,通过邮件或钉钉机器人通知你,实现“捡漏”自动化。
这个闲鱼数据分析工具,从一行行代码搭建起来,到最终能产出有洞察力的图表,整个过程就像是在庞大的二手市场里安装了一个数据雷达。它不能保证你每次都能“捡漏”,但能极大地消除信息不对称带来的盲目性,让你在交易时心里更有底。技术本身并不复杂,难的是对业务场景的理解和持续迭代的耐心。希望这个分享,能给你带来一些动手分析身边数据的灵感。
本文还有配套的精品资源,点击获取