简介基于Python的二手车爬虫数据可视化分析毕业设计源码面向计算机相关专业学生及爬虫入门者提供一套完整的项目解决方案。程序使用Selenium驱动Google浏览器抓取二手车网站页面借助lxml的etree对象与XPath解析DOM树并针对价格、表显里程等关键字段的字体文件加密进行了处理同时以pyecharts生成可视化图表通过pymysql完成MySQL数据的写入与读取。资源包共2000个文件以1745个Python源码文件为主辅以C头文件、文本说明、HTML页面、JSON配置等整体大小约53.73MB目录结构清晰便于按模块研读。目前已有147人学习下载。从中可以掌握动态网页爬取、加密数据破解、数据入库及可视化展示的完整链路并可直接改造用于其他二手车平台或类似电商网站的分析项目是毕业设计、课程设计与技能提升的实用参考资料。1. 二手车爬虫数据可视化毕设这套源码把反爬、入库、出图串成了一条完整链路用 Python 写爬虫不难难的是拿到一份能直接跑通毕设答辩的完整源码。这套二手车爬虫数据可视化分析设计恰好把最难啃的部分都覆盖了Selenium 驱动谷歌浏览器抓取动态页面lxml 的 etree 对象配合 XPath 解析 DOM 树价格和表显里程遇到字体文件加密需要单独破解数据通过 pymysql 写进 MySQL最后用 pyecharts 生成 Echarts 图表完成可视化展示。简单说就是从采集、清洗、入库到出图一条链路走完不用你再去东拼西凑。适合正在做毕设的本科生也适合想快速搞懂字体反爬破解思路的爬虫从业者。下面我按实际跑通的顺序拆开讲每一步都给你能抄的代码和参数说明。2. 抓取层拆解Selenium 驱动浏览器与 XPath 解析的完整流程2.1 为什么选 Selenium 而不是 Requests很多爬虫入门教程第一步就是 requests.get 拿 HTML再正则抽数据。但那套玩法在二手车网站上基本走不通列表页的车辆数据很多是 JS 动态渲染的直接 requests 拿到的 HTML 里可能只有个空壳连价格标签都找不到。另外页面里嵌了字体加密逻辑需要在真实浏览器环境里执行 JS 才能拿到被字形替换后的文本这个动作 requests 也替代不了。所以这套毕设源码选用 Selenium 驱动谷歌浏览器本质是让浏览器替你把页面渲染完再通过 driver.page_source 把渲染后的 DOM 交出来。代价是速度慢、内存占用高但换来了稳定性和对反爬机制的兼容性。实际跑的时候建议开无头模式既不用盯着浏览器窗口也能减少系统资源开销。要注意的是Selenium 驱动 Chrome 必须有一个对应版本的 chromedriver版本对不上会在启动阶段直接抛异常这个坑后面避坑章专门说。2.2 从启动浏览器到拿到渲染后的 DOM先看我在这套代码里最常用的一段初始化骨架from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from lxml import html # 无头模式配置减少弹窗和资源占用 opts Options() opts.add_argument(--headlessnew) opts.add_argument(--disable-gpu) opts.add_argument(--no-sandbox) opts.add_argument(--disable-blink-featuresAutomationControlled) # 隐藏自动化标识 driver webdriver.Chrome(optionsopts) driver.set_page_load_timeout(20) # 页面加载超时防止慢页面卡死 # 打开二手车列表页示例地址按实际站点替换 driver.get(https://example.com/usedcar/) # 显式等待列表节点出现最多等 15 秒 wait WebDriverWait(driver, 15) wait.until(EC.presence_of_element_located((By.XPATH, //div[contains(class,car-list)]))) # 把渲染完成的 HTML 转成 lxml 的可解析对象 doc html.etree.HTML(driver.page_source)这段代码的逻辑很直白先创建 Chrome 配置项再启动 driver用 get 打开目标页面然后显式等待页面里代表列表的容器节点出现。等这一步通过说明核心数据已经渲染完成再取 page_source 就是完整 DOM。传给 lxml 的 etree.HTML 方法之后后续所有 XPath 查找都在 doc 对象上做。几个参数值得单独说一下。headlessnew 是无头模式的 Chrome 新写法老版本写 headless 也能跑但新写法兼容性更好。set_page_load_timeout 设成 20 秒是为了防止某个慢接口把整个进程挂住超过时间直接抛异常让程序往下走。等待条件里那个 XPath 是按页面结构来的不同站点的列表容器 class 不一样需要按实际页面改。这里强烈建议用显式等待而不是 time.sleep 固定秒数因为慢接口时长不可控睡短了拿不到节点睡长了浪费时间。2.3 XPath 定位二手车关键字段class 命名要按页面改doc 就绪后剩下的就是用 XPath 抽字段。这套代码里车名、价格、里程、地区分别从不同结构的节点里取我把它整理成下面这样# 车名通常包在卡片标题的 a 标签里 names doc.xpath(//div[contains(class,car-title)]/a/text()) # 车源价格注意此时拿到的可能是字体加密后的字符 prices doc.xpath(//span[contains(class,car-price)]/text()) # 表显里程多数藏在 li 或 span 的描述性节点里 mileages doc.xpath(//li[contains(class,car-mileage)]/text()) # 所在地区一般跟随车源卡片和车名同级 regions doc.xpath(//div[contains(class,car-region)]/text()) # 用 zip 合并成一条条记录便于后续清洗入库 records list(zip(names, prices, mileages, regions))XPath 的写法是这套代码最需要按站点定制的地方。contains(class,car-title) 比直接等号匹配更抗干扰因为页面经常会一个节点挂多个 class写成等于容易漏数据。用 text() 取文本时有一个隐蔽问题如果某个字段缺失XPath 返回的是空字符串而不是报错zip 之后可能出现某个字段对不齐的情况所以入库前最好加一层非空过滤。价格和里程这里还藏着一个更深的坑页面上肉眼看到的是「10.80万」但 text() 取出来可能是乱码或特殊字符这正是字体加密的产物。第一次跑通这套流程的人十有八九会在这里卡住看到乱码以为是自己 XPath 写错了。实际不用慌下一章专门讲怎么把这块解密还原。字段解析跑通了后面入库才不会翻车。3. 字体反爬破解价格和表显里程的加密字体还原方法3.1 字体加密的原理先看懂它怎么藏数据二手车网站的价格和表显里程经常走字体反爬。页面源码里价格和里程字段的文本是一堆肉眼不认识的特殊字符但浏览器渲染时通过 CSS 里定义的 font-face 字体文件把特殊字符映射成正常的数字字形。所以你截图看页面数字是正常的用 XPath 的 text() 去拿拿到的却是被替换过的 unicode。这种反爬的本质是「显示层和源码层分离」。服务端把真实数字按某种规则替换成自定义字符编码再附带一个只有服务端知道的字体文件浏览器加载字体后按 cmap 表把字符映射回真实字形。爬虫拿到的页面源码里只有乱码字符想要还原就必须把那个 woff 字体文件下载下来解析它的 cmap 映射表建立「字符 → 数字」的对应关系再拿这个映射去替换 text() 取到的内容。解决思路分三步找到字体文件地址并下载用 fontTools 解析字体提取每个字符对应的字形名称和编码手动或自动建立字形到真实数字的映射表。这套流程在二手车网站上尤其常见因为价格和里程是用户最敏感的信息平台方愿意用反爬手段保护这些核心字段。3.2 下载字体文件并解析 cmap 映射字体文件的地址通常藏在页面的 CSS 或内联样式里通过 font-face 声明。常见做法是先用 XPath 或正则把 CSS 文本捞出来再找出 woff 或 ttf 后缀的 URL然后 requests 下载到本地。下面是这套代码里的核心解析段import requests from fontTools.ttLib import TTFont # 从页面 CSS 里提取字体文件地址常见为 woff 或 ttf font_url https://example.com/static/fonts/car-number.woff r requests.get(font_url) with open(car-number.woff, wb) as f: f.write(r.content) # 解析字体文件 font TTFont(car-number.woff) cmap font.getBestCmap() # 手动对照字形名与数字后建立映射字典 glyph_map { glyph1: 0, glyph2: 1, glyph3: 2, glyph4: 3, glyph5: 4, glyph6: 5, glyph7: 6, glyph8: 7, glyph9: 8, glyph10: 9, }cmap 是字体文件里「unicode 编码 → 字形名称」的映射表getBestCmap() 会优先返回适合程序解析的那个子表。拿到 cmap 后你就能把一个字符的编码转成字形名比如 chr(0xE001) 可能对应字形名 glyph4而 glyph4 在页面上渲染出来是数字 3。glyph_map 里存的就是「字形名 → 真实数字」的对应关系。这里有个很关键的点字体映射字典不要写死。同一个站点的字体文件可能会定期重新生成这次 glyph4 是数字 3下次可能变成数字 8。我一般会在每次爬取时都重新下载字体文件再解析而不是复用上一次的 woff。至于映射关系怎么确认常见做法是先打开一个已知价格的详情页找一条「页面显示价格 vs 源码文本」都能拿到的记录人工核对三四个字符后把映射补全之后就能自动化了。提示字体文件体积通常只有几十 KB下载成本很低没必要为省这一步去缓存每次都现拉现解析最稳。3.3 把加密文本还原成真实数字拿到 cmap 和 glyph_map 之后剩下就是写一个替换函数把取到的乱码文本逐字还原def decode_number(encrypted_text, cmap, glyph_map): result [] for ch in encrypted_text: # 先把字符转成 unicode 码点再查字形名 glyph_name cmap.get(ord(ch), None) if glyph_name in glyph_map: result.append(glyph_map[glyph_name]) else: result.append(ch) # 非加密字符比如小数点、单位原样保留 return .join(result) # 使用示例 raw_price prices[0] # 例从 text() 取到的乱码 real_price decode_number(raw_price, cmap, glyph_map) print(raw_price, real_price) # 预期输出乱码 → 10.80函数逻辑很简单遍历加密文本的每个字符用 ord(ch) 拿到它的 unicode 码点再用 cmap.get 查这个码点对应的字形名如果字形名在我们的 glyph_map 里就换成真实数字否则就原样保留。这样做的好处是小数点、万、公里这些非加密字符不会被误伤能直接跟着文本一起保留下来。常见的翻车点是忽略了小数点和单位。有些站点只对数字做字体替换小数点里的点还是普通字符这种情况函数能正常处理但有些站点的加密范围包含了小数点那你的 glyph_map 里还需要额外加一条「glyph_dot: .」的映射。还有一个细节是mileage 的文本长度不固定比如「5.6万公里」加密后可能变成 5 个字符解密后长度会还原成和肉眼一致的文本你可以把解密结果和页面截图对一眼核验映射表有没有建全。4. 数据落库与分析pymysql 读写 MySQL 与 pyecharts 可视化配置4.1 建表与写入价格字段别用 varchar 存爬下来的数据清洗完后下一步就是写进 MySQL。这套代码用的是 pymysql操作方式和 mysql-connector 差不多但 pymysql 是纯 Python 实现部署环境里装起来省事兼容性也更好。先看建表语句CREATE TABLE used_car ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(120) NOT NULL, price DECIMAL(10,2) COMMENT 价格单位万, mileage VARCHAR(50), region VARCHAR(20) DEFAULT , crawl_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;price 字段一定要用 DECIMAL 而不是 VARCHAR。价格解密后是「10.80」这样的字符串如果为了省事存成 VARCHAR后续做价格区间统计时还得 CAST 转类型而且很容易因为「10.80」和「10.8」这种格式差异导致聚合出错。DECIMAL(10,2) 的意思是总长 10 位、小数位 2 位存储万元级别的价格完全够用。写入逻辑用参数化 insertimport pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, password123456, databasecar_spider, charsetutf8mb4 ) cursor conn.cursor() insert_sql INSERT INTO used_car (title, price, mileage, region, crawl_time) VALUES (%s, %s, %s, %s, NOW()) for title, price, mileage, region in records: # 这里把解密后的价格字符串转成 float再传给 DECIMAL 字段 cursor.execute(insert_sql, (title, float(price), mileage, region)) conn.commit() cursor.close() conn.close()pymysql 的占位符是 %s不管字段类型是字符串还是数字都用 %s 占位然后真正的值放在第二个参数 tuple 里。这里注意不要自己拼 SQL 字符串一方面防注入另一方面也避免引号和转义的边界问题。crawl_time 直接用 NOW() 让 MySQL 写时间省得 Python 这边再处理时区。4.2 从 MySQL 读数据生成 pyecharts 图表数据入库不是终点毕设答辩要看图表。pyecharts 是这套代码做可视化的核心库它把 Echarts 的配置封装成了 Python 对象渲染出来是一个独立的 HTML 文件浏览器打开就能看演示的时候很方便。下面这段是统计各地区车源数量的柱状图from pyecharts.charts import Bar, Pie from pyecharts import options as opts import pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, password123456, databasecar_spider, charsetutf8mb4 ) cursor conn.cursor() # 按地区聚合统计车源数量取前 15 个地区 cursor.execute( SELECT region, COUNT(*) AS cnt FROM used_car GROUP BY region ORDER BY cnt DESC LIMIT 15 ) rows cursor.fetchall() bar ( Bar() .add_xaxis([row[0] for row in rows]) .add_yaxis(车源数量, [row[1] for row in rows]) .set_global_opts( title_optsopts.TitleOpts(title各地区二手车源数量分布), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate15)), yaxis_optsopts.AxisOpts(name数量) ) ) bar.render(region_bar.html)这一段把 SQL 聚合结果直接喂给了 Bar 图。add_xaxis 接收的是地区名列表add_yaxis 接收的是数量列表两个列表的顺序必须一一对应。axislabel_opts 里的 rotate15 很实用地区名长了以后横轴标签会互相遮挡旋转 15 度基本能解决。render 方法会生成一个完整的 HTML 文件里面已经内嵌了 Echarts 的 JS 资源双击就能打开看效果。除了地区分布价格区间分布也是二手车可视化里很常问到的图。做法是用 SQL 按价格分桶SELECT CASE WHEN price 5 THEN 0-5万 WHEN price 10 THEN 5-10万 WHEN price 20 THEN 10-20万 ELSE 20万以上 END AS price_range, COUNT(*) AS cnt FROM used_car GROUP BY price_range;这个思路比在 Python 里对每一行做判断要省事SQL 负责分桶Python 只负责取结果画出来的饼图直接展示各价格区间的占比。数据量小的时候两者差别不大但数据量过万后SQL 聚合比 Python 循环快一个量级答辩时如果被问到性能问题这个细节能加分。4.3 常用图表参数与输出说明pyecharts 的配置项很多刚上手容易懵。实际做毕设用到的核心就那几个我整理成下表方便对照配置项作用常用写法踩坑点TitleOpts设置图表标题title_optsopts.TitleOpts(title...)不设置则图表没标题答辩时要手动说明TooltipOpts鼠标悬停提示框tooltip_optsopts.TooltipOpts(triggeraxis)柱状图建议 triggeraxis散点用 itemLegendOpts图例开关legend_optsopts.LegendOpts(pos_top5%)图例默认在顶部多个数据系列时要注意区分AxisOptsX/Y 轴配置xaxis_optsopts.AxisOpts(name地区)轴名称不写图表只有数值没有维度说明LabelOpts数据标签label_optsopts.LabelOpts(formatter{c} 辆)饼图里的 formatter 经常被忽略默认只显示数字实际跑的时候我习惯每张图渲染成一个独立 HTML 文件文件名用图表的含义命名比如 price_range_pie.html、region_bar.html。答辩演示时按顺序打开比在代码里启动本地服务更省心也不会因为端口占用或静态资源路径问题翻车。如果想要一张大屏把多张图拼在一起可以再研究 pyecharts 的 Page 和 Grid 组件但毕设一般做到单图输出就够了。5. 避坑指南跑通这套毕设源码前的五个高频翻车点5.1 启动阶段浏览器驱动版本不匹配现象Chrome 一启动就抛 session 相关异常日志里提示 chromedriver 版本不支持或者直接报错无法创建 driver 对象。原因chromedriver 必须和本机 Chrome 主版本号严格匹配。Chrome 自动更新后版本变新旧 chromedriver 就罢工了。这是 Selenium 体系里最经典的环境问题和代码本身无关。解决先用 chrome://version 或命令行确认浏览器版本再去下载对应版本的 chromedriver。从 Chrome 115 开始如果你的 selenium 版本足够新可以直接用 Selenium Manager 自动管理驱动不用手动下载老版本就老老实实把 chromedriver 的路径配到环境变量里。顺手把驱动路径写死到脚本开头方便排查。5.2 数据解析阶段字体解密后数字错乱现象解密后价格从「10.80」变成「1080」或者「万」字和数字黏在一起看起来像 10.80 被放大了一百倍。原因字体替换时小数点或单位字符也参与了加密而你的 glyph_map 里只建了 0-9 的映射没有把小数点的字形映射进去导致小数点被丢弃数字拼接后自然就错位了。解决解密函数里一定要保留非数字字符。我的做法是先打印一条原始乱码文本和页面截图做对照确认哪些字符是数字、哪些是小数点、哪些是单位文字再把小数点加进 glyph_map。另一个技巧是解密后做一次 sanity check如果解密结果长度和肉眼看到的不一致基本就是映射表漏了字符。5.3 入库阶段MySQL 中文乱码现象地区、车名写进数据库后全是问号或者读出来显示成乱码。原因连接参数没设置 charset或者建表时没指定 utf8mb4。pymysql 默认用的字符集可能和你库里的表不一致中文在传输过程中就被转坏了。解决连接参数里加 charsetutf8mb4同时建表语句带 DEFAULT CHARSETutf8mb4。如果表已经建好可以 ALTER TABLE used_car CONVERT TO CHARACTER SET utf8mb4。遇到老项目还要检查 MySQL 服务端的 character_set_server 配置光改 Python 这边不够。5.4 翻页抓取节点失效与状态崩溃现象爬第一页正常翻到第三四页时 Selenium 抛 StaleElementReferenceException或者元素明明在页面上却定位不到。原因列表页 DOM 更新后之前缓存的 WebElement 对象引用已经失效。Selenium 里每个 WebElement 绑定的是页面里的一个具体节点DOM 一变旧引用就不能再用。解决每翻一页重新用 XPath 获取节点不要重用旧元素。解析逻辑尽量写成独立函数输入 driver输出解析结果翻页后重新调用。对于偶发的节点定位失败在外面包一层 try except重试一次就能绕过。这个重试机制在反爬环境里尤其重要。5.5 反爬检测无头模式被识别现象无头模式能打开页面但列表数据为空或者弹出一个滑块验证码怎么点都过不去。原因网站通过 navigator.webdriver 等特征识别出这是自动化浏览器直接返回空数据或触发验证。无头模式这类特征更明显被识别概率比有头模式高不少。解决加 --disable-blink-featuresAutomationControlled 隐藏自动化标识同时配合真实 User-Agent。还不行就改成有头模式用小窗口跑。注意做爬虫要遵守网站 robots 协议和法律法规这套代码只建议用于学习研究不要对线上站点做高频抓取。6. 进阶验证从单页抓取到全站采集的稳定化改造单页能跑通只是第一步毕设里通常要展示一套完整的数据采集流程那就得把单页逻辑封装成可复用的函数再加上分页、重试、去重和延时。这里我给一个改造后的骨架import time import random def parse_and_save(doc): # 把前面章节的 XPath 解析和 pymysql 入库逻辑封装进来 pass def crawl_page(driver, page_num): url fhttps://example.com/usedcar/p{page_num}/ driver.get(url) doc html.etree.HTML(driver.page_source) records zip( doc.xpath(//div[contains(class,car-title)]/a/text()), doc.xpath(//span[contains(class,car-price)]/text()), doc.xpath(//li[contains(class,car-mileage)]/text()), ) parse_and_save(records) def run(): for page in range(1, 51): try: crawl_page(driver, page) time.sleep(random.uniform(2, 6)) # 随机延时降低请求频率 except Exception as e: print(f第{page}页失败: {e}) continue # 失败页跳过不中断整个任务这个改造里有两个验证要点。第一个是去重用车辆的标题加价格作为唯一标识在 insert 前先查一下是否存在避免重复页面重复入库第二个是数量核对跑完后用 SQL 数一下总行数再和页面提示的车源总量对比差别在正常范围内说明抓取没漏。随机延时不是玄学是给目标服务器的基本礼貌频率太高容易被封。从那以后我每次跑爬虫前都强制走一遍「手动开页面→看 DOM→写 XPath→小批量验证」四步确认映射和字段没问题再放全量这套代码的打开方式我建议你也照着这个顺序来。希望帮到你。本文还有配套的精品资源点击获取