二手房数据可视化系统:Python爬虫+Flask+ECharts实战解析
简介基于Python的二手房数据可视化系统设计资料包面向计算机、数据科学及信息管理相关专业学生适用于毕业设计、期末大作业或课程设计。项目完整覆盖二手房交易数据的爬取、清洗、分析与可视化流程利用数据处理和科学计算库完成预处理再通过可视化库生成柱状图、折线图、散点图等图表直观呈现房价分布与变化趋势爬虫程序可抓取房源价格、面积、位置、建成年代等信息。资源共113个文件大小约29.29MB包含18个Python脚本、17个CSV数据文件、15个HTML页面、16个JavaScript脚本及17张图表图片前后端代码齐备目录结构清晰便于二次开发与学习。随包附带系统界面截图、操作演示视频及结果分享PPT可供答辩展示或项目说明使用。已有47人学习下载适合希望快速搭建同类可视化系统并理解完整项目实现思路的开发者参考。1. 二手房数据可视化系统毕设模板里最容易被“数据”卡住的环节做一个二手房数据可视化系统大部分人的第一个念头是“画图”真正动手才发现最耗时间的不是 ECharts 配置而是“数据拿到手是脏的、格式是乱的、字段对不上”。这套基于 Python 的系统设计资源正好把整条链路——爬虫采集、pandas 清洗、Flask ECharts 展示——串成了一条能直接跑起来的主线。我拆完这套项目的感受是它不是那种花哨的“大屏可视化”而是课程设计和毕业设计里最容易过审、也最容易二次改造成自己城市数据的路线。适合两类人一是做数据可视化课程大作业、毕设的学生需要一套能讲清楚“数据从哪来、怎么处理、怎么展示”的完整流程二是想自己搭一个房价分析小工具、但不想从零写页面的从业者。先把结论放这里这个系统的价值不在图表数量而在“换数据源也能跑”的工程结构。2. 数据采集模块爬虫怎么写、翻页怎么翻、请求被限怎么处理2.1 数据源选型为什么第一选择是链家结构二手房数据可视化最麻烦的不是可视化而是数据。常见的数据源有链家、贝壳、安居客、58 同城。我在实际拆解时发现链家的二手房列表页结构最适合教学场景列表页、详情页、区域筛选是分离的字段也比较规整比如总价、单价、面积、户型、楼层、朝向、装修、小区名基本都集中在同一个 HTML 块里而且链家的页面按城市和区域做 URL 前缀换城市只需要换前缀。相比之下贝壳的数据不少是接口异步加载的初学者一抓返回空页面容易劝退安居客和 58 的字段不够规整很多时候“单价”要自己从杂项文本里抠。所以这个系统设计里把数据源定位在链家或贝壳这类结构化较清晰的站点上属于常见做法。要注意的是这类站点都有反爬策略核心是限制请求频率、校验 headers。正常浏览速度下每秒 1 到 2 个页面是相对安全的节奏。2.2 列表页爬虫的主体代码爬虫部分通常由三块组成请求页面、解析字段、保存结果。我用一个最小可运行版本来说明实际系统里可以再加断点续爬和重试。# -*- coding: utf-8 -*- import requests import time import csv import re from fake_useragent import UserAgent headers {User-Agent: UserAgent().random} def get_one_page(city, page): # city: 城市拼音前缀比如 beijing # page: 第几页链家每页 30 条 url fhttps://{city}.lianjia.com/ershoufang/pg{page}/ try: resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 if resp.status_code ! 200: return None # 页面没拿到就直接返回空 return resp.text except requests.RequestException as e: print(请求失败:, e) return None def parse_page(html): # 这里用正则在 HTML 中抽取房源卡片 # 每套房子在 li[classclear] 里字段格式形如 # div classtotalPricespan438/span万/div # div classunitPricespan54321/span元/平/div result [] # 常见做法是按字段分条提取再用 zip 对位组装 # total_price_text re.findall(rdiv classtotalPrice.*?span(.*?)/span, html) return result抓页面的时候有几个参数值得解释一下timeout10限制单次请求最长等待时间避免某个页面卡死让整个爬虫挂起。10 秒在正常网络下足够如果网络差可以调到 15。resp.encoding utf-8强制指定编码。链家页面是 UTF-8但如果请求时没带对编码头requests 会根据响应头猜测偶尔会猜成 GBK导致中文乱码。headers里的 User-Agent 建议随机化fake_useragent库每次生成一个不同的浏览器 UA比一直用一个默认 UA 更不容易被识别为脚本。拿到 HTML 之后解析。这里给大家一个更稳的思路先打印一段 HTML确认字段对应的 class 名再写正则不要靠记忆写格式。很多毕业设计翻车就是“以为字段是classtotalPrice结果页面结构改版了”正则全空后面清洗就跟着白做。2.3 翻页、限速与断点续爬链家二手房列表页超过 100 页后会限制访问所以一般爬前 20 到 50 页就够分析用了。主循环可以写成这样def main(): city beijing all_data [] for page in range(1, 51): # 前 50 页 html get_one_page(city, page) if html: page_data parse_page(html) all_data.extend(page_data) print(f第 {page} 页抓到 {len(page_data)} 条) time.sleep(1.5) # 每页间隔 1.5 秒 with open(house_data.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[小区, 区域, 总价, 单价, 面积, 户型]) writer.writeheader() writer.writerows(all_data) if __name__ __main__: main()time.sleep(1.5)是这一整段里最重要的参数。如果你把间隔改成 0多半在第 20 页左右开始遇到访问被限制。这个限制是暂时性的停一停就能恢复但爬虫进程不会自动等所以遇到连续几次返回空页面时常见做法是“退避式等待”第一次失败等 5 秒第二次等 10 秒最多等 60 秒然后再重试而不是无限重连。断点续爬的常见做法是记录page下标比如写一个progress.txt每次成功抓完一页就更新页码。下次启动时先读这个文件从上次的位置继续避免因为被限制导致前功尽弃。这个环节在数据可视化系统设计里往往被忽略但答辩时提到“支持断点续爬”会显得整体工程更完整。3. 数据清洗与标准化把“万”“元/平”“暂无数据”变成能聚合的数值3.1 抓下来的原始数据长什么样爬虫保存下来的 CSV 通常是这样的字段示例原始值小区阳光花园区域朝阳 / 望京总价438万单价54321元/平面积82.5平米户型2室1厅朝向南装修精装这里有两个直接问题总价和单价是字符串带着“万”“元/平”没法直接求均值面积里的“平米”不统一有的记录叫“㎡”更麻烦的是有些字段是“暂无数据”或者空字符串。如果不处理做出来的图表会非常难看价格轴上出现字符串报错或者某个区域因为缺数据直接没有柱子。3.2 pandas 清洗主流程清洗阶段的核心就是两步把带单位的字符串转成数值把缺失和异常值处理掉。下面这段代码是可以直接放到项目里跑的import pandas as pd df pd.read_csv(house_data.csv, encodingutf-8-sig) # 1. 总价去单位438万 - 438.0 df[总价] df[总价].astype(str).str.replace(万, , regexFalse) df[总价] pd.to_numeric(df[总价], errorscoerce) # 2. 单价去单位54321元/平 - 54321.0 df[单价] df[单价].astype(str).str.replace(元/平, , regexFalse) df[单价] pd.to_numeric(df[单价], errorscoerce) # 3. 面积去单位82.5平米 / 82.5㎡ 统一 df[面积] df[面积].astype(str).str.replace(r(平米|㎡), , regexTrue) df[面积] pd.to_numeric(df[面积], errorscoerce)一段段拆开看astype(str)保证每个值都是字符串避免某个单元格已经是数字时报replace不支持的问题。这是新手最容易踩的坑CSV 读进来某一列可能混合字符串和数字。str.replace(万, , regexFalse)里regexFalse的意思是按字面替换不是正则。对于“万”这种固定文字没必要开正则开正则反而容易把别的字段里的“万”误删。pd.to_numeric(..., errorscoerce)遇到转不了的值比如“暂无数据”会变成NaN而不是抛异常。这就是清洗时最关键的“后悔药”它把脏数据降级为缺失值后面统一处理。对“暂无数据”的处理我一般用两步先看占比占比小直接删除整行占比大就分区域均值填充。具体代码如下print(df[[总价, 单价, 面积]].isna().sum()) # 删除关键字段为空的记录 df df.dropna(subset[总价, 单价, 面积]) # 删除重复房源同一小区户型面积总价视为重复 df df.drop_duplicates(subset[小区, 户型, 面积, 总价])这里dropna的subset参数很关键只检查这三列如果检查全表会把“装修”为空但价格正常的房源也删掉白白损失数据。重复删除同理用业务上认为“同一条房源”的字段组合来判定不要整行去重因为两个不同房源很可能朝向、楼层不同但其他字段相同。3.3 价格分箱与区域聚合可视化里比较常见的两个分析维度一个是“各区域均价”一个是“价格区间分布”。区域聚合直接groupby就行avg_price df.groupby(区域)[单价].agg([mean, median, count]) avg_price.columns [均价, 中位数, 挂牌量] avg_price avg_price.sort_values(均价, ascendingFalse) avg_price.to_csv(区域均价.csv, encodingutf-8-sig)价格区间分布用pd.cut分箱bins [0, 30000, 50000, 80000, 100000, 200000] labels [3万以下, 3-5万, 5-8万, 8-10万, 10万以上] df[价格段] pd.cut(df[单价], binsbins, labelslabels) price_dist df.groupby(价格段, observedFalse).size()pd.cut的边界要以你所在城市的价格分布来定。北京、上海区间要上探到 10 万以上成都要放到 2 万起步。这种参数放到可视化系统设计里就是后期最容易被老师追问的“为什么这么分箱”的点回答“按城市价位动态配置”会比“写死的”听起来更合理。清洗和聚合完成之后数据就变成了几个很小的 CSV 或 JSON这些就是可视化的输入。到这一步系统才真正进入“数据可视化”的环节。4. 可视化落地Flask 后端 ECharts 前端的页面工程4.1 为什么选 Flask ECharts 这套组合这个系统设计里可视化部分最常见的技术选型是两类一类是 Pyecharts在 Python 里直接生成图表再渲染成 HTML另一类就是 Flask 提供数据接口前端用 ECharts 原生画图。我在实际拆解中更推荐后者原因是答辩和演示时页面是可以交互的鼠标悬停出数值、点击图例筛系列这套交互是原生 ECharts 最顺手。Pyecharts 生成的 HTML 虽然开箱即用但想改交互细节比如联动、钻取、地图下钻时被封装层限制得比较多得绕进生成后的 JS 里改反而更麻烦。Flask 接口将来换成其他前端比如 Vue 里的开发页面后端不用动数据可视化系统的“数据层与展示层分离”也更经得起提问。4.2 Flask 数据接口后端只需要三个接口首页、区域均价接口、价格分布接口。代码量不大但结构要清晰from flask import Flask, jsonify, render_template import pandas as pd app Flask(__name__) df pd.read_csv(区域均价.csv, encodingutf-8-sig) app.route(/) def index(): return render_template(index.html) app.route(/api/avg_price) def api_avg_price(): data df.sort_values(均价, ascendingTrue) # 排序放在后端前端少干活 return jsonify({ areas: data[区域].tolist(), prices: data[均价].tolist(), counts: data[挂牌量].tolist() }) app.route(/api/price_dist) def api_price_dist(): dist pd.read_csv(价格分布.csv, encodingutf-8-sig) return jsonify({ segments: dist[价格段].tolist(), count: dist[count].tolist() }) if __name__ __main__: app.run(host127.0.0.1, port5000, debugFalse)强调几个细节host127.0.0.1是只允许本机访问演示时安全。如果你要给同学用手机看改成0.0.0.0这样同一局域网内的设备能通过你的电脑 IP 访问页面。debugFalse课程设计跑演示时不要开 debug 模式。debug 模式会在代码修改时自动重启还会在页面报错时输出调用栈在答辩现场出现这些是减分的。本地调试可以用debugTrue演示前必须关掉。接口返回 JSON 时把df的排序、去重都放在后端做前端只负责渲染这样前端代码能保持很薄也不会出现“图表 Axes 顺序乱跳”的玄学问题。4.3 ECharts 显示区域均价的页面前端index.html的核心部分如下!DOCTYPE html html langzh-CN head meta charsetutf-8 title二手房数据可视化/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idavgPriceChart stylewidth: 90%;height:480px;/div script fetch(/api/avg_price) .then(res res.json()) .then(data { var chart echarts.init(document.getElementById(avgPriceChart)); chart.setOption({ title: { text: 各区域二手房挂牌均价 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.areas }, yAxis: { type: value, name: 元/平米 }, series: [{ type: bar, data: data.prices, itemStyle: { color: #5470c6 } }] }); }); /script /body /html这套写法有几个值得注意的控制点echarts.init传入的div必须有显式宽度高度否则图表经常渲染成零高白屏。tooltip: { trigger: axis }让鼠标悬停显示当前柱子的数值这个是答辩演示时最常用的交互老师一般会拿鼠标划一下看数字所以 tooltip 不能省。ECharts 的 CDN 如果打不开校园网络、内网环境图表会报echarts is not defined。常见做法是把echarts.min.js下载到static/js/目录用本地引用这个我会在第 5 章避坑里单独讲。4.4 加一个价格分布饼图和联动筛选在同一个页面里再加一个饼图展示“价格段分布”两块图共用同一批数据接口互不干扰fetch(/api/price_dist) .then(res res.json()) .then(data { var pie echarts.init(document.getElementById(priceDistChart)); pie.setOption({ title: { text: 挂牌房源价格段分布 }, series: [{ type: pie, radius: [35%, 70%], data: data.segments.map((name, i) ({ name: name, value: data.count[i] })) }] }); });接口层面如果要做“按城市筛选”只需要在/api/avg_price后面再加一个查询参数app.route(/api/avg_price) def api_avg_price(): city request.args.get(city, 全部) if city ! 全部: df_filtered df[df[城市] city] ...这里把“城市”挂到 CSV 的列上接口传参数时带上前端就能通过下拉框实现城市切换。这种“接口 前端筛选”的写法比在 Python 里起多个 Flask 服务去分别给图要干净得多这也是这个系统设计里很容易扩展的点想加“户型占比”“朝向占比”“地铁距离分析”都只需要加接口和图表配置不动主流程。5. 避坑与排查运行这个系统时的五个翻车现场这一章我从拆包、跑通到完整演示把最容易翻车的五个现场按“现象 → 原因 → 解决”记录下来希望对照着复现的你有实际作用。5.1 爬虫跑十几页就抓不到数据了现象前面几页正常大概第 15 到 25 页开始页面返回空白或验证码status_code接近 200 但parse_page抽不到任何字段。原因请求频率太高短时间内大量请求同一个列表结构触发了站点的访问频率限制。不是 UA 的问题单纯是被限流。解决把time.sleep(1.5)调到 2 到 3 秒同时加一个失败退避连续 3 次解析为空就停止等 60 秒再继续。我一般会写一个简单的计数封锁逻辑比如fail_count 3就休眠两分钟。5.2 CSV 里的中文乱码现象用 Excel 打开house_data.csv中文小区名和区域名全是乱码但用记事本打开却是正常的。原因写 CSV 时用了encodingutf-8Excel 默认按 GBK 解析把 UTF-8 文件读成了乱码。解决写入时用encodingutf-8-sig带 BOM 的 UTF-8Excel 能正确识别。后端读取时也要对应pandas 读utf-8-sig保持一致不要在清洗时混用utf-8和utf-8-sig否则会出现“读出来带乱码开头”的隐性 bug。5.3 Flask 启动提示端口被占用现象运行app.run()报OSError: [Errno 98] Address already in use或socket.error: [Errno 56]。原因之前跑过一次 Flask 没关掉进程还占着 5000 端口或者系统里有其他服务占用了。解决换端口把port5000改成5001或者找到旧进程杀掉。Windows 下用netstat -ano | findstr 5000查 PID 再taskkill /F /PIDmacOS/Linux 用lsof -i:5000查 PID 再kill -9。演示前建议先pkill -f flask清一遍残留进程免得现场启动报错。5.4 ECharts 图表空白控制台报echarts is not defined现象页面打开是空的F12 看控制台提示echarts is not defined。原因CDN 外链加载被网络环境阻断最常见的是校内网和演示现场没有外网访问权限。解决提前把echarts.min.js下载到项目的static/js/目录标签改成script src{{ url_for(static, filenamejs/echarts.min.js) }}/script。本地文件还会带来一个额外好处加载速度比 CDN 快答辩演示时不会出现图表闪一下才出来。5.5 清洗后条形图的“均价”出现异常大值现象正常城市均价 5 万左右结果某个区域柱子的均价冲到 20 万或者有负值。原因源数据里有异常值比如“单价”字段为0的条目或看成了“车位”的房源或者清洗时把“总面积”混成了单平米价。解决在groupby前加一层筛选df df[df[单价].between(1000, 200000)] # 按城市量级调整上下限再用结果自查把df.groupby(区域)[单价].agg([mean,median,count])打印出来中位数明显偏离均值时优先看是不是异常值造成的。这个坑在数据可视化系统设计中很典型因为图表太容易让人忽略数据本身的合理性答辩时老师看一眼就能发现问题。6. 换数据源也通用的技巧把整套系统改造成自己的城市或平台这套系统的真正价值不是说换一个城市就推倒重来而是把“爬取、清洗、可视化”三个环节解耦让每一层都能单独替换。我做这套改造时最常用的是一个 config 文件把所有可变参数收进去CONFIG { city_prefix: chengdu, # 链家城市前缀 max_page: 30, # 爬取页数 price_bins: [0, 15000, 25000, 40000, 60000, 100000], price_labels: [1.5万以下, 1.5-2.5万, 2.5-4万, 4-6万, 6-10万], exclude_keywords: [车位, 地下室], encoding: utf-8-sig }这样改造城市逻辑时只需改city_prefix和分箱参数爬虫、清洗、可视化代码一行都不用动。换成贝壳等其他平台时也只需要把parse_page里的字段抽取规则换掉后面清洗和展示部分继续复用这是这个项目结构最大的性价比所在。还有一个很实用的习惯把清洗后的结果直接存 SQLite而不是每次重新爬。df.to_sql(house, conn, if_existsreplace)之后再跑可视化时直接从数据库读能省掉一大半时间。我当时因为没有做这一层缓存同一个数据集反复爬了三遍才意识到“爬一次、洗一次、存起来、随便画”才是正确顺序。最后一个答辩高频技巧是把 ECharts 图表导出成静态 HTML。EChartssetOption之后调用chart.getDataURL()拿到图片 base64或者直接保存为 HTML 文件方便在没法启动 Python 服务的机器上演示。我习惯的做法是写一个小的导出脚本清洗完数据后自动生成一份“报告页”包含价格分布、户型占比和区域排名这样即使现场断网、后端起不来也能用浏览器打开静态页把结果讲清楚。有一次我在演示前 20 分钟 Flask 崩了靠的就是这份静态报告撑过了现场从那以后每次做这类数据可视化系统设计我都会强制多走一步——导出静态版再启动服务。这个方法虽然小但关键时刻能当后悔药用。希望帮到你。本文还有配套的精品资源点击获取