)
简介这是一份面向计算机相关专业学生期末大作业、课程设计与毕业设计的Python爬虫与可视化项目针对汽车之家数据采集与展示需求提供完整可运行的源码和已采集好的数据文件。作者为chengxuyuanlaow经导师指导并获得99分高分代码结构清晰、运行门槛低适合初学者直接上手。资源压缩包共15个文件以xml配置、py源码、csv数据及md说明文档为主同时包含png效果图与pyc缓存文件等整体仅312KB便于下载与本地部署。已有196人学习下载说明其受到不少同类学习者认可。除爬虫逻辑与可视化脚本外资料中还附带项目配置模块、数据表与README说明可直接对照运行并二次修改节省从零搭建的时间。尤其适合需要完成期末大作业或项目实战练习的人群可作为选题参考、框架模板或调试排错依据。1. 这不只是一个爬虫作业汽车之家数据的完整链路期末大作业里最忙的一类需求就是“爬取数据可视化”。标题里出现“源代码全部数据”六个字整个事情就变成了交出一份从采集、清洗、存储到出图全流程的完整工程老师拿到手之后要能按文档跑通看到脚本、数据文件和图表都真实存在。做这类项目时我会先立一个目标爬虫只是起点真正占时间的是落库之后的清洗和可视化选型。下面按大多数人做这份大作业最稳的路线走——先分析汽车之家的页面结构和数据接口用 requestsBeautifulSoup 拿数据把“全部数据”用 CSV/SQLite 落地再用 pyecharts 拼出可交付的可视化页面最后补充源代码组织和答辩排错点。2. 先摸清汽车之家的数据结构和URL规律才是爬虫不翻车的前提2.1 选对抓取对象车型库、口碑、论坛三类的解析难度完全不同汽车之家的数据大致分三块车型库、口碑评价、论坛内容。期末大作业最推荐抓车型库原因是它结构规整——每辆车有固定字段品牌、车系、年款、指导价、发动机、变速箱、车身类型而且价格和配置数据天然适合做后续的分布图和对比图。口碑板块大多靠异步接口加载前端页面是动态渲染的直接用 requests 拿 HTML 往往是空的论坛评论则需要处理翻页、验证码和用户信息对期末项目来说性价比太低。数据模块主要内容爬取难度车型库品牌、价格、配置低以静态 HTML 为主口碑评价用户评价、打分中多为异步 JSON 接口论坛帖子、评论高翻页和反爬较多从列表页到详情页URL 设计上有两处值得反复推敲一是分页参数不同栏目的参数名可能叫 page、p、pageIndex也可能藏在 POST 表单里二是品牌和车系 ID大部分真实 ID 会出现在 URL 的路径或查询参数中是个连续的数字。常见做法是把列表页一页一页地抓从 HTML 里解析出车系 ID 和名称再拼接详情页 URL 去抓对应页面这样不需要额外维护一张车系表也不容易错过新增车型。抓取过程中务必打开浏览器开发者工具F12的 Network 面板逐个请求查看哪些是页面 HTML、哪些是 JSON 接口再决定解析方案。2.2 requests BeautifulSoup 抓列表页的最小脚本如果你只想快速看见数据先跑通这个最小例子确认环境里 requests 和 beautifulsoup4 都装好了。下面这段代码直接请求列表页 HTML用 CSS 选择器提取车系 ID 和车名import requests from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Referer: https://www.autohome.com.cn/car/, Accept-Language: zh-CN,zh;q0.9, } def fetch_list_page(page_no): # 先抓首页再从Network面板里确认分页URL的真实拼接规则 url https://www.autohome.com.cn/car/ if page_no 1: url fhttps://www.autohome.com.cn/car/list_{page_no}.html # 示例规则按实际抓包替换 resp requests.get(url, headersHEADERS, timeout10) resp.encoding gbk # 老页面常用GBK解析结果乱码时优先检查这里 soup BeautifulSoup(resp.text, html.parser) items soup.select(.search-result-list .list dl) for item in items: name_tag item.select_one(dt a) if name_tag is None: continue car_name name_tag.get_text(stripTrue) car_id name_tag.get(href, ).split(/)[-1].split(.)[0] print(page_no, car_id, car_name) if __name__ __main__: fetch_list_page(1)这段代码里有三个关键点。第一是把 requests 封装在函数里方便后面用循环控制抓取范围第二是显式设置resp.encoding汽车之家部分老页面是 GBK 编码不设置的话中文会变成乱码第三是 CSS 选择器.search-result-list .list dl换设备抓的时候要用 DevTools 核对它是否仍然能命中列表标签页面改版后通常只需替换这一行。脚本跑通后把dl里车的价格、排量等信息完整解析出来就完成了第一层数据提取。2.3 反爬的硬门槛UA、Referer、Cookie和请求频率一起配合直接抓汽车之家这样的站点大多数人遇到的第一个报错是403 Forbidden第二个是打印resp.text后发现只有一个验证页面。前者多半是缺少 User-Agent后者是缺少 Referer或者没有带上访问者正常使用的 Cookie。应对起来最稳的做法是用requests.Session()把浏览器里的请求头Cookie一次性塞进 session再保持同一个会话抓取多页这样对服务端而言你是一个连续浏览的普通用户。还有一个参数容易被忽略静默间隔。连续分页抓取时固定间隔会形成规律的请求节奏反过来用随机间隔更接近真实访问习惯。一般会让脚本在每页请求之间随机睡 0.5 秒到 1.5 秒import time import random time.sleep(random.uniform(0.5, 1.5))不管时间长短都要控制整体量级一次脚本别跑几千页。带上重试逻辑遇到requests.exceptions.ConnectionError时退避重试最多 3 次for attempt in range(3): try: page session.get(url, headersHEADERS, timeout10) if page.status_code 200: return page except requests.exceptions.RequestException as e: print(f第{attempt1}次请求失败: {e}) time.sleep(2)到这里为止爬虫部分已经能稳定产出数据。很多同学写到这一步就急着画图但我建议先停下来想一下这些数据要存在哪里、以什么格式存才能支撑后面“全部数据”这个交付要求。2.4 动态页面怎么判断先看Network里的XHR而不是急着上selenium车型库和口碑板块差异明显。一个页面如果滚动加载出更多内容那么底层几乎都是异步 JSON 接口。这类接口一般在 XHR 分类里能找到返回格式多半是 JSONP 或者 JSON。找到接口 URL 后直接用 requests 替代浏览器的请求也就够了比启动一遍 selenium 快很多。除非目标页面的关键数据完全依赖执行 JavaScript 生成才需要考虑浏览器渲染方案。汽车之家大部分列表和详情页都可以直接用 HTML 或者普通接口拿到我为这个项目选型时没有用浏览器渲染既省内存也降低调试成本。判断页面的数据是服务端渲染还是客户端渲染有一个快速方法爬下来之后用正则或 BS4 全文搜索某个价格字段搜得到就是静态渲染搜不到就去找 XHR。这个判断花不了两分钟但能决定整个爬虫架构静态则 BS4 解析异步则 json.loads 解析接口。提示抓取的页面范围和用途请控制在个人学习研究范围内并降低请求频率避免对站点造成访问压力。3. 数据掉在地上之前先接住清洗与持久化到SQLite和CSV3.1 怎么设计表结构才担当得起“全部数据”三个字“全部数据”听上去是越多越好但期末大作业实际交付时一个完整规范的表结构比字段数量更有说服力。我一般会在数据库里建一张主表car_info字段如下后面所有可视化都从这个表里读取数据字段名类型说明idINTEGER PRIMARY KEY自增主键brandTEXT品牌例如丰田car_nameTEXT车系名称model_yearTEXT年款例如2024款price_minREAL指导价下限price_maxREAL指导价上限engine_typeTEXT发动机类型gearboxTEXT变速箱类型body_typeTEXT车身形式source_pageINTEGER来源页码方便回溯价格为什么要存最小和最大两个字段因为同一车系下有多个配置版本指导价通常是一个区间存成单一价格会丢失信息给后续的分析造成误差。model_year用 TEXT 而不是 INTEGER因为数据库里会出现“2024款”“改款”等混合文本强转数字会报错或变成空值。除了主表我一般会再加一张crawl_log表记录每次抓取的时间、总页数、成功条数和失败条数。别小看这张日志表期末大作业要把“源代码全部数据”一起交付靠这张表能清清楚楚说明数据是怎么来的答辩时这就是过程材料。3.2 用pandas做一遍清洗重复、空值、异常值和类型错位一次清掉爬虫接下来的行为很固定把每页解析到的数据不断追加到一个列表最后一次性转为 pandas 的 DataFrame。清洗阶段的首要任务是去重。汽车之家的列表页经常出现车系在不同条件下重复展示同一个car_name会反复出现去重时不能只按名称要按car_name model_year这两个字段一起判断因为不同年款的同名车是合法数据。import pandas as pd df pd.read_csv(car_raw.csv, encodingutf-8-sig) df df.drop_duplicates(subset[car_name, model_year], keepfirst) df df.dropna(subset[brand, car_name]) df[price_min] pd.to_numeric(df[price_min], errorscoerce) df[price_max] pd.to_numeric(df[price_max], errorscoerce) # 过滤明显异常的数据价格上下限颠倒或者价格区间过大 df df[df[price_max] df[price_min]] df df[(df[price_max] 0) (df[price_max] 1000)] print(f清洗后剩余 {len(df)} 条记录) df.to_csv(car_clean.csv, indexFalse, encodingutf-8-sig)这里pd.to_numeric(errorscoerce)的作用是把无法转成数字的文本值变成 NaN避免之后因为字符串类型做不了数值计算筛选条件中指导价上限大于 0 且小于 1000是防止爬到了异常单位或者页面乱码生成的数据。编码必须用utf-8-sig这样用 Excel 打开 CSV 时中文不会乱码这也是交付“全部数据”给老师的通用格式。3.3 清洗完落库SQLite写入、导出Excel两个动作一步到位pandas 写入 SQLite 非常简单不需要额外安装数据库服务用 Python 自带的 sqlite3 就能完成import sqlite3 df pd.read_csv(car_clean.csv, encodingutf-8-sig) conn sqlite3.connect(autohome.db) df.to_sql(car_info, conn, if_existsreplace, indexFalse) conn.close()to_sql默认按 DataFrame 的列建表if_existsreplace表示如果表已存在就先删除再重建。这样数据库里最终表和前面设计的表结构略有差异但期末项目的侧重点是流程完整、数据可复现如果想要更严格的约束可以先用CREATE TABLE建表再用conn.executemany批量插入。如果老师要求交付 Excel顺便导出一次df.to_excel(autohome_cars.xlsx, indexFalse, sheet_name车型数据)到这一步再回头看“全部数据”的交付物就包含了三层car_raw.csv原始数据、car_clean.csv清洗后的数据、autohome.db数据库文件。三层数据互相印证哪一层都有据可查。4. 可视化不只是画图从matplotlib到pyecharts页面的一步步升级4.1 先用matplotlib确认数据有没有分析价值很多人一上来就追求炫酷大屏结果画出来全是异常值白费半天。我会先画一张最朴素的分布图确认数据形状是正常的import sqlite3 import matplotlib.pyplot as plt import pandas as pd df pd.read_sql(SELECT car_name, price_min, price_max FROM car_info, sqlite3.connect(autohome.db)) df[price_mid] (df[price_min] df[price_max]) / 2 plt.figure(figsize(10, 6)) df[price_mid].plot.hist(bins30, color#2b7bba) plt.suptitle(汽车之家车系指导价中间值分布) plt.xlabel(价格 (万元)) plt.ylabel(车系数) plt.show()这张图能在 10 秒内告诉你两件事数据覆盖区间是否符合预期例如大量集中在 10-30 万区间有没有明显的离群值例如单价 1000 万。如果分布图出现断崖断层不要急着美化先回去看采集范围是不是有缺失页。matplotlib 的定位是验证工具不是最终交付物到了展示阶段pyecharts 更符合“可视化项目”四个字的成品感。4.2 pyecharts的核心参数init_opts、set_global_opts和datazoom一个都不能少pyecharts 是生成 ECharts 图表的 Python 封装输出 HTML 文件配置方式比原生 ECharts 简洁。先看一个品牌车系数量 Top 20 的柱状图完整写法from pyecharts import options as opts from pyecharts.charts import Bar import pandas as pd import sqlite3 df pd.read_sql(SELECT brand, COUNT(*) as cnt FROM car_info GROUP BY brand ORDER BY cnt DESC LIMIT 20, sqlite3.connect(autohome.db)) bar ( Bar(init_optsopts.InitOpts(width1200px, height600px, themelight)) .add_xaxis(df[brand].tolist()) .add_yaxis(车系数, df[cnt].tolist(), category_gap35%) .set_global_opts( title_optsopts.TitleOpts(title汽车之家车系品牌分布), yaxis_optsopts.AxisOpts(name数量), xaxis_optsopts.AxisOpts(name品牌, axislabel_optsopts.LabelOpts(rotate45)), datazoom_optsopts.DataZoomOpts(orientvertical, range_start0, range_end60), ) ) bar.render(brand_top20.html)核心组件的作用可以这样对照着记组件作用必调参数InitOpts画布尺寸与主题width、height、themeTitleOpts图表标题title、subtitleDataZoomOpts数据缩放range_start、range_endLabelOpts标签文案rotate、positionwidth/height控制页面在答辩演示时的展示比例品牌名是中文横排会重叠旋转 45 度是最低成本的处理DataZoomOpts让结果在输出静态 HTML 时仍然支持鼠标缩放图多的时候查看局部细节很有用。图表继续叠加价格区间占比饼图、发动机类型分布等逻辑完全一样从 SQL 读取聚合结果再配置对应图表组件。核心是要理解 pyecharts 的数据结构——它接收的是普通的 Python list所以df[brand].tolist()这种转换是必不可少的。4.3 把多张图拼成一个可视化页面Page组件比Flask更省心期末项目的可视化终点是一份能双击打开、放到 U 盘就带走的 HTML 文件。用 Flask 起服务当然是正解但对一个课程作业来说多了一层部署成本。pyecharts 自带的 Page 组件足够用它会按顺序把多张图渲染到一个页面里最后输出一个独立的 HTML不依赖网络也能打开from pyecharts.charts import Page, Bar, Pie page Page(layoutPage.SimplePageLayout) page.add(bar) page.add(pie) page.render(dashboard.html)Page.SimplePageLayout是清晰展示多图最稳的方式它会自动给每个图留出上下间距不会相互挤压。如果以后想升级成可视化大屏效果再把 Page 换成 Tab 组件或者用 Grid 做多个坐标轴组合但第一版不建议做这一步。先保证图全、数据对、文字标注清楚视觉效果可以放在最后半小时统一调。5. 源代码怎么组织才能一遍跑通以及期末答辩最常被问的3个问题5.1 工程目录和运行顺序把“源代码全部数据”整理成能直接复现的工程这是整个交付的临门一脚。源代码的目录结构我会固定成这样清晰、可复现、且答辩时老师一眼能看明白autohome_project/ ├── requirements.txt ├── main.py # 一键执行入口 ├── spider/ │ └── autohome_spider.py # 爬虫模块 ├── process/ │ └── clean_and_save.py # 清洗与落库模块 ├── visual/ │ └── make_charts.py # 可视化模块 ├── data/ │ ├── car_raw.csv # 原始数据 │ ├── car_clean.csv # 清洗后的数据 │ └── autohome.db # 数据库 └── output/ └── dashboard.html # 最终可视化页面requirements.txt里至少要有requests、beautifulsoup4、pandas、openpyxl、matplotlib、pyecharts这几项。运行顺序是先pip install -r requirements.txt然后按以下顺序执行python main.py --spider --pages 5会只跑爬虫--clean会只做清洗--visual只生成图表。把这三个开关做进 argparse 里的好处是答辩现场演示时可以分步展示每个环节的产物而不是一键跑完看不出中间过程。5.2 按现象排查请求被拦、字段消失、中文乱码的一张表爬虫项目跑不通时90% 都可以按现象快速定位。我遇到最多的还是这三种现象直接原因处理方式打印resp.status_code是 403 或 418请求头不完整或请求频率过密补 Referer 和 UA用 Session 持久化拉长间隔解析结果中car_name列表为空页面结构改版CSS 选择器失效打开 DevTools 重新定位标签替换 select 里的选择器CSV 用 Excel 打开全乱码保存时编码不是 utf-8-sig导出时指定encodingutf-8-sig数据库里中文变成乱码sqlite 连接没处理 UTF-8连接成功且字符异常时检查 Python 环境与文件编码排错顺序建议固定为先看 HTTP 状态码再打印截取后的resp.text文本片段比对字段最后检查 select 是否命中了预期标签。不要一遇到问题就怀疑反爬大多数情况是页面结构调整导致的选择器失配。5.3 生产可用的两个额外保障断点续爬和字段漂移检测既然标题里强调“源代码全部数据”那这份源代码就不应该只是跑一次就完事。我会补两个小功能把完成度从“能跑”提升到“可维护”。断点续爬的核心是记录已抓取车系 ID。在启动时先把数据库中已有的car_name集合加载进内存抓取循环里遇到重复名称就跳过每成功抓到一条记录就把 ID 实时追加写入一个本地done.txt。这样中途断网或程序异常退出再次启动时文件里已经记录了进度不会从头白跑。字段漂移检测是一个更实用的技巧每抓完一页统计这一页解析出的有效字段数和总条数如果有效字段数突然下降到上一页的一半以下就把当前页完整 HTML 保存成一个error_page.html留档并打印醒目的异常日志valid_cnt sum(1 for item in items if item.select_one(dt a)) if last_valid_cnt 0 and valid_cnt last_valid_cnt * 0.5: with open(error_page.html, w, encodingutf-8) as f: f.write(resp.text) raise RuntimeError(f第{page_no}页字段异常已留档) last_valid_cnt valid_cnt这个逻辑能兜住数据源改版后最怕的一种情况不是直接报错而是静默抓了一大堆空字段还输出成图导致最后交付的数据是坏的。跑完整个采集流程之后把 error_page.html 里的字段和正常页对比一次记录下差异字段名下次改版时直接改选择器就行——这份源代码就真的能重复用了。本文还有配套的精品资源点击获取