免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Python爬虫+Flask+Echarts构建房产数据可视化系统实践

Python爬虫+Flask+Echarts构建房产数据可视化系统实践 简介论文围绕基于大数据Python爬虫的房产数据可视化分析系统展开面向大数据专业学生和毕业设计开发者针对房产数据获取难、展示不直观的问题提供了一套完整的系统设计思路。资源共1个文件为docx格式压缩包大小1.47MB已有96人浏览学习。论文按绪论、相关技术、需求分析、系统设计的章节推进覆盖建筑朝向供暖占比、房屋租金区间、户型占比、面积租金走势、总价与建筑面积分布及聚类分析、房租预测等主要功能并讨论了Python爬虫、Flask框架、MySQL存储和Echarts可视化等关键技术的协同方式。读者可借此快速理解房产数据可视化系统的实现路径也能参考其章节框架与功能设计用于毕业设计、课程项目或实际开发。1. 从爬虫到看板房产数据可视化系统的真实构成一位做房产投资的朋友曾经吐槽想了解某新区的小区租金和户型配比得先打开五六个网站手动记数据。后来我用 Python 爬虫把房源信息抓下来存入 MySQL再用 Flask 提供查询接口前端用 Echarts 画成可视化看板整个过程从手动变成一键刷新。这篇文章会拆开这套系统requests 爬虫如何控制抓取页数、Flask 路由如何对接 SQL 聚合、Echarts 的 option 配置怎么和后台字段对应以及聚类和房租预测的简单实现。适合刚完成 python 爬虫教程、想做一个完整数据可视化大屏项目的读者也适合正在做大数据相关毕业设计需要源码参考的人。爬虫不等于批量下载核心是建立一条可复用的数据管道。这个项目里管道的入口是几个列表页 URL经过请求、解析、清洗、入库最后通过 Web 接口呈现在浏览器里。每一环都有容易忽略的细节比如请求头不完整会返回 418面积字段带着单位会让聚类结果完全失真MySQL 字符集选错会导致中文乱码。下面按我实际开发的顺序从最底层的爬虫一直讲到可视化最后给出聚类和预测的落地代码。你不需要一次跑通全部但可以参考每个模块的边界和参数设计。2. 数据采集层requests爬虫与MySQL写入的工程化细节2.1 请求策略headers、session 与页数控制爬虫第一件事不是写解析规则而是让服务器认为你是普通浏览器。常见做法是组装一个请求头包含 User-Agent、Referer、Accept-Language 等字段。为了避免并发过高被封我会用 requests.Session 保持连接并设置一个简单的页数上限。以下代码来自这套系统的数据爬取模块import requests import re import json import pymysql headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example-realty-site.com/, } session requests.Session() session.headers.update(headers) def get_index(url): html session.get(url, timeout10) if html.status_code 200: get_data(html) else: print(请求页面{}出错状态码{}.format(url, html.status_code)) index 45 for url in urls: page_index 46 - index print(现在爬取的是第{}页.format(page_index)) index - 1 get_index(url)这段代码里session会复用底层 TCP 连接比每次都新建requests.get更高效。index 45的作用是限制总页数避免海量请求打爆目标服务器也保证数据的时效性。真实项目中建议把页数上限做成配置项或者用time.sleep(random.uniform(1, 3))控制请求频率这属于分布式爬虫里最基本的礼貌。需要说明的是这里没有用 Scrapy 这类重型框架因为目标数据量在万级以内单机 requests 足够。如果你要抓的网站有登录校验还需要在 headers 里带上 Cookie或者用session.post先登录这也是 python 爬虫教程里最容易忽略的一环。这里只讨论对公开数据的合理抓取不要跳过登录验证或阻塞接口爬虫的工程目标是降低重复劳动而不是突破访问边界。注意任何爬虫项目都应遵守目标网站 robots 协议和访问频率限制。这里讨论的是对公开数据的合理抓取不要用多线程突然打满对方带宽出了问题先检查自己的延时和 User-Agent。2.2 解析房源数据正则提取与 JSON 转换目标网站多数会把房源数据直接渲染在window.__SEARCH_RESULT__这个变量里所以不需要 BeautifulSoup 遍历 DOM直接正则匹配再转 JSON 就够了。re.findall拿到的是一个字符串经过json.loads解析成字典再按字段抽取。下面演示关键解析代码def get_data(html): # 从响应体里提取 JSON 数据 html_data re.findall(rwindow.__SEARCH_RESULT__ (.*?)/script, html.text)[0] json_data json.loads(html_data) houses json_data[engine_jds] for house in houses: record { title: house.get(title), price: house.get(price), type: house.get(house_type), area: house.get(area), address: house.get(address), status: house.get(status), tag: house.get(tag), } save_to_mysql(record)这里有个细节正则中的(.*?)是非贪婪匹配如果页面结构里出现多个script很容易匹配到第一个结束标签。我的做法是先按/script分割响应体再定位包含__SEARCH_RESULT__的那一段。json_data[engine_jds]是房源列表的键名不同网站字段名不同需要你自己打印json_data.keys()确认。解析后的数据还会做一轮清洗。比如面积字段里混着“㎡”价格字段是“3000元/月”需要统一去掉单位并转成 float。空值和重复记录也要处理通常是先判断字段是否存在再用 dict 的 key 作为去重依据。这里不再贴循环代码但你可以把json_data转成 pandas DataFrame然后调用drop_duplicates和fillna这会比手写循环快很多。清洗后的字段会直接影响后面的 SQL 聚合所以这一步宁可多写几个分支也不要直接入库。2.3 MySQL 表设计字段类型与写入存储这块我直接用了 pymysql因为和 Flask 生态兼容好。设计表之前先明确要存哪些业务字段房屋 ID 编号、标题、价格、类型、地区、地址、状态、标签。下面是简化后的建表语句CREATE TABLE house_data ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 房屋Id编号, title VARCHAR(50) NOT NULL COMMENT 标题, price VARCHAR(50) NOT NULL COMMENT 价格, type VARCHAR(50) COMMENT 户型, area VARCHAR(50) COMMENT 面积, address VARCHAR(50) COMMENT 地址, status VARCHAR(50) COMMENT 状态, tag VARCHAR(50) COMMENT 标签 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段类型的选择有一个容易踩的坑价格和面积不要直接用 DECIMAL因为原始爬取数据里常带着单位或范围值比如“3000-3500元”。如果强行转换解析异常时整条记录会丢失。我会先把原始字符串存进 VARCHAR等分析阶段再用 SQL 的CAST或者 Python 二次清洗成数值。接下来是写入函数。数据库连接信息应该从环境变量读取而不是写死在代码里。getConn()和getEngine()这两种叫法在不同项目里都存在本质上都是返回一个连接对象。def getConn(): return pymysql.connect( hostlocalhost, userroot, passwordyour_password, databasehouse_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) def save_to_mysql(record): sql INSERT INTO house_data (title, price, type, area, address, status, tag) VALUES (%(title)s, %(price)s, %(type)s, %(area)s, %(address)s, %(status)s, %(tag)s) conn getConn() cursor conn.cursor() cursor.execute(sql, record) conn.commit() cursor.close() conn.close()这里用%(title)s这种命名占位符比%s更直观尤其字段一多不容易传错顺序。cursorclassDictCursor让查询结果以字典形式返回方便后续在 Flask 里直接jsonify。commit必须显式执行否则事务不会自动提交这是新手最容易遗忘的一步。表结构设计完成后建议加上CREATE INDEX idx_price ON house_data(price)。虽然数据量小的时候用不到但一旦你要做基于价格区间的聚合查询索引能明显加快速度。在数据爬虫场景里存储阶段最常见的异常是主键冲突和字段长度超限前者可以用INSERT IGNORE跳过重复后者需要你在清洗阶段限制字符串长度或者把字段长度从 50 调整到 100。把这一层做好上层 Flask 分析接口才能稳定返回数据。3. Flask服务中的分析模块SQL聚合与Pyecharts接口设计3.1 为什么用Flask而不是Django系统不需要用户管理、后台权限这类重功能只需要提供几个接口给前端页面拉数据Flask 的轻量特性就成了优势。它的路由注册方式直观一个函数对应一个 URL配合jsonify可以直接返回 JSON。和 Django 相比Flask 的 ORM 也不是必需品直接用 pymysql 写 SQL 反而更可控尤其在做多表关联或聚合统计时你能看到完整的执行计划。在设计接口之前先想清楚数据流向浏览器发起请求Flask 路由收到参数后调用分析函数分析函数拼接 SQL 查询 MySQL拿到结果转成 list最后jsonify返回。业务逻辑并不复杂所以 Flask 的蓝图和工厂模式在这个体量下也显得多余。保持单文件路由按模块切分函数就能满足开发效率。3.2 主程序入口与数据读取封装在拆后台接口之前先把主程序入口写清楚。这里用最简单的app.run启动开发服务器from flask import Flask, jsonify, render_template import pymysql app Flask(__name__) def sql(sql): conn getConn() cursor conn.cursor() cursor.execute(sql) data cursor.fetchall() conn.commit() cursor.close() conn.close() return data app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)sql函数是整个数据分析模块的地基。注意cursor.execute(sql)之后用fetchall()拿到所有记录conn.commit()在只有 SELECT 的场景下不是必须的但考虑到后续可能有写入操作保留也无妨。参数化查询才是重点尽量不要用 f-string 拼接 SQL否则遇到单引号很容易报错还有注入风险。host0.0.0.0让局域网内的机器也能访问方便手机调试前端样式。3.3 分析接口户型占比与租金走势系统里最常用的两个分析是“户型占比”和“租金走势”。前者统计不同户型的数量后者按面积分组看租金均值。这两个查询有一个共同点原始数据中type可能是“3室2厅/南北”需要用SUBSTRING_INDEX切分。下面是户型占比的接口代码app.route(/api/type_ratio) def type_ratio(): data sql( SELECT SUBSTRING_INDEX(type, /, 1) AS house_type, COUNT(*) AS count FROM house_data WHERE type IS NOT NULL AND type ! GROUP BY house_type ORDER BY count DESC LIMIT 10 ) return jsonify(data)SUBSTRING_INDEX(type, /, 1)的意思是从type字段左侧开始截取到第一个斜杠之前。如果原始值是“3室2厅/南北”结果就是“3室2厅”。LIMIT 10保证 Top10 户型否则饼图里会有几十个碎片扇形。真实场景里还要考虑“/”不存在的情况SUBSTRING_INDEX会返回整个字段所以需要加WHERE type LIKE %/%或用IF(INSTR(type,/), SUBSTRING_INDEX(...), type)兜底。再看租金走势接口。这里的需求是“各面积租金走势”也就是不同面积区间对应的平均租金。常见做法是先按面积字段算出区间再求平均值。面积字段清洗后存成 VARCHAR所以要用CONVERT转成 DECIMALSELECT CONCAT(FLOOR(CAST(area AS DECIMAL(10,2)) / 20) * 20, -, FLOOR(CAST(area AS DECIMAL(10,2)) / 20) * 20 20) AS area_range, ROUND(AVG(CAST(REPLACE(price, 元/月, ) AS DECIMAL(10,2))), 2) AS avg_price FROM house_data WHERE area REGEXP ^[0-9.]$ GROUP BY area_range ORDER BY area_rangeFLOOR(CAST(area AS DECIMAL) / 20) * 20是把面积映射到 20 平方米的整数倍区间比如 65 平米落入 60-8085 落入 80-100。REPLACE(price, 元/月, )去掉单位。这里有一个非常关键的过滤条件WHERE area REGEXP ^[0-9.]$它能剔除那些“暂无数据”或带汉字的垃圾记录。如果不用这个过滤CAST会返回 0聚类和走势图都会出现异常点。3.4 业务统计接口的设计边界除了上述两个系统还设计了“建筑朝向与供暖类型占比”“租金分布区间”“总价建筑面积趋势”等接口。这些接口模式都一样区别只在 SQL 和返回结构。我的建议是每个接口保持“只做一件事”的原则前端需要两个数据就请求两次不要硬拼一个复杂 JSON。缓存也很重要可以把sql()的查询结果放进 redis或者内存字典里设置过期时间避免每次打开看板都全表扫描。这个阶段还要考虑接口异常时的返回值例如try except包住查询并把错误信息写到日志前端才能及时判断是接口挂了还是数据为空。另外如果项目用了 Pyecharts 库来生成图表后端可以直接返回 Echarts 渲染页面而不只是 JSON。Pyecharts 的Bar、Pie、Line等组件可以和 Flask 的render_template结合。但要注意 Pyecharts 版本升级后 API 变动大比如page类在 v2 和 v1 里的导入路径不同网上很多旧教程会报错。遇到这类问题最好先pip list看版本再对照官方文档改 import。这里为了保持前端可控我的做法是后端只给 JSON前端用 Echarts 原生语法渲染减少一层中间库的心智负担。注意如果爬虫抓到的数据本身带有明显的反爬标记比如所有价格都变成 0那问题大概率不在 Flask而在数据入库前的清洗逻辑。先查原始响应体再看 SQL 聚合结果别急着改前端。4. 数据可视化落地Echarts各图表与前端参数配置4.1 为什么选择EchartsPython 生态里做可视化首选的可能是 Matplotlib但它是静态图片没法鼠标悬停看数值也没法和地图、大屏联动。Echarts 的交互能力更强文档里提供了大量示例可以直接把 option 结构复制到前端代码。而且它对前端开发者友好只要会写 JavaScript 就会调。所谓的“数据可视化大屏”很多就是基于 Echarts 加一个深色背景。在这套系统里我选择 Echarts 而不选 Pyecharts是因为前端需要更多的定制动作比如点击图表区块联动别的模块直接用原生 Echarts 更好控制。4.2 前端页面与图表容器Flask 的render_template(index.html)会加载templates/index.html。页面里每个图表占一个div必须给这个容器设置宽度和高度否则图表画不出来。下面是一个最小可用的模板片段div idtype-ratio-chart stylewidth: 100%; height: 400px;/div script src{{ url_for(static, filenameecharts.min.js) }}/script script fetch(/api/type_ratio) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(type-ratio-chart)); chart.setOption({ title: { text: 户型占比分析 }, tooltip: { trigger: item }, legend: { orient: vertical, left: left }, series: [{ name: 户型, type: pie, radius: 55%, data: data.map(item ({ name: item.house_type, value: item.count })) }] }); }); /script这里用原生fetch去请求 Flask 接口不需要 jQuery。data.map(item ({name: item.house_type, value: item.count}))把后端返回的列表转换成 Echarts 需要的{name, value}结构。如果接口返回的是 None 或者空列表chart.setOption会显示空白前端最好加一层判空逻辑否则用户看到的是白屏。echarts.init必须在 DOM 已经挂载后调用如果这段脚本写在div前面容器还没生成初始化就会失败。4.3 六种图表的数据映射系统里要展示的图表类型很固定我整理了一张对应表方便你后续复用图表类型数据接口核心 option 字段关键说明饼图/api/type_ratioseries.typepie字段切分后按 count 排序限制 Top10柱状图/api/rent_distributionxAxis.data, series.data租金区间做横轴数量做纵轴折线图/api/rent_trendxAxis.data, series.data面积区间为横轴平均租金为纵轴散点图/api/price_area_trendseries.data[[price, area]]总价和建筑面积的二维分布词云/api/tag_wordcloudseries.typewordCloud需要额外引入 echarts-wordcloud 扩展地图/api/area_price_mapseries.typemap地区字段需要匹配地图经纬度文件这张表里最容易出错的是词云和地图。词云不是 Echarts 内置类型必须额外加载echarts-wordcloud.min.js否则会报Unknown series wordCloud。地图需要注册 GeoJSON 数据如果不在目标城市需要从外部源下载或使用registerMap接口。这两个扩展包都会增加页面体积建议用动态加载而不是直接引入。我更常用的是散点图和折线图因为它们对数据分布形态的表达最直接。4.4 解决常见渲染问题图表第一次渲染正常刷新后报“Cannot read properties of undefined”是最常见的问题。原因一般是 fetch 返回的数据还没到位图表容器已经初始化。解决办法是把初始化和数据请求放进Promise里或者先charts echarts.init(element)再在then里setOption顺序不能反。另一个问题是容器宽度为 0。Echarts 初始化时如果容器是隐藏的或者 CSS 用了display:none图表会被画成 0x0。我会在setOption后调用chart.resize()并且监听窗口 resize 事件。具体做法可以在window.addEventListener(resize, () chart.resize())。如果页面用的是 Bootstrap 栅格还需要等轮播或 Tab 切换完成后手动触发 resize。后端地址跨域也是高频问题。Flask 默认同源如果前端部署在 5000 端口后端也是 5000就没有问题。但如果前端单独放在 8080请求 5000 接口会报 CORS 错误。最简单的解决方案是使用 Flask-Cors 扩展在app Flask(__name__)后加CORS(app)。本地调试阶段保持同源即可部署到生产环境再用 Nginx 反代统一入口可以省掉很多跨域麻烦。注意Echarts 图表初始化必须在 DOM 已挂载的情况下进行否则init会返回空对象。可以把所有setOption放进一个renderAllCharts()函数在页面 onload 和 fetch 完成后各调用一次。5. 进一步拆解聚类分析与房租预测的实用技巧5.1 KMeans 聚类对总价与面积做分组聚类分析要回答的问题是“哪些房产是同类”。以total_price和area两个维度为例直接做 KMeans 会受量纲影响总价动辄百万面积只有几十聚类结果会偏向价格。需要做标准化。代码如下import pandas as pd from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler df pd.read_sql(SELECT total_price, area FROM house_data, getConn()) df df.dropna() X df[[total_price, area]].values X_scaled StandardScaler().fit_transform(X) kmeans KMeans(n_clusters4, random_state42) df[cluster] kmeans.fit_predict(X_scaled)StandardScaler会把每个特征变成均值为 0、方差为 1 标准化值KMeans 是基于距离的算法必须这么做。n_clusters4不是拍脑袋可以跑一组silhouette_score选轮廓系数最大的值。如果你不想在代码里耦合 sklearn可以直接在 SQL 中按面积和总价的乘积或比值做 RANK 分区也能得到粗粒度的分类但不如 KMeans 细腻。聚类结果最终可以映射到散点图用不同颜色区分簇别。5.2 房租预测线性回归的最小实现房租预测最难的部分不是模型而是特征构造。面积、户型、朝向、所属区域、楼层、装修状态都可以转成数值特征。一个可用的最小模型是用面积和户型房间数做特征然后调用线性回归from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split features df[[area, bedroom, livingroom]] features[bedroom] features[bedroom].fillna(0) X_train, X_test, y_train, y_test train_test_split( features, df[rent], test_size0.2, random_state42 ) model LinearRegression() model.fit(X_train, y_train) print(R2:, model.score(X_test, y_test))这里把户型字段拆成bedroom和livingroom两个数值列字符串转数字可以用pd.get_dummies。线性回归给出一个可解释的 baseline如果 R2 能到 0.6 以上就能用于决策。如果效果不够好可以尝试随机森林或 XGBoost但注意对房租这种长尾分布数据要对目标值做np.log1p平滑否则少数高价房会拉偏模型。5.3 从查询到服务把模型结果暴露成接口聚类和预测最终要通过 Flask 接口提供给前端。常见做法是把训练好的模型序列化到磁盘接口加载后返回当前输入的预测值。这样每次刷新看板不需要重新训练也能保证响应速度。代码片段import joblib # 训练结束后保存 joblib.dump(model, rent_model.pkl) # Flask 接口里加载并预测 model joblib.load(rent_model.pkl) app.route(/api/predict, methods[POST]) def predict(): data request.get_json() pred model.predict([[data[area], data[bedroom], data[livingroom]]]) return jsonify({rent: pred[0]})joblib是 sklearn 官方推荐的序列化工具比 pickle 更快也支持 numpy 数组。接口里request.get_json()拿 POST body前端传参时要注意字段名和训练特征顺序一致。此处如果能加入业务规则会更实用比如面积小于 10 平方米的隔断房直接返回 0这比模型输出更符合现实约束。最后一个技巧训练集聚类结果可以导出成 CSV 或写回 MySQL作为cluster_id字段冗余存储。这样在可视化看板上要按“同类房源”筛选时不需要每次跑模型查询直接走索引响应时间会短很多。本文还有配套的精品资源点击获取
返回列表