Python二手车爬虫数据分析可视化系统:从采集到大屏的完整实现
2026/9/23 16:50:02 网站建设 项目流程

简介:本资源为基于Python的二手车爬虫数据分析可视化系统毕业设计完整项目包,面向计算机、软件工程、人工智能、通信工程等专业的在校学生与教师,可用于毕业设计、课程设计、作业提交或项目初期立项演示。项目已通过测试运行,功能完整,个人评分98分,适合小白进阶学习,也便于在现有代码基础上二次修改扩展。压缩包共约2000个文件,整体53.99MB,以1745个py源码文件为核心,辅以112个h头文件、88个txt说明、15个html页面、13个json配置、11个pdf文档及少量js、css、c等文件,覆盖爬虫采集、数据清洗、分析建模与可视化展示等模块。目前已有536人学习下载。读者可获得完整源码、数据库与使用文档,理解二手车数据从抓取到图表呈现的全流程实现思路,并参考目录结构与排错方法快速完成自己的毕设或课设任务。

1. 二手车数据从哪来:一套能跑通的采集链路长什么样

二手车价格数据是典型的「脏、散、变」三高数据:同一辆车在车商页面写「2019款 改款 2.0L」,在个人卖家那里可能只写「19年 2.0」,品牌名还夹着全角空格和 emoji。想用 Python 做二手车爬虫数据分析可视化系统,第一件要解决的事不是画图,而是让采集链路稳定产出结构化记录。这套系统适合两类人:一是做毕业设计、需要完整闭环(采集→存储→分析→可视化)的同学,二是想练手 requests + SQLAlchemy + Pandas + ECharts 这条主流技术栈的入门者。它不追求分布式高并发,追求的是单机可复现、字段可解释、图表能讲故事。下面按我实际搭过的顺序,把每个环节的参数和坑讲清楚。

2. 采集层:requests 抓列表页与详情页的最小闭环

2.1 先定字段再写代码,别反过来

很多人上来就requests.get,抓到 HTML 再想解析什么,结果字段反复改、数据库反复重建。我一般先定一张字段表,采集和入库都围着它转。二手车列表页通常能给到:标题、价格、指导价、上牌时间、行驶里程、排放标准、城市、详情链接;详情页再补:变速箱、排量、过户次数、车源编号。字段定完,解析函数只做一件事——把 HTML 节点映射到这些 key。

字段来源类型说明
title列表页str原始标题,保留不清洗
price列表页float单位万元,去「万」字
guide_price列表页float新车指导价,可能为空
reg_date列表页str上牌时间,统一成 YYYY-MM
mileage列表页float单位万公里
city列表页str城市名,用于分组
detail_url列表页str详情页入口
gearbox详情页str变速箱类型
displacement详情页float排量 L

提示:字段表一旦确定,数据库建表语句和 Pandas 的列名都从它派生,后面改字段只改一处。

2.2 列表页请求:请求头、翻页与限速

列表页是分页接口或分页 HTML,核心是构造 URL 和翻页终止条件。下面这段是我常用的骨架,重点看headersparamssleep三处。

import requests import time import 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", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_list(page: int, city: str = "bj") -> str: url = "https://example-car.com/usedcar/list" params = {"city": city, "page": page, "size": 40} resp = requests.get(url, headers=HEADERS, params=params, timeout=10) resp.raise_for_status() # 随机停顿,降低被限流的概率 time.sleep(random.uniform(1.2, 2.8)) return resp.text

逻辑说明:params把城市和页码交给服务端,避免手拼字符串出错;raise_for_status让 4xx/5xx 直接抛异常,而不是把错误页当正常页解析;time.sleep用随机区间而不是固定值,固定间隔反而更容易被识别为脚本。参数上,size不要贪大,40 左右一页既减少请求数又不至于触发风控;timeout必须设,否则网络卡住会挂死整个循环。

翻页终止条件别只看「有没有下一页按钮」,更稳的是判断本页解析出的记录数是否小于size,小于就说明到底了。这个判断比 DOM 结构可靠,因为页面改版时按钮 class 最先变。

2.3 详情页解析:用 lxml 还是 BeautifulSoup

解析库我一般用 lxml + XPath,速度快、表达式紧凑;BeautifulSoup 更适合结构混乱、需要容错的页面。二手车详情页字段分散在多个 div 里,XPath 写起来直观。下面是一个解析函数,注意每个字段都做了空值兜底。

from lxml import etree def parse_detail(html: str) -> dict: tree = etree.HTML(html) def text(xp): r = tree.xpath(xp) return r[0].strip() if r else None return { "gearbox": text('//div[@class="info-item"][1]/span/text()'), "displacement": text('//div[@class="info-item"][2]/span/text()'), "transfer_count": text('//div[@class="info-item"][3]/span/text()'), }

逻辑说明:text内部函数统一处理「节点不存在」的情况,返回 None 而不是抛异常,这样单条记录缺字段不会中断整批采集。参数上,XPath 里的[1][2]是位置索引,页面结构一变就会错位,所以采集完要抽样人工核对几条,别全量跑完才发现字段串位。

2.4 用 SQLAlchemy 把数据落库,别存 CSV 就完事

热搜里常出现sqlalchemy储存爬虫数据,这不是没道理:CSV 没法做去重、没法增量更新、并发写还容易坏。SQLAlchemy 的 ORM 让「采集→入库」变成一次session.add。建表时给detail_url加唯一索引,重复采集时用merge或先查后插,避免数据翻倍。

from sqlalchemy import create_engine, Column, Integer, String, Float from sqlalchemy.orm import declarative_base, sessionmaker Base = declarative_base() class UsedCar(Base): __tablename__ = "used_car" id = Column(Integer, primary_key=True, autoincrement=True) title = Column(String(255)) price = Column(Float) mileage = Column(Float) city = Column(String(32)) detail_url = Column(String(255), unique=True) gearbox = Column(String(32)) engine = create_engine("mysql+pymysql://user:pwd@localhost:3306/car?charset=utf8mb4") Base.metadata.create_all(engine) Session = sessionmaker(bind=engine)

逻辑说明:unique=True是去重的关键,配合入库前的查询或INSERT ... ON DUPLICATE KEY UPDATE实现增量。charset=utf8mb4必须写,否则标题里的 emoji 和生僻字会报错。参数上,连接串里的库名、账号密码按本地环境改,别硬编码进提交的代码里。

3. 清洗与分析:Pandas 把脏标题变成可分组字段

3.1 从数据库读到 DataFrame 的正确姿势

分析层第一步是把表读进来。用pd.read_sql直接读,别先导 CSV 再读,多一道工序多一处出错。

import pandas as pd from sqlalchemy import create_engine engine = create_engine("mysql+pymysql://user:pwd@localhost:3306/car?charset=utf8mb4") df = pd.read_sql("SELECT * FROM used_car", engine) print(df.shape, df.isnull().sum())

逻辑说明:先看shape和缺失值分布,再决定清洗策略。如果price缺失超过三成,说明列表页解析规则有问题,要回去修采集而不是在分析层硬填。参数上,read_sql的 SQL 尽量只选需要的列,全表读在数据量大时会吃内存。

3.2 价格、里程、上牌时间的标准化

价格字段常见「12.8万」「12.80万」「面议」,里程常见「3.5万公里」「3.5万公里(表显)」。用正则统一抽取数字,抽不到的置为 NaN 并单独统计。

import numpy as np def to_float(x): if pd.isna(x): return np.nan import re m = re.search(r"(\d+\.?\d*)", str(x)) return float(m.group(1)) if m else np.nan df["price"] = df["price"].apply(to_float) df["mileage"] = df["mileage"].apply(to_float) df["reg_year"] = df["reg_date"].str.extract(r"(\d{4})").astype(float)

逻辑说明:to_float只取第一个数字,能覆盖绝大多数写法;reg_year单独抽年份,方便后面按年分组算折旧。参数上,正则\d+\.?\d*不匹配负数,二手车价格和里程本来也不该为负,出现负数说明解析串位,要排查。

3.3 用分箱和分组把连续值变成可解释维度

可视化要的是「能对比的类别」,不是一堆散点。把价格分箱成「10万以下 / 10-20万 / 20-30万 / 30万以上」,把里程分箱成「3万内 / 3-8万 / 8万以上」,再按城市、年份做透视。

bins = [0, 10, 20, 30, 1000] labels = ["10万以下", "10-20万", "20-30万", "30万以上"] df["price_band"] = pd.cut(df["price"], bins=bins, labels=labels) pivot = df.pivot_table(index="city", columns="price_band", values="id", aggfunc="count", fill_value=0) print(pivot.head())

逻辑说明:pd.cut把连续价格切成有序类别,pivot_table直接产出「城市 × 价格区间」的计数矩阵,这个矩阵就是后面柱状图和热力图的直接输入。参数上,bins的边界要按你采集到的实际价格分布调,别照搬,否则某一档可能全是 0。

3.4 折旧分析的常见口径与陷阱

二手车分析绕不开折旧:同款车第一年掉多少、第三年掉多少。常见做法是用「当年指导价」和「当前售价」算保值率,但这里有个大坑——指导价字段经常缺失,而且不同配置指导价差很多。我一般只对「指导价非空且样本量大于 30」的车型做保值率,样本太少的车型算出来没有统计意义。另一个陷阱是把上牌年份当购车年份,实际两者可能差几个月,做年度对比时影响不大,做月度分析就会失真。

4. 可视化:从 ECharts 到可视化大屏的落地选择

4.1 图表选型:什么数据配什么图

可视化不是把数据全画出来,而是每张图回答一个问题。价格分布用直方图或箱线图,城市对比用横向柱状图,年份与价格关系用折线或散点,品牌占比用饼图或矩形树图。热搜里的可视化大屏适合答辩展示,但大屏的核心是布局和联动,不是图表数量,堆十几张图反而看不清重点。

分析目标推荐图表数据输入
价格整体分布直方图price 单列
各城市车源量横向柱状图city 计数
年份与均价关系折线图reg_year 分组均值
品牌价格区间箱线图brand + price
价格带占比饼图/环形图price_band 计数

4.2 用 Pyecharts 生成可交互图表

Pyecharts 把 ECharts 封装成 Python 对象,适合在脚本里直接出 HTML。下面生成一张城市车源量柱状图。

from pyecharts.charts import Bar from pyecharts import options as opts city_count = df["city"].value_counts().head(10) bar = ( Bar() .add_xaxis(city_count.index.tolist()) .add_yaxis("车源量", city_count.values.tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="城市车源量 TOP10"), xaxis_opts=opts.AxisOpts(name="城市"), yaxis_opts=opts.AxisOpts(name="数量"), ) ) bar.render("city_count.html")

逻辑说明:value_counts().head(10)先排序再截断,避免城市太多导致 X 轴挤成一团;set_global_opts统一设置标题和轴名,比在每个系列里重复设置清晰。参数上,head(10)的 10 按展示宽度调,大屏可以放到 15,网页嵌入建议 8 以内。

4.3 大屏布局的三个实用约束

做可视化大屏时,我踩过的坑集中在三点:一是分辨率,按 1920×1080 设计,别用响应式百分比,否则投影仪上全乱;二是配色,深色底配高饱和系列色,浅色底配低饱和,混用会显得脏;三是数据刷新,答辩演示用静态 HTML 就够,真要接实时数据再上 Flask 接口,别为了「实时」两个字把架构搞复杂。热搜里的python爬虫可视化界面如果指桌面 GUI,Tkinter 或 PyQt 都能做,但 Web 大屏在展示效果和开发效率上更划算。

5. 避坑与排查:采集和分析阶段最容易翻车的地方

5.1 现象:跑几十页后返回空列表或验证页

原因:请求频率过高触发风控,或User-Agent长期不变被标记。解决:把sleep区间拉大到 2-5 秒,准备多个User-Agent轮换,并在解析前判断返回内容是否包含关键节点,不包含就跳过并记录页码,而不是硬解析出空记录。

5.2 现象:数据库里同一条车源出现多条

原因:detail_url没加唯一约束,或入库用add而非先查后插。解决:给detail_url建唯一索引,入库前用session.query(UsedCar).filter_by(detail_url=url).first()判断,存在则更新价格等易变字段,不存在才插入。

5.3 现象:价格字段大量为 NaN

原因:页面价格写法超出正则覆盖范围,比如「12万8」「1X万」这类非标准写法。解决:先抽样 50 条把价格原文打印出来,统计所有写法,再补正则分支;补完仍抽不到的,单独存一张异常表,人工看几条就能定位规律。

5.4 现象:Pandas 分组结果里出现空组或顺序错乱

原因:pd.cutbins边界没覆盖全部数据,超出边界的值变成 NaN 被丢弃;或分组列里有前后空格导致同一城市被拆成两组。解决:分箱前先df["price"].describe()看极值,把bins上界设得足够大;分组前对city等文本列统一str.strip()

5.5 现象:Pyecharts 生成的 HTML 打开空白

原因:图表数据里含 NaN 或非 JSON 可序列化类型,渲染时静默失败。解决:出图前对输入列做dropna()和类型转换,value_counts()的结果本身就是干净的,但透视表要检查有没有 NaN,必要时fillna(0)

6. 让这套系统更耐用的两个进阶习惯

第一个习惯是给采集加「断点续跑」。做法很简单:在数据库里记一张crawl_log表,字段是citypagestatusupdated_at,每采完一页写一条。重跑时先查status='done'的最大页码,从下一页继续。这个习惯在毕设答辩前夜尤其救命——网络一断不用从头再来。第二个习惯是把清洗规则写成独立函数并配单元测试,比如to_float("12.8万") == 12.8to_float("面议") is np.nan,规则一改跑一遍测试就知道有没有误伤。我自己的教训是:第一版系统把所有逻辑塞在一个 800 行的脚本里,改一个正则要通读全文,后来拆成fetch.pyparse.pyclean.pyviz.py四个文件,每个文件不超过 200 行,排查问题时定位速度快了不止一倍。验证方法上,我一般随机抽 20 条数据库记录,手工打开对应详情页核对字段,准确率低于 95% 就先修解析再谈分析。这套系统值不值得做,取决于你能不能把「采集稳定」和「字段可解释」这两件事做扎实,图表只是最后一步。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询