news 2026/10/14 9:48:43

二手车爬虫可视化毕设项目:从数据采集到分析闭环

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
二手车爬虫可视化毕设项目:从数据采集到分析闭环

简介:面向毕业设计、课程设计及期末大作业场景的Python项目完整方案,围绕二手车爬虫与数据可视化展开,覆盖数据采集、清洗、存储和图表展示全流程。项目已获导师指导并通过答辩,评审得分97分,下载后无需修改即可运行。资源共2000个文件,以py源码文件为绝对主体(1745个),辅以h/C底层接口、txt/json数据与说明、HTML/JS/CSS可视化页面、PDF文档等,压缩包约54.99MB,目录结构完整。项目附带完整数据库文件,便于直接复现数据存储与查询逻辑,文档说明覆盖设计思路、接口实现与使用步骤。内容涵盖完整源码、数据库文件与文档说明,可直接用于答辩展示或二次开发,省去从零搭建环境与调试排错的时间成本。目前已有658人学习使用。

1. 为什么二手车的爬虫可视化项目能拿高分:先跑通数据闭环再谈分析

很多人的毕设简历里写“Python爬虫 + 可视化”,一上台就被问住:爬下来的数据长什么样?清洗了几步?图表背后的结论是什么?给二手车写爬虫可视化也不例外——如果只是从某二手车平台抓了几千条在售车源,用 pyecharts 画上几张价格分布图,答辩时很难自圆其说。这个资源能解决的核心问题不是“怎么爬”,而是把一套完整的数据闭环跑给你看:请求采集、字段清洗、MySQL 落库、可视化分析、结论反哺——每一步都有源码、文档和数据库文件兜底。适合正在做毕设、课程设计或期末大作业的同学,也适合想快速搭起一个“爬虫 + 数据分析”完整项目来面试演示的开发者。拿高分的点也恰恰在于:它不像 demo,更像一个小型数据产品。

2. 数据采集层:requests 与 BeautifulSoup 的字段抓取设计

2.1 为什么用 requests 而不是 Scrapy:毕业设计场景下的选型逻辑

Scrapy 是工业级爬虫框架,自带并发、去重、中间件和命令行工具,但在这个资源里并没有被选为主力采集工具,核心原因有三个:一是项目目标偏数据分析,采集量级在万条以内,requests + BeautifulSoup 的同步请求完全够用;二是结构简单好讲,答辩时你能把每一步请求、解析、入库说清楚,而不是被框架的组件关系绕晕;三是依赖更少,部署到别人机器上不容易因为环境问题翻车。

requests 的本质是封装了 HTTP 协议的客户端,你用它发起 GET 请求拿到 HTML 字符串,再交给 BeautifulSoup 去解析。常见的做法是定义一个 session 保持连接,并在 headers 里带上 User-Agent、Referer 这些字段,避免被服务器直接挡掉。对于静态页面,这个组合非常稳。若平台页面是动态渲染的,那就得换 selenium 或模拟接口请求,这属于进阶玩法,后面会展开。

2.2 核心抓取代码的逐段拆解:请求、列表解析与详情页回填

这个资源里的爬虫脚本大概是分三步走的:先请求列表页拿车源 ID 和基本信息,再请求详情页补齐里程、排量、变速箱等字段,最后统一落库。下面是我根据资料整理出的典型代码结构:

import requests from bs4 import BeautifulSoup import time import random def build_session(): session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://www.example-automarket.com/", "Accept-Language": "zh-CN,zh;q=0.9" }) return session def parse_list_page(html): soup = BeautifulSoup(html, "html.parser") items = [] for card in soup.select(".car-card"): item = { "car_id": card.get("data-id"), "title": card.select_one(".car-title").get_text(strip=True), "price": card.select_one(".price").get_text(strip=True), "href": card.select_one("a").get("href") } items.append(item) return items

build_session 里做的事情是统一会话管理。headers 不写全的话,部分站点会按 UA 特征直接返回 403;带上 Referer 是为了让请求看起来像从站内跳转过来的。parse_list_page 里的 select 选择器是这个资源的可调参数,如果目标站点改版,你只需要换掉这些 CSS 路径,不影响后续逻辑。get_text(strip=True) 会把标签内的空白字符清掉,拿到干净的文本,但注意价格单位、货币符号仍然混在里面,留给下一步清洗。

def fetch_detail(session, href): resp = session.get(href, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") return { "mileage": soup.select_one(".mileage").get_text(strip=True), "gearbox": soup.select_one(".gearbox").get_text(strip=True), "displacement": soup.select_one(".displacement").get_text(strip=True), "first_reg": soup.select_one(".reg-date").get_text(strip=True) } def crawl_all(max_pages=50): session = build_session() all_data = [] for page in range(1, max_pages + 1): url = f"https://www.example-automarket.com/page/{page}" try: resp = session.get(url, timeout=10) if resp.status_code != 200: break items = parse_list_page(resp.text) for it in items: detail = fetch_detail(session, it["href"]) it.update(detail) all_data.append(it) time.sleep(random.uniform(0.5, 1.5)) except Exception as e: print(f"page {page} error: {e}") return all_data

crawl_all 这个函数里的 max_pages 是控制采集规模的开关,毕设场景建议 30~80 页,既保证样本量足够说明问题,又不至于把时间全耗在请求上。time.sleep(random.uniform(0.5, 1.5)) 是给请求之间加随机间隔,这个设计很关键,既能降低对目标站点的压力,也避免因为请求频率过高被临时封 IP。每个列表页解析完马上请求详情页,是典型的“列表 + 详情”两段式采集,比单抓列表页多拿到核心字段,数据维度更完整。

2.3 采集字段的设计思路:一手数据和二手字段要分开存

抓回来的字段不能一股脑塞进一张表里。我的习惯是:列表页给的是“展示层数据”,比如标题、标价、链接;详情页给的是“车况层数据”,比如里程、排量、首次上牌时间。这两类数据变更频率不同,改价只影响列表,车况和上牌时间基本不变。所以表的字段设计应该是车源 ID 做唯一标识,标价和上牌时间单独成列,便于后续做价格与车龄的关联分析。

这个资源的 sql 文件里大概就是这么设计的,表名一般类似 car_info,字段包括:id、title、price、mileage、gearbox、displacement、first_reg_date、source_url、crawl_time。其中 crawl_time 记录了抓取时间,这在做增量更新时非常有用。另外,如果你打算做价格预测,一定要留出“上牌时间”和“里程”这两个强特征字段,它们是二手车估价模型里最重要的输入。别只存展示文本,尽量存成可计算的数字类型,清洗后入库更省事。

3. 数据清洗与入库:从脏文本到结构化 SQL 表的完整转换

3.1 常见脏数据长什么样:价格单位、空值、重复记录

爬下来的数据远没有页面显示得干净。我在实际拆这个项目的时候整理过三类典型问题:

第一类是价格字段混入单位。页面上显示“12.80 万”、“8800 元”,直接转 float 会报错,必须先判断单位再换算,统一转成元的整数或万元的浮点数,否则后续的价格分布图和均价计算全部失去意义。

第二类是空值和占位符。没写里程的车源可能显示为“-”或者“暂无”,直接丢弃会损失样本量,常见处理方式是填充为 0 或 NULL,在分析阶段单独过滤。

第三类是重复记录。同一个车源可能被多个关键词搜出来,或因为翻页重复抓取,需要用 car_id 或 source_url 做唯一约束去重。不去重的话,价格均值会被高估或低估,答辩时一问到样本量就露馅。

3.2 清洗脚本的最小可用实现:pandas 清洗 + 正则替换

下面这段代码是这个项目清洗脚本里的主干逻辑,基于 pandas 做 DataFrame 级操作,能显著减少循环代码:

import pandas as pd import re def clean_price(value): if not isinstance(value, str): return value value = value.replace(",", "").strip() if "万" in value: return float(re.sub(r"[^\d.]", "", value)) * 10000 if "元" in value: return float(re.sub(r"[^\d.]", "", value)) return float(value) def clean_dataframe(df): df = df.drop_duplicates(subset=["car_id"], keep="first") df["price"] = df["price"].apply(clean_price) df["mileage"] = df["mileage"].astype(str).str.replace("万公里", "") df["mileage"] = df["mileage"].replace("-", "0").astype(float) df["first_reg_date"] = df["first_reg_date"].astype(str).str.extract(r"(\d{4}[-/]\d{1,2})") return df

clean_price 里的正则只保留数字和小数点,把“12.80 万”转成 128000 元,统一价格货币。mileage 列的处理逻辑类似,“3.5 万公里”转成 3.5,以万公里为单位存储,分析时乘 10000 换算。first_reg_date 用正则把“2022-05-08 上牌”截成“2022-05”,方便后续按年份聚合分析。这样清洗后的 DataFrame 类型统一,可以直接写入 MySQL。

3.3 入库与去重的正确姿势:唯一索引与 INSERT ON DUPLICATE KEY

清洗后的数据要落进 MySQL。数据库文件里提供了建表语句,但如果你自己重建,核心是给 car_id 建唯一索引,并配合 INSERT ON DUPLICATE KEY 做增量更新:

CREATE TABLE IF NOT EXISTS car_info ( id INT AUTO_INCREMENT PRIMARY KEY, car_id VARCHAR(64) NOT NULL, title VARCHAR(255), price DECIMAL(12,2), mileage FLOAT, gearbox VARCHAR(16), displacement FLOAT, first_reg_date VARCHAR(32), source_url VARCHAR(255), crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY idx_car_id (car_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
import pymysql def insert_to_db(df): conn = pymysql.connect( host="localhost", user="root", password="123456", database="car_db", charset="utf8mb4", autocommit=True ) sql = """ INSERT INTO car_info (car_id, title, price, mileage, gearbox, displacement, first_reg_date, source_url) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE price=VALUES(price), mileage=VALUES(mileage) """ data = list(df.itertuples(index=False, name=None)) with conn.cursor() as cursor: cursor.executemany(sql, data) conn.close()

这条 SQL 语句里的 ON DUPLICATE KEY UPDATE 是实现幂等入库的关键。第一次跑任务插入新数据,第二次跑同一个任务时会自动更新价格和里程,而不是再插一条重复记录。pymysql 的 executemany 批量执行比逐条 execute 快非常多,数据量上万时能明显感觉到差距。需要注意:df.itertuples 出来的元组顺序必须和 SQL 里的字段顺序一致,否则数据会错位,真踩了这个坑你只能翻库逐条对。

3.4 数据库连接小坑:字符集和事务提交

很多人在入库时报错 UnicodeEncodeError 或者中文乱码,十有八九是连接串里没有指定 charset="utf8mb4"。MySQL 5.5.3 以上建议直接 utf8mb4,它完整支持四字节字符,比如一些车源标题里的特殊符号。另外 autocommit 设置很关键,不设的话数据始终只在事务里,连接一断数据全丢,查库还是空表。

4. 可视化分析:pyecharts 图表设计与从图里反推答辩结论

4.1 为什么选 pyecharts 而不是 Matplotlib:交互图表更适合答辩演示

Matplotlib 是学术图表首选,画出的分布图、散点图很专业,但它输出的是静态图片。答辩场景下,一组能 hover 显示数值、能缩放、能切换图例的交互式图表明显更有冲击力。pyecharts 生成的 HTML 图表可以直接嵌入 PPT 或者用浏览器全屏演示,且基于 echarts 渲染,图表对象是 JSON 配置,便于保存快照或二次修改。

这个项目的图表设计大概覆盖了五个维度:价格区间分布、里程与价格散点关系、品牌/车型均价对比、上牌年份与价格走势、变速箱类型与保值率。每一张图背后都要对应一个分析结论,別为了凑图而画图。

4.2 核心图表代码:价格分布与散点图的参数说明

下面这段是从资源中提炼出来的核心可视化配置,生成两个常用图:价格直方图和里程-价格散点图:

from pyecharts import options as opts from pyecharts.charts import Bar, Scatter def price_distribution(df, bins=[0,5,10,15,20,30,50]): labels = [f"{bins[i]}-{bins[i+1]}万" for i in range(len(bins)-1)] df["price_band"] = pd.cut(df["price"], bins=bins*10000, labels=labels) counts = df["price_band"].value_counts().sort_index() bar = ( Bar(init_opts=opts.InitOpts(width="900px", height="500px", page_title="二手车价格分布")) .add_xaxis(labels) .add_yaxis("车源数量", counts.tolist(), category_gap="40%") .set_global_opts( title_opts=opts.TitleOpts(title="二手车价格区间分布"), yaxis_opts=opts.AxisOpts(name="车源数量"), xaxis_opts=opts.AxisOpts(name="价格区间"), ) ) return bar def mileage_price_scatter(df): scatter = ( Scatter(init_opts=opts.InitOpts(width="900px", height="500px")) .add_xaxis(df["mileage"].tolist()) .add_yaxis("二手车样本", df["price"].tolist(), symbol_size=8, label_opts=opts.LabelOpts(is_show=False)) .set_global_opts( title_opts=opts.TitleOpts(title="里程与价格关系"), xaxis_opts=opts.AxisOpts(name="里程(万公里)", type_="value"), yaxis_opts=opts.AxisOpts(name="价格(元)", type_="value"), ) ) return scatter

pd.cut 的 bins 参数是分箱边界,这里把价格切成 5 万以下、5-10 万、10-15 万等六个区间。这个分箱粒度覆盖了二手车的主流交易区间,让分布看起来有层次。散点图是画“里程”和“价格”两个连续变量的关系,如果样本量足够,能看到明显的负相关趋势线——但这里没有做回归拟合,答辩时你自己要能解释“数据点大致呈下降趋势,但存在大量离群点”。

4.3 如何从图里引出分析结论:答辩讲稿背后的数据逻辑

图表不是终点,数据结论才是。比如价格分布图呈右偏,说明低价车源占多数,市场以经济型车为主;散点图出现“低里程但价格异常低”的点,要去查是不是事故车或标价错误;年份维度的曲线能看出哪一年的车保值率最高。这套逻辑整理成 3~5 条结论,比任何开场白都更能撑场面。

顺带提一个细节:pyecharts 生成的 HTML 文件名和图表标题也要专业一点,比如 price_distribution.html、mileage_price.html,不要用 test1.html。文档说明里会提到如何批量导出这些图表方便论文插图使用,答辩前把这些 HTML 放到一个 charts 目录里统一管理即可。

5. 避坑与排查:爬虫毕设最容易翻车的五个场景

5.1 页面结构一变,选择器全部失效

现象:昨天跑得好好的脚本,今天突然解析不到数据,items 列表为空,入库条数为 0。

原因:绝大多数二手车平台前端会不定期改版,CSS 类名、HTML 结构或接口返回字段都可能调整。硬编码选择器的脚本对这类变化最敏感。

解决:把页面选择器集中到配置文件里,而不是散落在各函数中。一旦抓不到数据,先打印一条原始 HTML 定位最近的稳定父节点,再从父节点往下逐级选择。建议给 parse_list_page 加一个 if not items 的日志告警,第一时间暴露问题。

5.2 请求被反爬拦截:状态码 200 但内容为空

现象:resp.status_code 返回 200,但 text 里没有车源列表,反而出现验证码页面提示。

原因:平台用 JS 或后端校验了请求头特征,单纯的 200 不代表拿到真实页面。requests 的默认 headers 太干净,缺少浏览器指纹。

解决:先从浏览器开发者工具里复制真实的请求头,把 Accept、Sec-Fetch-Mode 等字段一并加上。再用 session.get 前先访问一次首页获取 cookie。不要用高并发,单线程加 0.5~2 秒的随机延时最稳。

5.3 价格字段带单位混入计算:均值被拉高几倍

现象:运行完统计脚本,均价显示二十多万,明显偏高。

原因:页面上既有“5.30 万”也有“8.80 万”,clean_price 函数里没用 replace 清理字符串就用 float 强转,直接把“10.00 万”当作 10 元处理,再把整列相加。

解决:清洗阶段严格按先转字符串、再正则提取数字、最后换算单位的顺序执行。正则以“万”为分界,分别乘以 10000 和 1。清洗后检查 price 列的最小值和最大值,确保区间符合业务常识。

5.4 数据库写入乱码:中文全变问号

现象:MySQL 里 title 字段显示为“????”。

原因:建表字符集不是 utf8mb4,或者连接串没指定 charset。连接串缺参时 pymysql 默认用 latin1 连接,写入中文必然丢失。

解决:重建表时用 ENGINE=InnoDB DEFAULT CHARSET=utf8mb4,连接参数里显式写 charset="utf8mb4"。已经乱掉的历史数据直接 DELETE 重抓,不要尝试修补,修补成本比重跑一遍还高。

5.5 采集任务跑到一半中断:断点续爬的设计

现象:脚本跑到第 42 页时网络超时崩溃,前 41 页的数据没入库,重跑一遍要等十几分钟。

原因:数据没有边爬边存,而是在全部抓完后再统一入库。一旦中间遇到未捕获异常,内存里的数据全部丢失。

解决:改成边爬边入库,每解析 10 页就提交一次。再引入一个简单的进度文件记录当前页码,重跑时读取进度跳过已完成的页。这一步优化在答辩时也算一个技术亮点,描述为“支持断点续爬的采集机制”。

6. 换个方向继续用:把单次采集升级成定时增量任务

这套项目不只是跑一次就结束的演示代码。我的习惯是拿到源码后先不改动跑通一遍,确认数据流没问题,再想两个问题:这个代码的边界在哪?换个数据源要改哪些地方?

先说第一个边界:单次抓取只能拿到当前时刻在售的车源,无法反映价格随时间的变化。你可以在原脚本外面套一个循环调度的壳。常见的做法是写一个 run_daily.py,里面用 schedule 库或者直接 while True + time.sleep(12h) 来触发 crawl_all。每次抓完数据后,在 crawler_log 表里插入一条任务记录,记录本次抓取的车源总数、耗时、成功页数。连续跑两周后,你就能拿到一组纵向数据——同一辆车在不同时间点的标价变化,这在面试里能讲出的东西比静态分布图多得多。

第二个思路是换数据源适配。这个源码里的选择器、字段映射、清洗逻辑都是围绕某个具体平台写的,但函数分层是通用的。你要换一个平台,只需要重写 parse_list_page 函数里的 select 选择器,和 clean_dataframe 里的价格单位。其他 session 管理、断点续爬、入库逻辑不用动。经验值供参考:一个静态页面的新平台适配,大约两小时内能完成。

第三件事和数据分析方向有关。有了价格、里程、上牌年份、变速箱和品牌这五个字段后,可以做简单的价格推算模型。用 pandas 做特征工程后,跑一个回归模型预测二手车的合理价格区间,再和实际标价对比,能筛出“异常标价”车源。这个方向并不需要复杂的机器学习知识,线性回归就能用,但会显著提升项目深度,答辩时问“你有什么可扩展的方向”时,这是个很好的切入点。

这个项目真正适合的场景,是你要一个能跑通、能讲清、能改动、能出数据的完整闭环。所以当你拿到源码时,建议先按文档把数据库导进去,跑一次爬虫任务,再用自带的图表脚本出一份报告——完成一次端到端验证后再开始改代码。从那以后,我每次拿到这类毕设项目,都强制自己先走一遍完整体验,把数据流里的每一步在文档里画出来,再动手改。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 9:47:37

MATLAB滑动窗口函数windowSliding的工程实践指南

1. 滑动窗口问题的痛点与解决方案 作为一名长期与时间序列数据打交道的工程师,我深知滑动窗口处理在机器学习预处理环节中的重要性。每次看到同事们在Python里用for循环手动切分数据,或者为了处理边界条件而抓耳挠腮时,我都忍不住想——难道就…

作者头像 李华
网站建设 2026/10/14 9:47:33

Win10开启OpenSSH Server:Linux与Windows文件互传实战指南

你有没有遇到过这种场景:公司的文件服务器是Linux,但同事们的办公机全是Windows。每次想从一台Win10机器上拉个报表到服务器,或者把服务器上的安装包推到Windows机器上,都得靠U盘、网盘或者临时搭个SMB共享来回折腾。其实Win10自带…

作者头像 李华
网站建设 2026/10/14 9:47:18

二层RSTP防环技术全解析:从原理到实战排障

干网络这行,整天说“防环”,其实很多人对二层RSTP的理解只停留在“配个STP就行”。真正遇到环网事故时,广播帧在交换机之间来回兜圈,几分钟就能让设备CPU飙到100%,全网时断时通。我自己处理过不少类似故障,…

作者头像 李华
网站建设 2026/10/14 9:47:09

SpringBoot+Vue+MySQL构建电商系统全栈开发实践

1. 项目概述这个基于SpringBootVueMySQL的在线商城系统是我去年指导的一个毕业设计项目,经过三个月的开发和优化,最终实现了一个功能完善、性能稳定的电商平台。作为一个完整的全栈项目,它涵盖了从用户注册登录到商品展示、购物车管理、订单处…

作者头像 李华
网站建设 2026/10/14 9:47:07

企业级API权限开放平台核心功能与安全实践

1. 平台定位与核心价值解析 Surpass API权限开放平台是企业级接口管理和分发的专业解决方案,其核心价值在于构建标准化、安全可控的API服务生态。我在实际企业数字化转型项目中多次采用该平台,发现它特别适合解决以下三类痛点: 权限管控颗粒…

作者头像 李华