十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

房屋信息可视化与价格预测系统:Python爬虫到机器学习

房屋信息可视化与价格预测系统:Python爬虫到机器学习 简介一套基于Python的房屋信息可视化及价格预测系统毕业设计源码面向计算机相关专业毕业生或课程设计学习者解决房价数据采集、分析展示与预测需求。系统提供用户注册登录、首页信息展示、房价分析及房屋管理等功能支持爬取安居客、58同城等网站的房价数据并完成可视化分析。资源共310个文件以46个Python源码文件、17个HTML页面、17个CSS样式、43个JS脚本为主同时包含MySQL数据库脚本、CSV数据文件及PDF/Word/PPT说明文档压缩包约17.74MB目录结构清晰便于二次开发。目前已有41人学习。配套内容完整涵盖前后端源码、数据表结构、项目介绍文档等可帮助快速理解系统设计思路适合答辩参考与功能扩展。1. 房屋信息可视化与价格预测系统到底解决什么问题一个做毕设的同学找我调试这套基于 Python 的房屋信息可视化与价格预测系统时第一反应是预测房价的模型一定很复杂。实际把源码跑起来才发现最耗时间的模块是爬虫和可视化适配模型反而只占最后二十行代码。这个系统面向学校负责网络安全的老师这类运营角色提供用户注册登录、首页房源展示、房价分析可爬到安居客、58同城等站点的数据、预测价格展示和房屋管理等完整闭环。它的价值在于把爬虫—入库—建模—出图串成一条能直接演示的链路适合正在做 Python 毕业设计或课程设计的人作为二次开发基底。2. Python 3.6.8 MySQL 5.7 组合下的系统架构与数据库设计2.1 为什么是 3.6.8 和 5.7 这对老组合系统环境说明锁的是 Python 3.6.8 和 MySQL 5.7看到这个版本号别急着升级。requests、lxml、pandas 在 Python 3.6 上都有现成的 wheel 包装上就能 import不需要编译 C 扩展而 MySQL 5.7 的 JSON 类型、全文索引对住宅价格数据的存查冗余很小配合 Navicat11 的图形界面能省不少调试时间。关键是 Navicat11 连接 5.7 使用老式认证插件不会出现 MySQL 8.0 那类 caching_sha2_password 导致的连接失败。在课堂演示和答辩场景里这套组合的宗旨是少出意外而不是跑出极致性能。2.2 三张核心表用户、房源、预测缓存三张表的分工如下| 表名 | 用途 | 主要字段 | | user | 注册登录与角色管理 | username, password, role_type | | house_info | 爬虫房源数据落库 | district, unit_price, total_price | | price_predict | 预测结果缓存 | predict_price, model_version |用户表服务注册登录模块房源表是爬虫落库和可视化展示的数据源预测缓存表承接训练好的回归模型输出方便前端快速拉取。下面是建库建表脚本。CREATE DATABASE IF NOT EXISTS housing DEFAULT CHARACTER SET utf8mb4; USE housing; CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL COMMENT 登录名, password VARCHAR(255) NOT NULL COMMENT MD5加盐后的密码, real_name VARCHAR(64) DEFAULT NULL COMMENT 运营人员姓名, role_type TINYINT DEFAULT 1 COMMENT 1管理员 2运营, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE house_info ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL COMMENT 房源标题, district VARCHAR(64) DEFAULT NULL COMMENT 行政区, community VARCHAR(128) DEFAULT NULL COMMENT 小区名, house_type VARCHAR(32) DEFAULT NULL COMMENT 户型如3室2厅, area DECIMAL(8,2) DEFAULT NULL COMMENT 建筑面积单位㎡, total_price DECIMAL(12,2) DEFAULT NULL COMMENT 挂牌总价单位万元, unit_price DECIMAL(10,2) DEFAULT NULL COMMENT 单价单位元/㎡, source_site VARCHAR(32) DEFAULT NULL COMMENT 数据来源站点, source_url VARCHAR(512) DEFAULT NULL COMMENT 房源原始链接, crawl_time DATETIME DEFAULT NULL COMMENT 抓取时间, PRIMARY KEY (id), KEY idx_district (district), KEY idx_unit_price (unit_price) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE price_predict ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, house_id INT UNSIGNED NOT NULL, predict_price DECIMAL(12,2) DEFAULT NULL COMMENT 模型预测总价单位万元, model_version VARCHAR(32) DEFAULT NULL COMMENT 模型标识, predict_time DATETIME DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;SQL 里的 KEY 要说明idx_district 和 idx_unit_price 是可视化分析页的常见筛选列爬虫每天写入几千条时这两列不加索引的扫表会明显变慢用户名唯一索引则是注册模块并发防重的兜底。密码字段用 MD5 加盐而不是明文是这套系统后端校验逻辑里默认的约定盐值存在密码串里比对时先按分隔符拆出盐再重新拼接计算。DECIMAL 而不是 FLOAT是为了避免单价计算出现浮点尾巴。2.3 表字段设计时容易忽略的两个细节第一个细节是 source_url 的长度。安居客的单条详情链接往往超过 256 个字符如果字段给得太短爬虫入库时会在 INSERT 阶段报 Data truncation。第二个细节是 predict_time 的价值是配合 house_id 做同一房源多次预测的历史留痕前端展示最新一条即可因此不需要建联合唯一索引否则模型微调后重复写入会失败。补充一个我一般会在建表后立即执行的测试语句INSERT INTO user (username, password, real_name, role_type) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 系统管理员, 1);这里的密码串对应明文 123456MD5 后是常见的测试数据。登录模块拿到前端传参后先做同样的加密再和库中值比对不要直接用明文查询避免 SQL 日志里留下完整密码。源码里如果保留明文比对逻辑第一个要改的就是它。2.4 PyCharm 里快速跑通的环境配置步骤配置顺序我一般固定为先装 Python 3.6.8再装 MySQL 5.7 和 Navicat11最后打开 PyCharm 导入源码修改数据库连接。Python 安装时勾选Add Python to PATH可以省去后续解释器找不到的麻烦MySQL 安装向导里选好 root 密码后用 Navicat 新建连接并执行上面三个建表语句。1. 选择 File - Settings - Project:xxx - Python Interpreter指向 python3.6 2. 在 Terminal 里执行 pip install -r requirements.txt 3. 打开 db_config.py把 password 改成你的 MySQL root 密码 4. 运行 app.py 或 manage.py看到监听 5000 端口的日志即为成功提示如果 requirements.txt 缺失手动安装 flask、flask_sqlalchemy、pymysql、requests、beautifulsoup4、lxml、pandas、scikit-learn 这八个包就可以覆盖本系统的全部依赖。3. 房价数据爬虫模块从安居客响应到 MySQL 的数据管道3.1 选 requests BeautifulSoup 而不是 Scrapy这套系统面对的抓取规模是一个城市的在售房源大概万条量级频率是每天一到两次。用 Scrapy 可以但会引入 Item Pipeline、Downloader Middleware、Twisted reactor 这一套概念调试时出错堆栈比较长反而不利于课程设计演示。我一般的主线是 requests 负责请求、BeautifulSoup 负责解析、pymysql 负责入库三个部分各自能单测。如果后续要拓宽到多城市多站点再考虑用 Scrapy 包一层调度。3.2 一个可直接改用的抓取与入库示例下面这段抓取函数只关心列表页到房源字段的最小闭环以安居客列表页为例。import time import pymysql 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/120.0 Safari/537.36, Referer: https://beijing.anjuke.com/ } def fetch_page(city, district, page): url fhttps://{city}.anjuke.com/sale/{district}/p{page}/ resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 return resp.text这里的 Referer 是模拟从城市首页跳转到列表页缺少它时部分请求会直接返回 403。resp.encoding 强制为 utf-8因为列表页标题里混着中文地名默认猜测编码偶尔会出错。解析函数def parse_items(html): soup BeautifulSoup(html, lxml) items [] for li in soup.select(div.property-content): title_tag li.select_one(h3.property-content-title-strong a) if not title_tag: continue title title_tag.get_text(stripTrue) link title_tag.get(href) desc li.select_one(p.property-content-info-text) price_tag li.select_one(span.property-price-total-num) if price_tag is None: continue total_price float(price_tag.get_text(stripTrue)) items.append({ title: title, source_url: link, house_type: desc.get_text(stripTrue) if desc else , total_price: total_price, source_site: anjuke, crawl_time: time.strftime(%Y-%m-%d %H:%M:%S) }) return itemsBeautifulSoup 的 select 传的是 CSS 选择器property-content 是列表项容器类名实际运行时要端到端打印一遍 HTML 确认类名是否变化。列表页改版后常见的问题是 h3 里的标题选择器失效这时优先检查 li 和 h3 的 class 是否被加后缀。select_one 取不到元素时返回 None所以要判空再 continue否则 float(None) 会把整个任务炸停。入库函数def save_items(items): conn pymysql.connect( host127.0.0.1, userroot, password123456, databasehousing, charsetutf8mb4 ) try: with conn.cursor() as cursor: sql INSERT INTO house_info (title, house_type, total_price, source_site, source_url, crawl_time) VALUES (%s, %s, %s, %s, %s, %s) for it in items: cursor.execute(sql, ( it[title], it[house_type], it[total_price], it[source_site], it[source_url], it[crawl_time] )) conn.commit() finally: conn.close()这段用参数化 SQL 避免拼接注入风险execute 的第二个参数是元组顺序必须和 VALUES 占位符一致。crawl_time 写入的是当前服务器时间后续做按日增量统计时直接 GROUP BY DATE(crawl_time)。3.3 反爬、去重与失败重试的三张底牌单靠固定请求头跑几百页会被站点限流我一般把 UA、延时和去重一起处理现象直接原因常用处理403 或滑块请求频率太高、UA 被标记请求头里轮换 UA每次请求前 sleep 1~3 秒字段为 None页面结构改版或需要登录打印 HTML 片段重新核对 CSS 选择器重复数据同一链接被多次抓取以 source_url 为查重键入库前 SELECT 判断去重代码一般放在 save_items 之前def is_duplicate(conn, source_url): with conn.cursor() as cursor: cursor.execute( SELECT COUNT(*) FROM house_info WHERE source_url %s, (source_url,) ) return cursor.fetchone()[0] 0提示sleep 不宜写死用随机的 0.5 到 1.5 秒能明显降低连续请求特征。每个城市每个区域跑一个循环推荐把缩略图地址也存下来可视化模块做小区封面时会用到。3.4 爬虫跑批的入口与日志输出抓取入口我习惯放在 crawler/main.py逐区域、逐页串行执行并把每页成功条数和失败原因写进根目录的 crawl.logimport logging logging.basicConfig(filenamecrawl.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s) def run(districts): total 0 for district in districts: for page in range(1, 4): try: html fetch_page(beijing, district, page) items parse_items(html) save_items(items) total len(items) logging.info(%s p%d saved %d, district, page, len(items)) except Exception as exc: logging.exception(%s p%d failed: %s, district, page, exc) print(total saved:, total)如果某页解析异常logging.exception 会输出完整堆栈而不是让整个爬虫中断。课程设计答辩时直接展示 crawl.log 里的分段记录比口头解释抓了多少条有说服力。4. 房屋信息可视化大屏与价格预测模块的实现细节4.1 源码包里那些 CSS 文件分别承担什么职责项目根目录有一套前端静态资源初次看的人容易被文件数量劝退其实每个文件的分工很明确表单、表格、弹窗、图表分别由不同框架支撑bootstrap.min.css 提供栅格和基础组件layui.css 负责后台管理界面风格bootstrap-icons.css 和 all.min.css 是两套图标字体layer.css 和 laydate.css 分别给弹出层、日期控件使用tempusdominus-bootstrap-4.min.css 是 Bootstrap4 风格的 datetimepicker。它们之间没有冲突因为 class 命名空间不同。文件在页面里的作用bootstrap.min.css响应式栅格、按钮、表单控件layui.css后台 layout、导航菜单、表格样式bootstrap-icons.css / all.min.css图标字体用于菜单和按钮layer.cssiframe 弹窗、loading 遮罩laydate.css / tempusdominus-bootstrap-4.min.css日期范围选择器另外两个不常动的 cf.errors.css 和 layui.mobile.css 属于页面错误提示和移动端适配样式本地调试时基本不会走到分支。4.2 列表页与详情页的数据接口约定前端展示依赖后端 JSON 接口核心是两个/api/house/list 供房屋管理表格分页查询/api/price/trend 供价格趋势图使用。接口返回结构统一为 {code, msg, data}前端拿到 code 为 0 才渲染 data。下面以 /api/price/trend 为例返回最近 30 天各城区的单价中位数。from flask import jsonify, request app.route(/api/price/trend) def price_trend(): district request.args.get(district, ) sql SELECT DATE(crawl_time) AS d, district, ROUND(AVG(unit_price), 2) AS avg_price FROM house_info WHERE district %s AND crawl_time DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY DATE(crawl_time), district ORDER BY d rows query_db(sql, (district,)) return jsonify({code: 0, msg: ok, data: rows})GROUP BY DATE(crawl_time) 会把同一天同一城区的多套房源聚合成一行AVG 得到当天均价。接口参数 district 可选为空时返回全部城区的对比数据前端用图例区分。4.3 ECharts 面积图和散点图的渲染示例可视化页引入 echarts.min.js 后用 fetch 拉上面接口再把 data 拆成 x 轴日期和 y 轴价格两组数组fetch(/api/price/trend?districtchaoyang) .then(res res.json()) .then(res { if (res.code ! 0) return; // 把后端行记录拆成 ECharts 需要的平行数组 const dates res.data.map(row row.d); const prices res.data.map(row row.avg_price); const chart echarts.init(document.getElementById(trendChart)); chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: dates }, yAxis: { type: value, name: 单价(元/㎡) }, series: [{ name: 朝阳区均价, type: line, smooth: true, areaStyle: {}, data: prices }] }); });map 两次遍历前端数组把后端行记录拆成 ECharts 需要的平行数组smooth 会让折线变圆滑价格波动大时看趋势更直观areaStyle 给折线下面的区域填充半透明颜色这就是可视化大屏观感的主要来源。模块里还可以用相同思路渲染各户型总价的 boxplot。4.4 价格预测模型特征工程与训练接口预测模块的目标是基于历史房源数据预测总价特征建议选面积、户型、面积单价和房龄这些字段都在 house_info 或后续清洗结果里。源码中常见做法是用 scikit-learn 的 LinearRegression 训练再把测试集 R² 打印出来。import pandas as pd from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split def train_and_predict(): df pd.read_sql(SELECT area, house_type, total_price FROM house_info, conn) df[room_count] df[house_type].str.extract(r(\d)室)[0].astype(float) df df.dropna(subset[area, room_count, total_price]) X df[[area, room_count]] y df[total_price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) model LinearRegression() model.fit(X_train, y_train) return model.score(X_test, y_test)正则提取户型字符串里的室前面的数字把文本特征转成数值特征dropna 删除缺面积或总价的脏行random_state 设置后每次训练集的划分结果一致答辩时复现分数不会变。model.score 返回的是决定系数0.7 以上就说明面积和居室数对总价有较强的解释力。训练完成后把 model 用 joblib 保存预测接口读入模型后对前端传入的户型参数返回总价。5. 进阶技巧价格预测结果缓存与定时增量爬取5.1 用 APScheduler 做定时增量爬取爬虫不建议用 while True sleep 裸跑关掉终端进程任务就断了答辩在即没人守在电脑前。我一般会在项目里挂 APScheduler 的 BlockingScheduler每天凌晨两点触发一次增量抓取配合 source_url 去重实现只补新增不改旧数据。from apscheduler.schedulers.blocking import BlockingScheduler def start_crawler(): scheduler BlockingScheduler() scheduler.add_job(run, cron, hour2, minute0, args[[chaoyang, haidian]]) scheduler.start()cron 触发器在凌晨两点执行一次避开了房源站点白天的高峰时段args 传的列表对应 run 函数的 districts 参数。如果环境里装不了 APScheduler用操作系统 crontab 一样能调度命令是# 每天凌晨2点执行增量爬取 0 2 * * * cd /opt/housing_project /usr/local/bin/python3.6 crawler/main.py5.2 把模型序列化而不是每次重训预测接口如果每次请求都重新 train_and_predict耗时会增加十几倍。常见做法是训练一次后 joblib.dump 到 models/price_model.pkl预测接口读取模型对象省掉 pandas 读库和 fit 的开销。import joblib joblib.dump(model, models/price_model.pkl) loaded joblib.load(models/price_model.pkl) predict_price loaded.predict([[88.5, 3]])[0]predict 接收二维数组[[88.5, 3]] 表示面积 88.5 平方米、3 室返回值是单元素数组取 [0] 得到预测总价。模型文件在服务器重启后依然存在不会因为进程退出而丢失。5.3 让预测误差展示在可视化页面上可以把测试集每套房子的真实总价和预测总价拼成一个 scatter 数据集前端用 ECharts 散点图画出来能直接回答模型哪些区间预测得准。pred model.predict(X_test) # scatter_data 每个元素为 [真实总价, 预测总价] scatter_data [ [round(float(actual), 2), round(float(p), 2)] for actual, p in zip(y_test.values, pred) ]scatter_data 每一项是 [真实总价, 预测总价]点在 yx 直线附近越密集代表误差越小。可视化页面加一条 reference line 后把这份散点图直接放进答辩 PPT 的模型验证页即可。本文还有配套的精品资源点击获取
返回列表