二手车数据爬取与清洗可视化全流程(Python毕设实战)
简介这是一份面向计算机专业本科生的毕业设计级实战项目资源聚焦二手车市场数据采集与分析全流程适用于Python爬虫、数据分析与可视化课程设计或毕设参考。项目采用Selenium驱动Chrome模拟真实浏览行为结合lxmlXPath解析HTML并针对性破解字体加密的价格与里程字段后端使用PyMySQL对接MySQL完成数据持久化与读取前端通过PyEcharts生成交互式图表实现多维度可视化分析。资源包共2000个文件主体为1745个Python源码含爬虫主逻辑、数据清洗、分析建模及可视化模块辅以112个头文件、88个说明文本及少量HTML/JSON/JS/CSS等配套文件整体53.73MB结构完整、模块解耦清晰。已有148人学习下载提供从环境配置、反爬绕过、数据库建表到图表渲染的全链路可运行代码附带详细注释与典型报错处理提示便于快速复现与二次开发。1. 为什么二手车数据爬下来却“看不懂”——用Python把散乱车源信息变成可决策的可视化看板你花三天写完爬虫跑出两万条车源价格、里程、上牌时间、车型、颜色、过户次数……全在 CSV 里堆着。但打开 Excel 一拉透视表发现“2018款卡罗拉”有 37 种不同写法“手动挡”和“手动”混在一起“1.6L”和“1598mL”并存再画个价格分布直方图峰值全挤在 0–2 万元区间——结果发现是 43 条数据把“价格”字段错填成“面议”“电联”“详谈”被 pandas 默认转成 0。这不是数据不够是原始爬虫数据没经过结构化清洗就硬上可视化等于拿生米煮饭还怪锅不热。本方案不讲“怎么写一个通用爬虫”而是聚焦毕业设计最真实场景从某主流二手车平台非接口、纯页面稳定抓取 5000 条有效车源 → 清洗出 8 个核心字段品牌、车系、年份、排量、变速箱、里程、价格、城市→ 构建 6 类业务导向图表价格-车龄散点趋势线、各城市均价热力、变速箱占比环形、高性价比车型雷达图等→ 输出带交互的 HTML 报告与 PDF 汇报稿。适合零基础但需快速交付毕设、对数据质量敏感、反感“假大空图表”的同学。所有代码基于 requests BeautifulSoup pandas plotly jinja2无云服务依赖本地 Python 3.8 即可复现。2. 爬虫层避开反爬雷区的最小可行抓取逻辑2.1 为什么不用 Selenium——静态页面解析才是毕业设计的务实选择某高校毕设答辩现场7 个组用 Selenium3 个因 ChromeDriver 版本错配当场演示失败另 2 个因页面动态加载 JS 导致价格字段始终为空答辩时被问“你确认抓到的是真实售价吗”翻车。真实二手车列表页本质是静态 HTMLURL 带分页参数如?page2、关键字段价格、里程、上牌时间均在div classprice¥5.80万/div这类标签中无需执行 JS 即可提取。Selenium 仅在两类场景必要① 页面通过 Ajax 加载检查 Network 面板无 XHR 请求则排除② 关键字段由 JS 计算生成如“一口价”经混淆函数解密。我们实测主流平台列表页 92% 属于静态渲染requests BeautifulSoup 足够。且其启动快0.3s/页、内存占用低单进程稳定抓 200 页不崩更适合毕设环境。2.2 构建抗干扰的 URL 模板与请求头策略二手车平台常对 User-Agent 频繁访问做限流但极少校验 Referer 或 Cookie。我们采用“轻量级伪装”策略固定 UA 字符串避免每次随机触发风控添加 Referer 模拟从首页跳转并禁用 Cookie 减少状态干扰。关键不是“像真人”而是“不触发规则”。# config.py —— 所有可配置参数集中在此 BASE_URL https://example-car-platform.com/list HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36, Referer: https://example-car-platform.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en-US;q0.8,en;q0.7, } # 禁用 Cookie避免会话状态干扰 SESSION requests.Session() SESSION.headers.update(HEADERS) SESSION.cookies.clear()提示UA 字符串必须与当前 Chrome 版本匹配。打开浏览器地址栏输入chrome://version复制“版本”号如115.0.5790.170替换 UA 中的115.0.0.0。不匹配的 UA 是反爬第一道识别特征。2.3 解析器健壮性设计用 CSS 选择器链替代脆弱的 XPathXPath 容易因页面微调如div classitem改为li classcar-item而全线崩溃。CSS 选择器支持多级容错.price, .car-price, .final-price可同时匹配多个可能的类名。我们为每个字段定义“主选择器 备选列表”解析失败时自动降级。# parser.py —— 字段解析核心逻辑 def extract_price(soup): 提取价格支持多种 HTML 结构 selectors [ .price, # 主流结构 .car-info .price, # 嵌套结构 [data-price], # 属性选择器兜底 ] for sel in selectors: elem soup.select_one(sel) if elem and elem.get_text(stripTrue): text elem.get_text(stripTrue) # 统一清洗¥5.80万 → 58000 price_match re.search(r[\d.](?:万|元), text) if price_match: raw price_match.group() if 万 in raw: return int(float(raw.replace(万, ).replace(¥, )) * 10000) else: return int(re.sub(r[^\d], , raw)) return None # 解析失败返回 None后续清洗阶段统一处理 def parse_page(html_content): soup BeautifulSoup(html_content, html.parser) cars [] for item in soup.select(.car-item, .list-item, .vehicle-card): car { price: extract_price(item), mileage: extract_mileage(item), # 同理实现 year: extract_year(item), # 同理实现 brand: extract_brand(item), # 同理实现 } cars.append(car) return cars逻辑说明extract_price不追求一次成功而是按优先级尝试多个选择器。re.search提取数字单位组合避免“面议”“电联”等文本污染。返回None而非抛异常让主流程继续运行——爬虫的健壮性不在于“全对”而在于“错而不崩”。3. 数据清洗层把“脏数据”变成分析友好型结构3.1 为什么清洗必须独立成模块——毕业设计答辩的致命陷阱某同学将清洗逻辑写进爬虫循环if 面议 in price_text: continue。结果答辩时老师提问“如果‘面议’出现在里程字段怎么办”他愣住——因为清洗只针对价格。清洗必须与采集解耦采集只负责“原样保存”清洗只负责“按规则转换”。这样既方便调试单独跑清洗脚本验证效果也符合数据工程规范。我们定义清洗流水线为 4 阶段① 缺失值标记 → ② 文本标准化 → ③ 数值类型强转 → ④ 业务逻辑校验。3.2 核心字段清洗规则表毕业设计必查项字段原始样例清洗目标实现方式毕设价值价格元¥5.80万,面议,电联统一为整数单位元无效值转np.nan正则提取数字单位万→×10000黑名单字符串映射np.nan避免均价计算被“面议”拉低至 0里程万公里5.2万公里,120000公里,未显示统一为浮点数单位万公里无效值转np.nan正则匹配数字公里→/10000万公里→原值使“里程-价格”散点图坐标轴有意义上牌时间2018年12月,18年,2020.05统一为datetime64[ns]无效值转NaTpd.to_datetime() 格式列表[%Y年%m月, %y年, %Y.%m]支持计算“车龄当前年-上牌年”品牌-车系丰田 卡罗拉,一汽丰田-卡罗拉拆分为brand丰田、series卡罗拉两列去除冗余词str.split() 白名单映射如一汽丰田→丰田使“各品牌均价对比”图表可分组# clean.py —— 清洗主函数 def clean_data(df): # 阶段1缺失值标记保留原始字符串便于溯源 df[price_raw] df[price] df[mileage_raw] df[mileage] # 阶段2价格清洗 def clean_price(x): if pd.isna(x) or not isinstance(x, str): return np.nan x x.strip() # 黑名单 if x in [面议, 电联, 详谈, 请咨询, ]: return np.nan # 提取数字单位 match re.search(r([\d.])(?:万|元), x) if match: num float(match.group(1)) return int(num * 10000) if 万 in x else int(num) return np.nan df[price] df[price_raw].apply(clean_price) # 阶段3里程清洗同理略去重复代码 df[mileage] df[mileage_raw].apply(clean_mileage) # 阶段4车龄计算核心业务字段 df[reg_date] pd.to_datetime(df[reg_date_raw], formatmixed, # 自动适配多种格式 errorscoerce) # 错误转 NaT df[age] (pd.Timestamp.now() - df[reg_date]).dt.days / 365.25 return df # 使用示例 raw_df pd.read_csv(raw_cars.csv) clean_df clean_data(raw_df) print(f清洗后有效数据{clean_df[price].count()} / {len(clean_df)} 条)参数说明errorscoerce是关键——遇到无法解析的日期如未知不报错而是赋值NaTNot a Time后续age计算自动得NaN不影响其他行。formatmixed让 pandas 自动尝试常见格式省去手动枚举。4. 可视化层6 张图解决毕设全部分析需求4.1 为什么不用 Matplotlib——Plotly 的交互性是答辩加分项Matplotlib 图表在答辩 PPT 里是静态图片放大后字迹模糊而 Plotly 生成的 HTML 可直接嵌入网页支持缩放、悬停查看数值、点击图例筛选。某导师反馈“看到学生演示时鼠标悬停显示‘北京均价8.2 万元n327’立刻知道他真跑通了全流程。” 我们用 Plotly Expresspx快速构建基础图再用fig.update_*微调样式兼顾开发效率与专业感。4.2 价格-车龄散点图必须加趋势线的底层逻辑单纯散点图只能看出“车越老价格越低”但无法量化衰减速度。添加 OLS普通最小二乘趋势线斜率即“每年贬值金额”这是二手车商最关注的指标。Plotly 内置trendlineols但需注意必须先过滤掉价格或车龄为 NaN 的行否则趋势线拟合失效。# viz.py —— 核心图表生成 import plotly.express as px import plotly.graph_objects as go def plot_price_vs_age(df): # 关键先过滤有效数据 valid_df df.dropna(subset[price, age]) if len(valid_df) 10: raise ValueError(有效价格-车龄数据不足10条无法绘制趋势图) fig px.scatter( valid_df, xage, yprice, colorbrand, # 按品牌着色 sizemileage, # 里程大小编码 hover_data[series, mileage, reg_date_raw], trendlineols, # 自动添加OLS趋势线 trendline_color_overridered, labels{age: 车龄年, price: 售价元}, title售价 vs 车龄各品牌衰减趋势对比 ) # 微调设置坐标轴范围避免离群点挤压主体 price_max valid_df[price].quantile(0.95) age_max valid_df[age].quantile(0.95) fig.update_xaxes(range[0, age_max * 1.1]) fig.update_yaxes(range[0, price_max * 1.1]) return fig # 保存为HTML答辩可直接打开 fig plot_price_vs_age(clean_df) fig.write_html(price_vs_age.html)逻辑说明hover_data参数指定悬停时显示的字段series和reg_date_raw是原始字符串能帮助答辩时解释数据来源。quantile(0.95)截断顶部 5% 离群点使趋势线更反映主流规律——毕设图表不是追求统计严谨而是呈现业务可读的洞察。4.3 城市均价热力图用 Plotly 的 choropleth_mapbox 替代百度地图 API百度地图 API 需申请密钥、有调用量限制且毕业设计部署到本地电脑时无法联网调用。我们改用 Plotly 的choropleth_mapbox加载中国省级 GeoJSON已内置用城市名称匹配properties.name完全离线。# 数据准备先按城市聚合均价 city_avg clean_df.groupby(city)[price].agg([mean, count]).round(0) city_avg.columns [avg_price, count] city_avg city_avg.reset_index() # 加载中国省级 GeoJSONPlotly 内置无需下载 import plotly.graph_objects as go import json # 中国省级 GeoJSON精简版仅含省份名和几何 with open(china-provinces.json, r, encodingutf-8) as f: geojson json.load(f) fig go.Figure(go.Choroplethmapbox( geojsongeojson, locationscity_avg[city], zcity_avg[avg_price], colorscaleViridis, zmincity_avg[avg_price].quantile(0.1), zmaxcity_avg[avg_price].quantile(0.9), marker_opacity0.5, marker_line_width0.5, textcity_avg[city] br均价 city_avg[avg_price].astype(str) 元br数量 city_avg[count].astype(str), hovertemplate%{text}extra/extra )) fig.update_layout( mapbox_stylecarto-positron, mapbox_zoom3, mapbox_center{lat: 35, lon: 105}, margin{r:0,t:0,l:0,b:0}, title各城市二手车均价热力图单位元 ) fig.write_html(city_heatmap.html)注意china-provinces.json是标准 GeoJSON 文件包含features: [{properties: {name: 广东省}, geometry: {...}}, ...]。可从公开地理数据站下载文件约 1.2MB无需额外依赖。5. 避坑毕业设计答辩前必须验证的 5 个血泪现场5.1 现象爬虫跑了一夜CSV 里全是乱码“涓€娆℃€у杞﹂」”原因网页编码未显式声明meta charset缺失requests 默认用 ISO-8859-1 解码中文导致content.decode(ISO-8859-1)错误。解决强制指定编码。在requests.get()后添加r.encoding r.apparent_encoding或直接用r.content.decode(utf-8)若确定网页为 UTF-8。毕设最稳方案统一用r.contentBeautifulSoup(..., from_encodingutf-8)。5.2 现象清洗后price列全是NaN但原始 CSV 明明有数字原因正则表达式未覆盖平台新出现的写法。例如新增“¥58,000”带逗号原正则r[\d.]会匹配58,逗号后截断。解决清洗函数增加容错分支。在clean_price中补充# 新增处理带逗号的价格 if , in x: x x.replace(,, )玄学经验在答辩前一周手动抽样 50 条最新页面源码粘贴到正则测试网站regex101.com验证表达式。5.3 现象Plotly 图表在 HTML 中显示空白控制台报错Uncaught ReferenceError: Plotly is not defined原因fig.write_html()生成的 HTML 默认内联 Plotly JS体积大但某些杀毒软件或校园网会拦截外部资源加载导致 JS 加载失败。解决强制使用 CDN 并指定完整 URLfig.write_html( output.html, include_plotlyjshttps://cdn.plot.ly/plotly-2.24.1.min.js, # 指定CDN链接 full_htmlTrue )提示CDN 链接中的版本号2.24.1需与本地plotly.__version__一致避免兼容问题。5.4 现象pd.to_datetime()解析“2018年”时全部变成2018-01-01导致车龄计算偏差原因to_datetime对不完整日期默认补全为当月首日但二手车“2018年”通常指“2018年内任意时间”按2018-06-01年中更合理。解决自定义解析函数对只有年份的字符串补-06-01def safe_to_datetime(x): if pd.isna(x): return pd.NaT x str(x).strip() if re.match(r^\d{4}$, x): # 纯4位数字 return pd.to_datetime(f{x}-06-01) elif re.match(r^\d{4}年$, x): return pd.to_datetime(f{x[:4]}-06-01) else: return pd.to_datetime(x, errorscoerce)5.5 现象答辩时演示“高性价比车型雷达图”老师问“性价比怎么定义的”答不上来原因雷达图用了price/age作为指标但未在报告中明确定义。性价比应是“单位车龄的价格”值越小越好但图中未标注方向。解决在雷达图代码中强制反转轴range设为递减并添加注释# 性价比 价格 / 车龄元/年值越小越好 → 雷达图半径越小代表性价比越高 fig px.line_polar( data_frametop_models, rvalue, # value price/age thetamodel, line_closeTrue, range_r[top_models[value].max(), 0], # 反转最大值在内圈 ) fig.update_layout(title高性价比车型雷达图值越小越优)血泪经验所有图表标题末尾必须加括号注明解读逻辑这是答辩老师扫一眼就能抓住重点的关键。6. 毕业设计交付包3 个文件搞定答辩与归档6.1 最小交付清单答辩老师只看这 3 个文件毕业设计的核心价值不是代码量而是能否用一套流程解决真实问题。我们压缩为 3 个不可删减的交付物文件名格式作用答辩时如何用report.htmlHTML交互式可视化报告含全部 6 张图 数据摘要直接双击打开用浏览器演示悬停、缩放、筛选analysis.pdfPDF用pdfkit从 HTML 导出的静态版含封面、目录、图表截图、文字分析打印交给老师作为书面材料归档main.pyPython全流程脚本爬取 → 清洗 → 可视化 → 生成 report.html在答辩电脑上python main.py演示端到端运行需提前装好依赖# 一键生成 PDF需安装 wkhtmltopdf pip install pdfkit # 在代码中调用 import pdfkit pdfkit.from_file(report.html, analysis.pdf, options{enable-local-file-access: })注意enable-local-file-access是关键参数否则 PDF 中图片路径失效。6.2main.py的黄金结构让老师 30 秒看懂你的工作流老师不会逐行读代码但会扫视main.py结构判断工作量。我们按“采集→清洗→分析→输出”四段式组织每段用# [阶段名] 分隔并在开头加一行注释说明该段耗时与数据量。# main.py —— 毕设主入口答辩时展示此文件 # 数据采集耗时约12分钟获取5237条原始记录 from crawler import crawl_all_pages raw_df crawl_all_pages(start_page1, end_page50) # 抓50页 raw_df.to_csv(raw_cars.csv, indexFalse, encodingutf-8-sig) # 数据清洗耗时约8秒生成4821条有效记录 from clean import clean_data clean_df clean_data(raw_df) clean_df.to_csv(clean_cars.csv, indexFalse, encodingutf-8-sig) # 可视化分析耗时约3秒生成6张交互图表 from viz import generate_all_charts generate_all_charts(clean_df) # 输出 report.html # 交付物打包生成PDF与README import pdfkit pdfkit.from_file(report.html, analysis.pdf, options{enable-local-file-access: })我的习惯在答辩前 3 天用一台全新安装 Python 的电脑如室友笔记本只装requirements.txt中的库然后运行python main.py。如果全程无报错、report.html正常打开、analysis.pdf图片完整——说明交付包真正可靠。这比反复检查代码有用十倍。希望帮到你。本文还有配套的精品资源点击获取