Python房价预测系统实战:从requests爬虫到MySQL落库与模型部署
发布时间:2026/9/30 16:39:01来源:尧图网络
简介这是一份面向计算机专业毕业设计场景的完整论文文档主题为基于Python的房价预测系统适合正在准备毕设选题、撰写论文或需要参考同类项目实现思路的本科生与指导教师。压缩包内共1个doc文件约2.55MB即论文正文本身涵盖摘要、绪论、爬虫原理与信息提取、MySQL数据库设计、机器学习建模及系统测试等章节结构完整、层次清晰。论文以58同城网房屋数据为研究对象结合BeautifulSoup、Scrapy等爬虫模块与Pandas、NumPy、Matplotlib等分析库并运用线性回归、决策树、随机森林、神经网络等算法建立预测模型同时涉及前台查询、可视化大屏与后台管理等功能设计。目前已有373人学习可为读者提供选题论证、技术选型、章节组织与写作规范等方面的直接参考帮助快速理清毕设论文的框架与实现脉络。1. 从一份房价预测论文.doc说起数据从哪来、模型怎么选、结果怎么存很多人做「基于Python的房价预测系统」时卡住的地方根本不是模型而是数据从哪来、字段怎么对齐、预测结果往哪存。论文里写「采用线性回归与随机森林对比R²达到0.89」很轻松但真到动手你会发现房价数据没有现成干净的表爬下来的字段名五花八门MySQL建表时价格字段类型选错模型跑出来的结果根本没法回填。这篇笔记就按一条能跑通的链路来讲用 requests 爬公开房源列表页清洗后落到 MySQL再用 scikit-learn 做回归对比最后把预测值写回数据库供查询。适合正在写房价预测方向论文、需要一套可复现工程骨架的人也适合刚学完 python 基础、想找一个完整项目练手的同学。波士顿房价预测那套内置数据集只能验证算法真要做「系统」数据管道和存储这一层绕不过去。2. 数据管道先立住requests 抓取与 MySQL 落库的完整链路2.1 为什么不用现成数据集非要自己爬波士顿房价预测数据集只有506条、13个特征特征还是1978年波士顿的犯罪率、房间数这类指标拿它训练出来的模型放到今天的房源场景里特征对不上、量纲对不上、价格分布也对不上。论文里如果只跑这个数据集答辩时很容易被问「你的系统和真实房价有什么关系」。常见做法是找公开的房源列表页抓取标题、区域、面积、户型、楼层、朝向、总价这几个字段自己构建一份和业务贴近的数据集。爬虫不是为了炫技是为了让后面的特征工程有东西可做。选 requests 而不是 selenium是因为列表页的数据通常直接渲染在 HTML 里用 requests 拿到响应后交给 BeautifulSoup 或 lxml 解析就够了速度快、资源占用低。只有当页面是前端异步渲染、HTML 里拿不到数据时才需要上 selenium。这一点在选型时要先确认否则会白写很多代码。2.2 抓取脚本请求头、分页与字段抽取import requests from bs4 import BeautifulSoup import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, } def fetch_page(url): # 每次请求间隔随机降低被限流的概率 time.sleep(random.uniform(1.5, 3.0)) resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding # 防止中文乱码 return resp.text def parse_list(html): soup BeautifulSoup(html, lxml) rows [] for item in soup.select(div.house-list-item): rows.append({ title: item.select_one(h3).get_text(stripTrue), region: item.select_one(span.region).get_text(stripTrue), area: item.select_one(span.area).get_text(stripTrue), layout: item.select_one(span.layout).get_text(stripTrue), total_price: item.select_one(span.price).get_text(stripTrue), }) return rows if __name__ __main__: all_rows [] for page in range(1, 51): # 抓50页约1500条 url fhttps://example.com/list?page{page} html fetch_page(url) all_rows.extend(parse_list(html)) print(抓取条数:, len(all_rows))这段代码里HEADERS里的 User-Agent 是必须的不带的话很多站点直接返回403。time.sleep加随机间隔是基本的礼貌抓取也是降低被封的手段。resp.apparent_encoding比写死utf-8稳因为不同站点编码不统一。parse_list里的 CSS 选择器要按目标站点的实际结构改这里只是示意。分页范围别一上来就设几千页先抓50页验证字段完整性再决定要不要扩量。提示抓取前先看目标站点的 robots.txt控制频率别把对方服务器打挂。这是工程底线不是可选项。2.3 清洗把「3室2厅」和「89.5㎡」变成数值爬下来的字段是字符串模型吃不了。面积要去掉「㎡」户型要拆成室和厅两个数字总价要去掉「万」并转成 float。这一步用 pandas 做最顺手。import pandas as pd import re def clean_area(x): m re.search(r([\d.]), str(x)) return float(m.group(1)) if m else None def clean_layout(x): m re.search(r(\d)室(\d)厅, str(x)) return (int(m.group(1)), int(m.group(2))) if m else (None, None) def clean_price(x): m re.search(r([\d.]), str(x)) return float(m.group(1)) if m else None df pd.DataFrame(all_rows) df[area] df[area].apply(clean_area) df[total_price] df[total_price].apply(clean_price) df[[rooms, halls]] df[layout].apply(lambda x: pd.Series(clean_layout(x))) df df.dropna(subset[area, total_price, rooms]) df[unit_price] df[total_price] * 10000 / df[area] # 元/平米 df.to_csv(houses_clean.csv, indexFalse, encodingutf-8-sig)clean_area用正则提取数字兼容「89.5㎡」和「89.5平米」两种写法。clean_layout返回元组再拆成两列比在 DataFrame 里逐行判断干净。unit_price是单价后面做特征时比总价更有可比性。dropna只删关键字段缺失的行不要一上来就dropna()全表否则会误删大量样本。存 CSV 时用utf-8-sig用 Excel 打开不会乱码。2.4 落库MySQL 建表与 SQLAlchemy 写入MySQL 建表时价格和面积用DECIMAL而不是FLOAT因为浮点数在比较和聚合时会有精度问题。区域、户型这种短文本用VARCHAR标题可能较长用VARCHAR(255)。CREATE TABLE house ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255), region VARCHAR(64), area DECIMAL(8,2), rooms INT, halls INT, total_price DECIMAL(10,2), unit_price DECIMAL(10,2), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;写入用 SQLAlchemy比手写 INSERT 拼接字符串安全也方便后面和 pandas 配合。from sqlalchemy import create_engine import pandas as pd engine create_engine( mysqlpymysql://root:yourpassword127.0.0.1:3306/house_db?charsetutf8mb4 ) df pd.read_csv(houses_clean.csv) df.to_sql(house, engine, if_existsappend, indexFalse) print(写入完成:, len(df))连接串里mysqlpymysql表示用 pymysql 驱动charsetutf8mb4必须带否则中文区域名会报错。if_existsappend是追加第一次建表后重复跑不会覆盖。如果报Cant connect to local MySQL server through socket说明 MySQL 服务没起或者端口不对先确认服务状态再查连接串。注意生产环境别用 root 直连建一个只有 house_db 权限的账号。密码不要写死在代码里用环境变量读。3. 特征工程与模型对比线性回归和随机森林到底差在哪3.1 特征怎么选、怎么编码清洗后的表里有 area、rooms、halls、region、unit_price。预测目标是 total_priceunit_price 不能作为特征因为它和目标有直接换算关系放进去就是数据泄漏R²会虚高到0.99但模型没有实际意义。region 是类别特征用 one-hot 编码别用序号编码因为区域之间没有大小关系。from sklearn.model_selection import train_test_split from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import r2_score, mean_absolute_error features [area, rooms, halls, region] target total_price X df[features] y df[target] pre ColumnTransformer([ (cat, OneHotEncoder(handle_unknownignore), [region]), ], remainderpassthrough) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) lr Pipeline([(pre, pre), (model, LinearRegression())]) rf Pipeline([(pre, pre), (model, RandomForestRegressor( n_estimators200, max_depth12, random_state42 ))]) for name, model in [(线性回归, lr), (随机森林, rf)]: model.fit(X_train, y_train) pred model.predict(X_test) print(name, R2%.3f % r2_score(y_test, pred), MAE%.2f万 % mean_absolute_error(y_test, pred))ColumnTransformer把类别列单独做 one-hot数值列原样保留这样不用手动拆表。handle_unknownignore保证测试集出现训练集没见过的区域时不会报错。随机森林的n_estimators200是树的数量太少欠拟合太多训练慢200是一个常用起点max_depth12限制深度防止过拟合房价数据特征不多深度不用给太大。3.2 两个模型的差异与选型线性回归假设特征和目标之间是线性关系面积每增加一平米总价增加一个固定系数。现实中房价和面积的关系大致线性但区域的影响不是线性的——核心区域和远郊的价差可能是指数级的。随机森林能捕捉这种非线性通常 R²会高一些但代价是模型不可解释你没法像线性回归那样直接读出「面积每平米值多少钱」。论文里如果要做对比建议两个都跑用 R²和 MAE 两个指标一起看。R²衡量拟合优度MAE 衡量平均预测误差多少万后者对业务方更直观。如果线性回归 R²0.75、随机森林 R²0.85差距明显就选随机森林如果两者接近选线性回归因为可解释性强答辩时好讲。3.3 预测结果回写数据库模型跑完不能只打印在终端要把预测值写回一张结果表供前端或论文截图用。result X_test.copy() result[true_price] y_test.values result[pred_price] rf.predict(X_test).round(2) result[error] (result[pred_price] - result[true_price]).round(2) result.to_sql(predict_result, engine, if_existsreplace, indexFalse)if_existsreplace每次跑覆盖旧结果避免重复累积。error列保留下来方便排查哪些样本预测偏差大。如果某类区域的误差系统性偏大说明该区域样本太少需要补数据。提示训练集和测试集的划分要在特征工程之前做或者用 Pipeline 包住预处理否则 one-hot 编码会用到测试集的信息造成泄漏。4. 避坑与排查房价预测系统落地时最容易翻车的五个点4.1 爬虫返回403或空列表现象requests 请求返回403或者返回200但解析出来0条。原因没带 User-Agent或者目标站点改了页面结构CSS 选择器失效。解决先加完整请求头再用浏览器开发者工具重新确认选择器路径。如果还是不行检查是不是需要携带 Cookie 或 Referer。4.2 MySQL 中文乱码或连接报错现象写入的区域名变成问号或者报Cant connect to local MySQL server through socket /tmp/mysql.sock。原因建表时字符集不是 utf8mb4或者连接串没指定 charsetsocket 报错通常是 MySQL 服务没启动或端口配置不对。解决建表和连接串都统一 utf8mb4用systemctl status mysql确认服务状态连接串里显式写127.0.0.1:3306而不是localhost。4.3 数据泄漏导致 R²虚高现象模型 R²达到0.98以上但实际预测新数据时误差很大。原因把 unit_price 或者和总价有直接换算关系的字段放进了特征。解决检查特征列表任何能通过简单运算推出目标的字段都要删掉。判断方法如果某个特征和目标的相关性超过0.95先怀疑泄漏。4.4 类别特征用序号编码现象区域编码成0、1、2后模型认为区域2是区域1的两倍。原因序号编码隐含了大小关系而区域之间没有。解决用 one-hot 编码或者用 target encoding但要小心泄漏。scikit-learn 的 OneHotEncoder 配合 ColumnTransformer 是最稳的做法。4.5 训练集测试集划分在预处理之后现象R²比实际偏高换一批数据后性能骤降。原因先对全量数据做了标准化或 one-hot再划分训练测试集测试集的信息泄漏到了训练过程。解决用 Pipeline 把预处理和模型串起来先划分再 fitPipeline 会自动只在训练集上 fit 预处理器。5. 把系统跑成一个可查询的服务Flask 接口与批量预测技巧模型跑通、结果落库之后论文里通常还需要一个「系统」的样子。最轻量的做法是用 Flask 起一个接口接收面积、户型、区域返回预测总价。这样论文里可以截图展示「输入参数→输出价格」的交互过程。from flask import Flask, request, jsonify import joblib import pandas as pd app Flask(__name__) model joblib.load(rf_model.pkl) # 训练好后用 joblib.dump 保存 app.route(/predict, methods[POST]) def predict(): data request.get_json() row pd.DataFrame([{ area: float(data[area]), rooms: int(data[rooms]), halls: int(data[halls]), region: data[region], }]) price model.predict(row)[0] return jsonify({pred_price: round(float(price), 2)}) if __name__ __main__: app.run(host0.0.0.0, port5000)保存模型时用joblib.dump(rf, rf_model.pkl)比 pickle 对 sklearn 对象更高效。接口接收 JSON返回预测总价。注意 Pipeline 里已经包含了预处理器所以传入的原始字段直接进模型就行不用手动 one-hot。批量预测的场景比如把数据库里所有未预测的房源一次性跑完不要逐条调接口直接读表、批量 predict、批量写回效率差几十倍。df_all pd.read_sql(SELECT * FROM house WHERE id NOT IN (SELECT id FROM predict_result), engine) if len(df_all) 0: df_all[pred_price] model.predict(df_all[[area, rooms, halls, region]]).round(2) df_all[[id, pred_price]].to_sql(predict_result, engine, if_existsappend, indexFalse)这段先查出还没预测过的记录批量预测后追加到结果表。NOT IN子查询保证不重复预测。数据量大时NOT IN会慢可以改成 LEFT JOIN 判断 NULL。一个我踩过的坑Flask 默认单线程论文演示时够用但如果要同时处理多个请求得加threadedTrue或者上 gunicorn。另外模型文件路径别用相对路径用os.path.dirname(__file__)拼绝对路径否则换个目录启动就找不到模型。这套链路从爬虫到接口代码量不大但每一层都有容易翻车的地方。我的习惯是每跑通一层就先存一次中间结果CSV 也好、数据库也好别等全链路写完再调试否则出了问题你都不知道是哪一层的锅。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网