ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

二手车爬虫数据可视化毕设全解析:Python爬虫到Flask+ECharts完整链路

二手车爬虫数据可视化毕设全解析:Python爬虫到Flask+ECharts完整链路 简介这是基于Python的二手车爬虫数据可视化分析毕业设计项目面向计算机相关专业学生适用于毕业设计、课程设计或期末大作业场景。项目覆盖从二手车网站数据爬取、清洗存储到可视化分析展示的完整流程包含爬虫脚本、数据库文件、可视化前端页面及配套文档说明已获导师指导并通过答辩评审得分97分。资源共2000个文件以1745个Python源码文件为主其中Python源码用于爬虫采集与数据分析HTML、JavaScript、CSS文件用于构建可视化展示界面JSON与TXT为结构化数据及注解PDF为项目文档说明压缩包大小约55MB整体结构清晰便于按模块查阅与二次开发。目前已有658人学习下载。项目下载即用、无需修改既能支撑毕业答辩也可作为爬虫与数据可视化课程的综合实战参考。1. 二手车爬虫数据可视化一套能直接答辩的毕设源码二手车数据的爬取与可视化分析是这几年 Python 方向毕业设计里翻牌率相当高的题目也是课程设计里少有的「数据来源明确、技术栈完整、演示效果好」的组合。这套基于 requests BeautifulSoup 实现爬虫、SQLite 落库、Flask 提供数据接口、ECharts 渲染图表的毕设项目把整条链路完整串好源码、数据库文件和文档说明都打包在同一个压缩包里解压后按依赖装完就能跑出可视化页面。它解决的核心问题是一回事二手车平台上分散的车型、价格、里程、年份数据如何自动抓取、清洗入库再变成答辩现场能展示的柱状图、饼图和趋势线。适合爬虫经验不深又想短期拿出完整系统的毕设党也适合课程设计需要一个可演示项目的场景。2. 项目拆解数据链路与三个核心模块的分工2.1 目录结构先分清爬虫、数据库和 Web 三层拿到压缩包解压之后第一件事不是跑代码而是先看目录结构。这套项目的组织方式在国内毕设源码里很有代表性分成爬虫、数据、Web 展示、文档四块。常见的文件布局如下second_hand_car/ ├── car_spider/ │ ├── spider.py # 主爬虫入口负责翻页循环 │ ├── parser.py # HTML 解析与字段提取 │ ├── database.py # SQLite 建表、插入、去重 │ └── config.py # 请求头、URL、翻页参数 ├── data/ │ ├── car_data.db # SQLite 数据库文件 │ └── car_data.csv # 清洗后的中间数据导出 ├── web/ │ ├── app.py # Flask 应用入口 │ ├── templates/ │ │ └── index.html # 可视化页面 │ └── static/ │ ├── js/ # ECharts 配置脚本 │ └── css/ # 页面样式 ├── docs/ │ ├── 需求分析.md │ ├── 设计文档.md │ └── 答辩准备.md ├── requirements.txt └── run.py # 一键启动脚本car_spider负责从目标站点抓取数据data目录下预置了已经跑好的car_data.db即使暂时不想碰爬虫直接启动 Web 服务也能看到完整可视化结果。web目录是系统的展示层Flask 提供接口HTML 页面负责渲染。run.py是整个项目的统一入口我一般会先打开它确认有没有做依赖检查很多毕设源码的 run.py 只是简单调用app.run()这个项目在这里补了一层依赖缺失提示对新手友好不少。依赖安装是整个复现过程的第一步。打开终端定位到项目根目录后执行pip install -r requirements.txtrequirements.txt里锁定了四个核心库requests、beautifulsoup4、flask、pandas。这里我想单独说下pandas的作用——它看起来跟爬虫没关系但实际上parser.py里的字段清洗和最终 CSV 导出都依赖它的DataFrame操作后面在database.py里做批量插入时也用得上。安装完成后先别急着启动确认一下数据库文件是否完整可读这一步能省掉后面大量排查时间。2.2 SQLite 表结构与字段设计背后的意图数据库是这套系统里的中转站。爬虫抓下来的数据是字符串经过清洗后写入 SQLiteWeb 端再按维度聚合查询。表结构设计直接决定了可视化接口好不好写项目里的建表语句大致如下CREATE TABLE IF NOT EXISTS car_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, brand TEXT, price REAL, mileage REAL, reg_year INTEGER, gearbox TEXT, displacement REAL, location TEXT, crawl_date TEXT ); CREATE INDEX IF NOT EXISTS idx_price ON car_info(price); CREATE INDEX IF NOT EXISTS idx_brand ON car_info(brand);字段类型的选择有几个细节值得展开。price和mileage用 REAL 而不是 TEXT是为了在 SQL 里可以直接写AVG(price)、MAX(mileage)这类聚合函数不用每次都显式 CAST。reg_year存整数方便按年份分组做趋势图。crawl_date记录爬取批次调试爬虫或比对两次抓取结果是否稳定时非常有用。索引的添加是项目里容易被忽略但很关键的部分。毕设数据量不大几千条记录不加索引也能秒出结果但加上idx_price和idx_brand后价格区间聚合和品牌分组这两条高频查询的响应速度会明显改善答辩演示时页面刷新不卡顿。建表语句写在car_spider/database.py里每次运行爬虫前会自动执行CREATE TABLE IF NOT EXISTS幂等设计重复跑不会报错。单条插入的核心逻辑长这样import sqlite3 def insert_car_item(conn, item): cursor conn.cursor() cursor.execute( INSERT INTO car_info (title, brand, price, mileage, reg_year, gearbox, displacement, location, crawl_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) , ( item[title], item[brand], item[price], item[mileage], item[reg_year], item[gearbox], item[displacement], item[location], item[crawl_date] )) conn.commit()参数化查询的?占位符是这段代码里必须坚持的写法它既防止 SQL 注入也自动处理了字符串里的特殊字符。commit()必须在close()之前调用顺序一旦反了数据不会真正落盘而且不报错属于最隐蔽的翻车点之一。批量入库时可以改用executemany把清洗好的数据组成 list 一次性提交性能差距在小数据量下不明显但代码更紧凑。2.3 数据验证与导出跑通链路前先检查存量数据数据库文件预置了爬取结果但我建议复现时先做一轮数据核查确认存量数据能不能支撑可视化。快速检查方式是在项目根目录打开终端执行sqlite3 data/car_data.db SELECT COUNT(*), AVG(price), MAX(mileage) FROM car_info;这里的sqlite3是命令行工具不用额外安装 Python 包。如果输出结果里COUNT(*)明显偏少比如只有几十条可视化图表再好看也会显得单薄这时候需要重新触发一次爬虫补数据。如果AVG(price)为 0 或空说明清洗环节出了问题优先排查parser.py里的类型转换逻辑。数据导出是另一个高频需求。很多同学答辩时要额外交一份 Excel 或 CSV 作为过程材料database.py里封装了对应的导出函数import pandas as pd import sqlite3 def export_to_csv(db_pathdata/car_data.db, output_pathdata/car_data.csv): conn sqlite3.connect(db_path) df pd.read_sql_query(SELECT * FROM car_info ORDER BY price DESC, conn) df.to_csv(output_path, indexFalse, encodingutf-8-sig) conn.close() print(fexported {len(df)} rows to {output_path})这里的encodingutf-8-sig是有讲究的。直接写utf-8导出的 CSV 用 Excel 打开时中文会乱码utf-8-sig加了 BOM 头Excel 才能正确识别编码。如果在答辩材料里需要这份 CSV这个参数不要省。跑完导出后csv 文件里应该能看到完整的品牌、价格、里程、年份信息数据链路就算验证通了。3. 爬虫模块实战requests BeautifulSoup 的实现细节3.1 请求头伪装与目标站点适配爬虫模块是整个系统的数据来源也是最容易出问题的环节。项目默认适配的是静态渲染的二手车列表页requests 请求拿到 HTML 后用 BeautifulSoup 解析。config.py里的请求头配置是绕开基础反爬的第一道门槛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: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://www.che168.com/, Connection: keep-alive }请求头的核心是 User-Agent很多站点对空 UA 或 Python-requests 默认 UA 直接返回 403。Referer 字段在部分站点会被校验如果发现响应码异常优先检查这两项。timeout参数必须显式设置没有它网络抖动时请求会长时间挂起爬虫进程看起来像死了一样。项目里spider.py的请求调用方式如下import requests import time def fetch_page(url, paramsNone, retries2): for attempt in range(retries): try: response requests.get( url, paramsparams, headersHEADERS, timeout10 ) if response.status_code 200: return response.text else: print(frequest failed, status: {response.status_code}, attempt: {attempt 1}) except requests.exceptions.Timeout: print(ftimeout, attempt: {attempt 1}) except requests.exceptions.ConnectionError: print(fconnection error, attempt: {attempt 1}) time.sleep(1) return None这个fetch_page做了三层保护timeout10防止无限挂起retries循环处理偶发网络错误sleep(1)让重试之间隔开一秒。如果两次重试都失败函数返回None调用方根据返回值决定是跳过还是终止。在毕设答辩场景里爬虫偶尔失败是允许的关键在于失败后程序不能崩溃要有清晰的状态输出。3.2 翻页循环、重试机制与去重策略单页数据量撑不起可视化翻页是爬虫的必备能力。项目的翻页逻辑采用 URL 参数拼接方式汽车之家的列表页页码在page参数里循环控制页数def crawl_pages(start_page1, max_pages50): all_data [] base_url https://www.che168.com/changsha/list/ for page in range(start_page, start_page max_pages): params {page: page, sort: price-asc} html fetch_page(base_url, paramsparams) if html is None: print(fpage {page} fetch failed, skip to next) continue page_data parse_car_list(html) if not page_data: print(fpage {page} has no data, stop crawling) break all_data.extend(page_data) print(fpage {page}: got {len(page_data)} items, total {len(all_data)}) time.sleep(2) return all_data翻页循环里有几个判断值得留意。fetch_page返回None时用continue而不是break因为单次请求失败后下一页可能是正常的直接中断会丢失大量数据。page_data为空时用break表示已经翻到最后一页或页面结构发生变化再往下翻只会浪费请求。sortprice-asc让数据在价格维度上有序分布后面画价格区间图时分布曲线会比较平滑。time.sleep(2)是爬虫能跑多长时间的关键参数。2 秒间隔面对普通站点足够但遇到反爬严格的平台建议改成 5 到 8 秒。爬取时间拉长换稳定性这对毕设来说完全划算反正数据量几千条就够了。去重策略是数据质量的重要保障。二手车列表页在翻页过程中可能出现重复车源直接入库会让统计结果失真。项目在database.py里用两个层面去重第一层是内存里的set记录已经见过的标题def deduplicate(items, seen_titles): unique_items [] for item in items: title item[title] if title not in seen_titles: seen_titles.add(title) unique_items.append(item) return unique_items第二层是数据库层面的兜底给title字段建唯一索引并用INSERT OR IGNORE方式写入。这样即使爬虫中途崩溃后重跑也不会产生重复记录。两层结合数据表里的记录数就是实际去重后的数量答辩时讲这个设计能体现出项目完整性。3.3 字段清洗与类型转换的边界问题爬虫拿到的原始字段几乎全是字符串带「万」字的价格、含「万公里」的里程、格式不统一的年份直接写库会让后续聚合查询一塌糊涂。项目的清洗逻辑集中在parser.py的clean_data()函数import re def clean_data(raw_items): cleaned [] for item in raw_items: price_text item.get(price, 0) mileage_text item.get(mileage, 0) year_text item.get(reg_year, 0) cleaned.append({ title: item.get(title, ).strip(), brand: extract_brand(item.get(title, )), price: float(re.sub(r[^\d.], , price_text)) if price_text else 0.0, mileage: float(re.sub(r[^\d.], , mileage_text)) if mileage_text else 0.0, reg_year: int(re.search(r\d{4}, year_text).group()) if re.search(r\d{4}, year_text) else 0, gearbox: item.get(gearbox, 未知), displacement: parse_displacement(item.get(displacement, )), location: item.get(location, 未知), }) return cleaned def extract_brand(title): if not title: return 未知 # 取标题第一个空格前的词作为品牌 parts title.split() if parts: return parts[0] # 兜底用正则匹配连续中文 m re.match(r[\u4e00-\u9fa5]{2,4}, title) return m.group() if m else 未知清洗代码里有几个处理逻辑值得展开。价格字段用re.sub(r[^\d.], , price_text)把「万」「万元」「」等无关字符全部清除只保留数字和小数点再交给float()转换。这样即使不同批次的页面单位格式有微小差异也能统一处理。车型年份字段更刁钻页面里经常显示「2015款」或「2015年上牌」直接int()会抛异常。正则re.search(r\d{4}, year_text)只提取四位数年份匹配不到就给默认值 0避免单条脏数据导致整个清洗流程崩掉。extract_brand的兜底逻辑同样重要——有些平台的标题以「【精品】」开头直接用split()[0]会拿错词所以先判断有没有空格没有空格的标题再用正则按连续中文匹配前几个字。清洗完成后所有字段的类型和单位都统一了后面的 SQL 查询和图表渲染就不用再操心格式问题。4. 可视化与 Web 展示Flask ECharts 的对接方式4.1 Flask API 层的查询设计与 JSON 返回可视化页面的数据来源是 Flask 提供的一组 API。web/app.py的角色是查询 SQLite 并按前端需要的维度返回 JSON。这层的设计原则是聚合计算尽量在 SQL 里完成前端只负责渲染不要在前端代码里做 group by 或循环统计。app.py的核心代码from flask import Flask, jsonify, render_template import sqlite3 app Flask(__name__) def query_db(sql): conn sqlite3.connect(data/car_data.db) conn.row_factory sqlite3.Row cur conn.cursor() cur.execute(sql) rows cur.fetchall() conn.close() return [dict(row) for row in rows] app.route(/api/price_distribution) def price_distribution(): data query_db( 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 car_info GROUP BY price_range ORDER BY MIN(price) ) return jsonify(data) app.route(/api/brand_distribution) def brand_distribution(): data query_db( SELECT brand, COUNT(*) as cnt FROM car_info WHERE brand ! 未知 GROUP BY brand ORDER BY cnt DESC LIMIT 10 ) return jsonify(data) app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)row_factory sqlite3.Row让每一行数据可以通过字段名访问配合dict(row)转成 JSON 可序列化的字典前端拿到的就是标准的[{}, {}]结构。价格分布的CASE WHEN语句把连续的价格值离散成四个区间这是直方图的标准做法。ORDER BY MIN(price)保证区间按价格从低到高排列前端不用再做排序。接口层的另一个细节是brand_distribution里加了WHERE brand ! 未知和LIMIT 10防止清洗时产生的无效数据污染图表同时只展示 top10 品牌避免品牌太多导致饼图标签挤在一起。接口调试时可以直接在浏览器地址栏访问http://localhost:5000/api/price_distribution返回的 JSON 结构一目了然前端图表空白时先看接口通不通。4.2 ECharts 三类图表的配置与数据映射前端页面通过浏览器端的 fetch 请求接口拿数据再喂给 ECharts 渲染。项目里配置了三张图价格分布柱状图、品牌占比饼图、年份趋势折线图。index.html里柱状图的核心逻辑fetch(/api/price_distribution) .then(response response.json()) .then(data { const chart echarts.init(document.getElementById(priceChart)); chart.setOption({ title: { text: 二手车价格分布, left: center, textStyle: { fontSize: 16 } }, tooltip: { trigger: axis }, grid: { left: 10%, right: 5%, bottom: 10% }, xAxis: { type: category, data: data.map(item item.price_range) }, yAxis: { type: value, name: 车辆数量 }, series: [{ type: bar, data: data.map(item item.cnt), barWidth: 50%, itemStyle: { color: #2f7be4, borderRadius: [4, 4, 0, 0] } }] }); });这里的两个data.map是 ECharts 接入数据最常见的写法分别把接口返回的price_range和cnt字段提取成两个数组。xAxis 的数据源是类别数组series 的 data 是对应的数值数组两者顺序一致才能对齐渲染。如果接口字段名改了怎么排查先在浏览器控制台打印data确认字段名是不是price_range和cnt再检查map里写的字段跟接口返回是否一致这两个地方对不上图表必然是空白。饼图配置与柱状图异曲同工series.type换成pie数据格式变成{ name: 大众, value: 128 }的结构fetch(/api/brand_distribution) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(brandChart)); chart.setOption({ title: { text: 品牌分布 TOP10, left: center }, tooltip: { trigger: item }, legend: { bottom: 0% }, series: [{ type: pie, radius: 60%, data: data.map(item ({ name: item.brand, value: item.cnt })) }] }); });饼图的radius: 60%控制圆环大小legend放在底部避免遮挡图表主体。折线图配置同柱状图相似series.type换成linesmooth: true让曲线更平滑。ECharts 的 CDN 引入放在body标签末尾script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script4.3 页面启动方式与演示环境的网络问题启动整个 Web 服务的命令是python run.py正常情况控制台会打印 Flask 的启动信息浏览器访问http://localhost:5000即可看到页面。host0.0.0.0意味着局域网内其他设备也能访问答辩演示时如果现场电脑有问题可以拿手机连同一 WiFi 打开页面兜底。debugTrue开发阶段能看到详细报错但演示前建议把debug改成False避免误触文件保存导致服务自动重启。网络问题是答辩现场最容易翻车的一环。ECharts 的 CDN 依赖外网如果教室没有外网图表就是空白。预防方式很简单——把echarts.min.js下载到项目里改成本地引用script src{{ url_for(static, filenamejs/echarts.min.js) }}/script用自己的电脑跑项目时不存在这个问题但答辩用的可能是教室机器提前把静态资源本地化能少一件心事。第 5 章的避坑部分还会再展开讲这类问题。5. 避坑指南毕设复现与答辩演示的常见问题5.1 爬虫请求被拒绝返回 403现象运行spider.py时控制台连续输出status: 403一页数据都抓不到。原因目标站点反爬升级校验了 User-Agent 之外的请求头字段或者当前 IP 已被临时限制。项目里的请求头配置在编写时有效遇到站点调整就需要手动适配。解决先确认config.py里的请求头是不是最新版本打开浏览器无痕窗口访问目标页面从开发者工具 Network 面板拷贝实际请求头。对比config.py把缺失的字段补上。其次检查访问频率time.sleep(2)在反爬严格的站点上不够改成time.sleep(5)或更大。如果都无效可能被限制了 IP换 WiFi 用手机热点试试。临时封禁一般几分钟到几小时会解除不用急着换 IP 池方案。批量抓取时建议加一个随机延时import random time.sleep(random.uniform(3, 6))固定间隔的请求有规律性随机间隔能降低被识别为脚本的概率。5.2 SQLite 报错 database is locked现象启动 Flask 后访问页面服务端报sqlite3.OperationalError: database is locked页面数据加载失败。原因SQLite 在同一时刻只允许一个写入方。上一个 Python 进程没有正常关闭数据库连接或者爬虫和 Web 服务同时打开了同一个数据库文件。解决先看看是不是有残留的 Python 进程在后台运行Windows 下用任务管理器结束对应进程macOS 用ps aux | grep python找到 PID 并杀掉。根源在于代码里没有保证连接关闭修复方式是把连接操作放到with上下文管理器中import sqlite3 from contextlib import closing def query_db(sql): with closing(sqlite3.connect(data/car_data.db)) as conn: conn.row_factory sqlite3.Row cur conn.cursor() cur.execute(sql) return [dict(row) for row in cur.fetchall()]closing保证无论查询是否出错连接都会在退出with块时关闭。把项目里所有手动conn.close()的地方都改成这种写法database is locked 基本不会再出现。另外要注意爬虫和 Flask 不要同时运行两个进程数据采集完再启动 Web 服务。5.3 ECharts 图表空白控制台报错现象页面能打开但三张图表区域全是空白浏览器控制台报红色错误。原因最常见的是echarts.init执行时对应的 DOM 元素还未渲染完成其次是接口返回的数据是空数组第三种是 CDN 加载失败导致echarts对象不存在。解决按顺序排查。先看控制台报错类型——如果是ReferenceError: echarts is not defined说明 CDN 没加载成功网络不通或地址已失效改用本地static/js/echarts.min.js引用。如果是Cannot read property init of undefined同样指向 echarts 对象不存在。如果没有任何报错但页面空白在 fetch 的.then里加一行console.log(data)确认接口返回是否有数据。接口返回空数组时检查数据库里car_info表有没有记录以及查询条件是否写错。初始化时机的问题把 echarts 相关脚本放到body底部或在window.onload事件里执行。答辩前有一个习惯值得养成断网状态下完整打开一次页面。ECharts 本地化引用后才能通过这个测试现场没有外网的场景下才不会手忙脚乱。5.4 Python 多版本环境下依赖冲突现象按requirements.txt装完依赖运行run.py时报ModuleNotFoundError: No module named flask但pip list里明明有 Flask。原因系统装了多个 Python 版本python run.py用的解释器和pip install装的解释器不是同一个。这是 Windows 环境最常见的毕设翻车点。解决统一用python -m pip形式安装依赖确保包进入当前解释器环境python -m pip install -r requirements.txt python run.py如果项目里有用到虚拟环境创建并激活后再安装python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install -r requirements.txt python run.py下一步确认当前python指向哪个解释器在终端里敲python --version和which python。如果发现默认指向了某个旧版本所有命令都用/path/to/python显式调用不与系统默认环境混用。这个坑排查成本不高但很多人卡在里面半天出不来。5.5 控制台中文乱码现象爬虫运行时控制台输出的中文是乱码比如PAGE 1正常但车辆信息显示成鍑虹巟。原因Windows 控制台默认编码是 GBK而 Python 打印的是 UTF-8 字符串两者不匹配导致显示错乱。这个问题在运行爬虫时影响不大但答辩演示时考官看到乱码会很减分。解决项目根目录创建.env或用标准库适配。最简单的处理是在spider.py文件头部加一行import sys import io sys.stdout io.TextIOWrapper(sys.stdout.buffer, encodingutf-8)或者在启动命令层面设置环境变量。Windows PowerShell 下先执行$env:PYTHONIOENCODINGutf-8 python spider.pyPYTHONIOENCODING环境变量让 Python 在输出时统一使用 UTF-8 编码控制台显示就不再有乱码。注意这个只在当前终端会话有效重开终端要重新设置。跑数据导出的 CSV 时记得沿用第 2 章提到的utf-8-sig编码Excel 打开才不会乱码。6. 跑通后的进阶方向换数据源、加模型与演示验证6.1 目标站点改成 Ajax 接口时的爬虫改造项目默认爬的是静态 HTML 页面但现在不少二手车平台的前端改成了 Vue 或 React列表数据通过 Ajax 接口动态加载。直接requests.get页面拿不到车辆信息返回的 HTML 里只有空壳的div#app。遇到这种站点改造方式很直接打开浏览器开发者工具切到 Network 面板刷新页面后筛选 XHR 请求找到返回 JSON 数据的那个接口。def crawl_from_api(): api_url https://example.com/api/car/list params { page: 1, pageSize: 20, sort: price } response requests.get(api_url, paramsparams, headersHEADERS, timeout10) data response.json() for item in data[data][list]: car_item { title: item.get(title, ), price: item.get(price, 0), mileage: item.get(mileage_km, 0) / 10000, reg_year: item.get(first_reg_year, 0), gearbox: item.get(gearbox_type, 未知), displacement: item.get(engine_volume, 0), } insert_car_item(conn, car_item)接口返回的字段名通常和页面展示不完全一致比如mileage_km是原始公里数除以 10000 转成万公里才能和项目里已有的单位对齐。reg_year可能叫first_reg_year价格可能叫dealer_price。要对着实际返回的 JSON 结构做字段映射清洗逻辑整体可以复用。响应里如果含total字段翻页循环可以用总条数除以 pageSize 来计算终止页比静态页面的空数据判断更精确。6.2 在可视化基础上加一个价格预测模块答辩时想加分可以在可视化之外补一个简单的价格预测分析。随机森林或线性回归都可以基于现有的price、mileage、reg_year三列数据用 scikit-learn 训练模型。项目数据量不大线性回归足够from sklearn.linear_model import LinearRegression import numpy as np def prepare_data(): rows query_db(SELECT mileage, 2025 - reg_year, price FROM car_info WHERE price 0) X np.array([[row[0], row[1]] for row in rows]) y np.array([row[2] for row in rows]) return X, y X, y prepare_data() model LinearRegression() model.fit(X, y) # 预测一辆里程 5 万公里、车龄 3 年的车 prediction model.predict([[5.0, 3.0]]) print(festimated price: {prediction[0]:.2f} 万元)这个模型的业务解释是清晰的里程和车龄是两个特征价格是目标变量。2025 - reg_year把年份转换成车龄拟合出的系数能直观地看到「里程每增加 1 万公里价格大约下降多少」。这个结论可以直接写进论文的分析章节比单纯几张图表更有内容。当然几百条数据的预测精度有限论文里表述为「趋势参考」而非「精确估价」逻辑上站得住。6.3 答辩演示前的一轮冷启动验证我复现过不少毕设项目也逐渐形成了自己固定的一套验证流程。拿到压缩包后第一次跑通时极其顺利但答辩现场换了一台电脑后全军覆没的情况并不少见。从那以后我每次演示前都会强制走一遍冷启动流程从重启电脑开始到最终页面完整展示三个图表为止全程不再碰任何无关操作只按顺序执行「安装依赖 → 启动服务 → 打开页面 → 刷新验证图表 → 截图留底」。这一步能把绝大多数环境问题提前暴露出来不用把风险留到现场。数据备份也是一个教训。跑爬虫之前把data/car_data.db先复制一份到data/backup.db。为什么这么操作爬虫脚本和数据库读写如果存在逻辑错误可能污染原始数据导致图表无法还原。有了备份随时可以恢复干净状态不用重新跑一遍爬虫省时间也省心。每次改动代码前备份久而久之在项目里也成了一种习惯。开发过程中遇到页面空白我会先看 Flask 控制台有没有报错再看浏览器 Network 面板里/api/price_distribution是否返回 200最后看响应体里的 JSON 数据是否正常。这套排查顺序可以覆盖九成以上的前端显示问题。答辩演示前最后一个小时通常已经不再做任何代码改动了只做流程演练。这套项目本身是直接可用的状态但现场演示的流畅度还得靠自己提前把关。希望这些经验能帮你的答辩过程少一些不确定性把时间花在讲技术逻辑本身而不是在环境配置里挣扎。以上是我对这个项目从拆解到复现的全部记录。压缩包里包含了源码、文档说明和数据库文件按文中流程跑一遍就能看到完整可视化效果需要的话可以直接拿去用。本文还有配套的精品资源点击获取
返回列表