Python房价预测系统实战:爬虫、MySQL与机器学习全链路
简介这份资源是一篇面向计算机专业本科毕业设计的完整论文文档主题为基于Python的房价预测系统适合正在准备毕设、需要选题参考或撰写论文的学生与指导教师使用。压缩包内共1个doc文件约2.55MB内容涵盖摘要、绪论、系统设计与实现、测试等完整章节可直接作为论文写作模板与项目方案参考。论文围绕网络爬虫技术展开以58同城网房屋数据为研究对象详细介绍了BeautifulSoup、Scrapy等爬虫模块的数据获取策略以及Pandas、NumPy、Matplotlib在数据清洗与可视化中的应用并采用MySQL完成数据存储与管理。系统运用线性回归、决策树、随机森林、神经网络等机器学习算法建立房价预测模型同时设计了前台查询界面、可视化大屏展示与后台管理功能。目前已有373人学习读者可从中获取完整的毕设论文结构、技术选型思路与系统实现流程对房地产数据分析与预测类课题具有较高的参考价值。1. 从一份房价预测论文.doc说起数据从哪来、模型怎么选、结果怎么信很多人做房价预测第一步就卡在数据上——网上找不到现成的、干净的、带足够特征的二手房数据集于是转头去写爬虫结果又被反爬拦在门外。这份「基于Python的房价预测系统论文.doc」背后真正要落地的东西其实是一条完整链路用 requests 或 xpath 爬虫把房源数据抓下来清洗后灌进 MySQL再用 Python 做特征工程和回归建模最后把预测结果可视化出来。它适合两类人一是要交课程设计或毕业论文的学生需要一套能跑通、能写进文档的完整方案二是想入门数据分析的开发者拿房价这个场景练手爬虫、数据库和机器学习三件套。下面我按自己实际做过的顺序把每个环节的参数、坑和验证方法讲清楚你照着能复现出一套属于自己的系统。2. 数据采集requests 爬虫抓房源列表与详情页的落地写法2.1 先想清楚抓哪些字段再决定爬虫结构房价预测的精度七成取决于你抓了哪些特征。我一般会锁定这几类字段总价、单价、建筑面积、户型几室几厅、楼层、朝向、装修情况、建成年代、小区名称、所在区域。列表页通常只有总价、面积、户型、小区和区域详情页才有朝向、楼层、装修、年代。所以爬虫要分两级先翻列表页拿到详情页 URL 和基础字段再逐个进详情页补齐特征。这里有个容易翻车的地方很多人一上来就写详情页爬虫结果列表页的分页逻辑没吃透翻到第三页就重复抓第一页的数据。分页参数常见的是page1、page2这种也有用offset或start的你得先手动翻两页对比 URL 变化确认翻页规律再写循环。2.2 列表页抓取的最小可运行代码import requests from lxml import etree 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.0.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, } def fetch_list_page(city, page): # 不同城市的列表页 URL 结构不同这里以某二手房平台为例 url fhttps://example.com/{city}/ershoufang/pg{page}/ resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 if resp.status_code ! 200: print(f第{page}页返回状态码 {resp.status_code}跳过) return [] html etree.HTML(resp.text) items html.xpath(//ul[classsellListContent]/li) results [] for item in items: title item.xpath(.//div[classtitle]/a/text()) total_price item.xpath(.//div[classtotalPrice]/span/text()) unit_price item.xpath(.//div[classunitPrice]/span/text()) link item.xpath(.//div[classtitle]/a/href) if not title or not link: continue results.append({ title: title[0].strip(), total_price: total_price[0].strip() if total_price else , unit_price: unit_price[0].strip() if unit_price else , detail_url: link[0], }) return results if __name__ __main__: all_data [] for p in range(1, 6): page_data fetch_list_page(bj, p) all_data.extend(page_data) # 随机休眠降低被封风险 time.sleep(random.uniform(1.5, 3.5)) print(f共抓取 {len(all_data)} 条列表数据)这段代码的逻辑很直白构造分页 URL发请求用 lxml 的 xpath 定位每个房源卡片把标题、总价、单价、详情链接抽出来。参数上重点看三个地方。timeout10是防止某个请求卡死拖垮整个循环resp.encoding utf-8必须显式设置否则中文可能乱码time.sleep(random.uniform(1.5, 3.5))是随机间隔比固定 sleep 更不容易被识别为机器行为。xpath 里的 class 名是示例你实际抓的时候要用浏览器开发者工具重新确认因为不同平台、不同时期的 DOM 结构会变。2.3 详情页补字段与入库前的清洗拿到detail_url后再写一个fetch_detail函数进详情页抓朝向、楼层、装修、建成年代。这里我踩过的坑是详情页的字段经常是「暂无数据」或者空字符串直接入库会导致后面建模时大量缺失。我的做法是在爬取阶段就做一次粗清洗——把「暂无数据」「暂无」统一替换成None面积和价格字段用正则把「平米」「万」这些单位去掉只留数字。import re def clean_price(text): if not text: return None num re.findall(r[\d.], text) return float(num[0]) if num else None def clean_area(text): if not text: return None num re.findall(r[\d.], text) return float(num[0]) if num else Noneclean_price和clean_area都用正则提取第一个数字这样「89.5平米」「1200万」都能转成浮点数。注意re.findall返回的是字符串列表要转float。如果文本里根本没有数字返回None后面入库时 MySQL 对应字段允许 NULL。3. 数据存储MySQL 建表、批量插入与连接踩坑3.1 表结构设计要预留特征列房价预测的特征列比较多建表时不要只建「价格、面积」两列否则后面加特征要频繁改表。我一般会一次性把可能用到的字段都建上用NULL允许空值。CREATE TABLE house_price ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255), city VARCHAR(50), region VARCHAR(100), community VARCHAR(200), total_price DECIMAL(10,2), unit_price DECIMAL(10,2), area DECIMAL(8,2), room_type VARCHAR(50), floor VARCHAR(50), orientation VARCHAR(50), decoration VARCHAR(50), build_year INT, detail_url VARCHAR(500), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;DECIMAL(10,2)比FLOAT更适合存价格避免浮点误差。utf8mb4能存中文和特殊字符。detail_url给到 500 长度因为有些详情页 URL 带一堆参数短了会截断。3.2 Python 批量插入与 mysql-connector 连接配置import mysql.connector from mysql.connector import Error def get_connection(): return mysql.connector.connect( hostlocalhost, port3306, userroot, passwordyour_password, databasehouse_db, charsetutf8mb4, connection_timeout10, ) def batch_insert(records): conn get_connection() cursor conn.cursor() sql INSERT INTO house_price (title, city, region, community, total_price, unit_price, area, room_type, floor, orientation, decoration, build_year, detail_url) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) try: cursor.executemany(sql, records) conn.commit() print(f成功插入 {cursor.rowcount} 条) except Error as e: conn.rollback() print(f插入失败{e}) finally: cursor.close() conn.close()executemany比逐条execute快很多几百条数据一次提交。connection_timeout10防止连接卡住。charsetutf8mb4要和建表时一致否则中文可能报错。如果遇到mysql ssl connection error可以在连接参数里加ssl_disabledTrue本地开发环境一般不需要 SSL。提示批量插入前先把 records 里的None处理好MySQL 的%s占位符能接受None但如果你用的是字符串拼接就会出错。4. 特征工程与建模从原始字段到可用的预测模型4.1 把文本特征转成模型能吃的数字原始数据里room_type是「3室1厅」这种字符串orientation是「南北」「东南」这种文本floor是「中楼层」这种描述。线性回归和树模型都不能直接吃字符串必须编码。我的做法是room_type拆成bedroom_num和living_num两个数值列orientation做 one-hot 或者按朝向评分南北通透给高分floor分成低、中、高三个类别用有序编码。import pandas as pd import numpy as np def parse_room_type(s): if not s or 室 not in s: return pd.Series([None, None]) parts s.replace(厅, ).split(室) bedroom int(parts[0]) if parts[0].isdigit() else None living int(parts[1]) if len(parts) 1 and parts[1].isdigit() else None return pd.Series([bedroom, living]) df pd.read_sql(SELECT * FROM house_price, conn) df[[bedroom_num, living_num]] df[room_type].apply(parse_room_type) df[orientation_score] df[orientation].map({ 南北: 5, 南: 4, 东南: 4, 西南: 3, 东: 3, 西: 2, 北: 2, 东北: 2, 西北: 2 }).fillna(2) df[floor_level] df[floor].apply( lambda x: 1 if x and 低 in x else (2 if x and 中 in x else 3) )parse_room_type用split把「3室1厅」拆成 3 和 1返回两个值。orientation_score用 map 做有序编码比 one-hot 更省维度也保留了朝向的优劣顺序。floor_level把楼层分成 1、2、3 三档。这些处理做完模型输入就是纯数值矩阵了。4.2 训练集划分与模型选择为什么我优先用随机森林房价预测本质是回归问题。线性回归可解释性强但对非线性关系拟合差随机森林和 XGBoost 能自动捕捉特征交互通常效果更好。我一般先用随机森林跑一个 baseline看特征重要性和 R²再决定要不要上 XGBoost。from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, r2_score features [area, bedroom_num, living_num, orientation_score, floor_level, build_year] df_model df[features [total_price]].dropna() X df_model[features] y df_model[total_price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) model RandomForestRegressor( n_estimators200, max_depth12, min_samples_split5, random_state42, n_jobs-1 ) model.fit(X_train, y_train) y_pred model.predict(X_test) print(MAE:, mean_absolute_error(y_test, y_pred)) print(R2:, r2_score(y_test, y_pred))n_estimators200是树的数量太少欠拟合太多训练慢200 是个常用起点。max_depth12限制树深防止过拟合。min_samples_split5表示节点至少 5 个样本才分裂。n_jobs-1用满所有 CPU 核。random_state42保证每次划分一致方便复现。跑完看 MAE 和 R²如果 R² 低于 0.6说明特征不够或者数据质量问题要回去补字段或清洗。4.3 特征重要性分析与模型调参方向训练完打印model.feature_importances_和特征名对应起来看。通常area和region如果做了编码权重最高build_year和floor_level次之。如果某个特征重要性接近 0可以考虑删掉减少噪声。调参时优先动max_depth和min_samples_split用GridSearchCV小范围搜不要一上来就大网格太耗时。5. 避坑与排查爬虫、MySQL、建模三个环节的血泪记录5.1 爬虫返回 403 或空列表现象requests 请求返回 403或者返回 200 但 xpath 解析出来是空列表。原因请求头缺少 User-Agent 或 Cookie或者页面是 JavaScript 动态渲染的requests 拿到的 HTML 里根本没有房源数据。解决先补全 User-Agent、Referer、Accept-Language如果还不行用浏览器开发者工具的 Network 面板看实际数据接口直接请求 JSON 接口再不行就上 Selenium 或 Playwright 渲染后再解析。5.2 MySQL 插入中文乱码现象插入的中文变成???或乱码。原因连接字符集、表字符集、字段字符集三者不一致。解决建表用utf8mb4连接参数加charsetutf8mb4Python 文件本身用 UTF-8 编码保存。三处对齐后基本不会再乱码。5.3 模型 R² 很低或为负数现象R² 小于 0.3 甚至为负。原因特征里混入了大量缺失值或异常值比如面积填了 0 或者总价单位没清洗干净。解决先df.describe()看数值分布把面积小于 10 或大于 1000 的记录过滤掉总价同理。另外检查dropna()之后还剩多少条数据如果只剩几十条模型根本学不到东西。5.4 训练集和测试集分布不一致现象训练集 R² 很高测试集 R² 很低。原因随机划分时某些区域或价格段的数据集中在一侧。解决用train_test_split的stratify参数按价格分箱后再划分或者直接用KFold交叉验证看多折的平均 R² 而不是单次划分的结果。5.5 爬虫被限速或封 IP现象抓了几百条后请求全部超时或返回验证码页面。原因请求频率太高触发了平台的风控。解决加大随机休眠范围比如random.uniform(3, 8)降低并发不要用多线程猛冲如果必须大量抓取分时段、分城市慢慢来。这块没有后悔药只能控制节奏。6. 进阶技巧用可视化界面把预测系统串起来6.1 用 Flask 搭一个最小预测接口论文里通常要求「系统」能演示光有脚本不够。我一般用 Flask 包一层前端提交面积、户型、朝向等参数后端调模型返回预测价格。from flask import Flask, request, jsonify import joblib import numpy as np app Flask(__name__) model joblib.load(house_model.pkl) app.route(/predict, methods[POST]) def predict(): data request.get_json() features np.array([[ data[area], data[bedroom_num], data[living_num], data[orientation_score], data[floor_level], data[build_year], ]]) price model.predict(features)[0] return jsonify({predicted_total_price: round(float(price), 2)}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)模型训练完后用joblib.dump(model, house_model.pkl)保存Flask 启动时加载。debugFalse在生产环境必须关掉否则有安全风险。前端可以用最简单的 HTML 表单也可以用 Vue 或 React核心是 POST JSON 到/predict。6.2 预测结果的可信度怎么验证不要只看 R²。我习惯做两件事一是拿几条真实房源手动输入特征看预测价格和实际挂牌价的偏差是否在可接受范围比如 ±15%二是画预测值 vs 真实值的散点图理想情况是点分布在对角线附近。如果某些价格段偏差特别大说明该段样本太少需要补数据。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False plt.scatter(y_test, y_pred, alpha0.5) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--) plt.xlabel(真实总价) plt.ylabel(预测总价) plt.title(预测值与真实值对比) plt.show()SimHei是黑体解决 matplotlib 中文显示问题。alpha0.5让点半透明重叠区域能看出密度。红色虚线是对角线点越靠近它说明预测越准。6.3 我踩过的一个玄学坑有次模型离线指标很好但 Flask 接口返回的预测值总是偏大。排查了半天发现是前端传过来的area是字符串np.array把它当成了 object 类型模型内部做了隐式转换导致数值异常。后来在接口里加了float(data[area])强制转换才解决。这个坑不报错只是结果不对特别难查。所以接口层一定要做类型校验和转换别信前端传过来的类型。做这类系统我的习惯是先把数据链路跑通——爬虫能稳定抓、MySQL 能正确存、模型能复现训练——再去优化精度和做界面。顺序反了后面全是返工。希望帮到你。本文还有配套的精品资源点击获取