☰
从房天下爬虫到房价预测:Python完整项目实战解析
2026/10/10 20:22:25 网站建设 项目流程

简介:面向楼市数据分析学习者与爬虫/机器学习初学者的Python实训项目源码包,基于房天下二手房真实场景,覆盖从网页爬取、数据清洗、可视化探索到房价预测建模的完整流程。压缩包共93个文件,以42个Python脚本为核心,辅以31张可视化分析图、6个CSV数据文件、5个XML配置及说明文档,整体大小约13.71MB。项目典型脚本包括爬虫随机请求头、数据清洗、数据理解、数据可视化、模型预测等环节,直观展示反爬处理、EDA分析与回归建模思路,各环节图表明晰呈现单价、朝向、装修等因素对总价的影响。已有175人学习下载,适合作为课程设计、小学期实训或个人项目起步参考,可直接运行调试,也可在此基础上扩展算法与特征工程。

1. 这份房天下二手房源码:从爬虫到房价预测模型的完整链路

这份源码是2024年夏季小学期实训项目的完整交付,92个文件、41个Python脚本,从房天下二手房页面爬取挂牌信息,清洗成结构化数据,再经过十几张关系图的可视化探查,最终训练出房价预测模型。如果你正在做Python爬虫或房价预测相关的课设、实训,想找一份从数据采集到建模全流程都能跑通的参考代码,这份资源值得花一个下午拆一遍。我拆完的最大感受是:这里最值钱的部分不是最后的预测模型,而是前面爬虫的随机请求头设计、清洗环节的脏值处理、以及那些探究变量与总价关系的数据探查图——它们决定了模型的效果上限。

2. 爬虫采集:随机请求头与XPath解析的落地细节

2.1 房天下的页面结构:先搞清楚数据在哪几层

房天下二手房列表页的URL结构是https://esf.fang.com/house/a{页码}/这样的分页路径,第一页不带a后缀。列表页每一套房子的信息——标题、小区、户型、建筑面积、朝向、楼层、建筑年代、总价、单价——基本都集中在一个房源节点内部。用浏览器开发者工具检查元素,能看到这些字段分别挂在不同的class或a标签的text里。项目里的爬虫脚本(爬虫.py、爬虫-随机请求头.py、7.6-test1-爬虫.py)都是围绕这个页面结构写的,差别只在于请求头的伪装程度和字段提取的完整度。

明确页面结构之后要做的第一件事不是写解析代码,而是测请求频率。房天下对单IP的访问频率比较敏感,连续快速翻页很容易触发验证页或者直接返回空列表。我一般会先手动访问列表页确认正常,再用脚本单页抓取看返回的HTML里有没有房源节点,确认结构没变后再批量跑。这个项目的urls.txt和urls2.txt就是把翻页地址预先生成好的产物,脚本启动后直接读文件逐条请求,省去了每次拼接页码的麻烦,也方便重跑时跳过已经抓过的页。

2.2 随机请求头:模拟浏览器身份的核心写法

反爬的第一道门槛就是 User-Agent 检测。房天下这类站点对UA特征比较敏感,固定一个UA跑全量翻页,几十页之后很容易被识别。项目里的爬虫-随机请求头.py采用UA池轮换的做法,这是爬房天下这类站点最基础的伪装手段。下面是这个脚本里核心请求函数的示意,思路与原脚本一致:

import requests import random import time USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/125.0", ] def get_headers(): return { "User-Agent": random.choice(USER_AGENTS), "Referer": "https://esf.fang.com/", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_page(url): # 每次请求前随机睡 1~2 秒,降低请求频率特征 time.sleep(random.uniform(1, 2)) resp = requests.get(url, headers=get_headers(), timeout=10) resp.encoding = resp.apparent_encoding return resp.text

这里的random.choice从UA列表里随机挑一个,比固定单个UA的存活率高很多。Referer填房天下首页,是为了让请求看起来像从站内跳转过来的,对部分校验逻辑有效。time.sleep(random.uniform(1, 2))是最容易被新手省略的一行——没有它,即使UA随机了,一秒内发十几个请求照样会被封IP。resp.apparent_encoding用来兜底编码问题,房天下页面实际是GBK编码,直接用 requests 默认的 ISO-8859-1 解析会乱码,所以这里要显式处理。

UA列表里放几款主流浏览器的版本号就够用。注意一点:fake_useragent库虽然省事,但它在某些环境下首次使用要下载数据文件,失败率不低,项目里直接内置UA列表的做法更稳,离线也能跑。我拆过的实训项目里,绝大多数都会在后期从fake_useragent换成这种硬编码列表,原因就是省事且可控。

2.3 XPath提取字段:从HTML节点到结构化数据

拿到页面HTML之后,下一步就是用 lxml 的etree解析,把每个房源节点里的字段抠出来。列表页的房源信息基本都在class为shop_list的列表里,每一套房对应一个信息节点。下面这段XPath提取是项目里常用的写法:

from lxml import etree import csv def parse_house_list(html_text): tree = etree.HTML(html_text) items = [] # 定位每一套房子的节点块 node_list = tree.xpath('//dl[@class="shop_list"]/dd[@class="info"]') for node in node_list: title = node.xpath('./h4/a/text()') total_price = node.xpath('.//span[@class="red"]/b/text()') unit_price = node.xpath('.//div[@class="price"]/span[@class="red"]/text()') base_info = node.xpath('.//p[@class="tel_shop"]/text()') if title and total_price: items.append({ "标题": title[0].strip(), "总价": total_price[0].strip(), "单价": unit_price[0].strip() if unit_price else "", "基础信息": base_info[0].strip() if base_info else "", }) return items # 追加模式写入CSV,方便断点续爬 def append_to_csv(items, path="house.csv"): with open(path, "a", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["标题", "总价", "单价", "基础信息"]) if f.tell() == 0: writer.writeheader() writer.writerows(items)

这段代码有两个细节值得注意。第一,text()返回的是列表,所以后面要接[0]再strip,直接拿列表去拼接字符串会报错。第二,append模式写入是断点续爬的关键——一旦中途被反爬打断,下次从最后记录的页继续跑就行,不需要重新抓全量。

基础信息里包含的是"2室1厅 / 90㎡ / 南北 / 简装"这种用斜杠分隔的拼接字符串,后续在数据清洗阶段再拆成独立列。这一步故意不做拆分,是因为列表页的格式偶有变化,先原样存下来,清洗时统一处理更抗造。项目里一堆以日期和test命名的脚本(7月3日.py、7.4-test1.py等)是实训过程中逐步迭代的版本,真正的主线看main.py、house_remix.py和最终的几个预测脚本就够。

3. 数据清洗与理解:把文本字段变成可训练的数字特征

3.1 字符串清洗:去掉单位、符号和非数字内容

爬虫落盘的数据长什么样,直接看CSV就能发现:总价列是"345万",单价列是"34567元/㎡",建筑面积列里混着"90㎡"和"90.5 ㎡"两种写法,朝向列可能有"南北""南""东西"等取值。这种情况不能直接进模型,pd.to_numeric遇到这些字符串会整列转成NaN。项目里的数据清洗.py做的事情就是把这些带单位的文本统一转成float。

import pandas as pd df = pd.read_csv("house.csv", encoding="utf-8-sig") # 总价:去掉"万"字,转成数值 df["总价"] = df["总价"].astype(str).str.replace("万", "", regex=False) df["总价"] = pd.to_numeric(df["总价"], errors="coerce") # 建筑面积:去掉"㎡",注意单位和数字之间可能有空格 df["建筑面积"] = df["建筑面积"].astype(str).str.replace("㎡", "", regex=False).str.strip() df["建筑面积"] = pd.to_numeric(df["建筑面积"], errors="coerce") # 单价:去掉"元/㎡" df["单价"] = df["单价"].astype(str).str.replace("元/㎡", "", regex=False) df["单价"] = pd.to_numeric(df["单价"], errors="coerce") # 去掉总价或面积为空的记录 df = df.dropna(subset=["总价", "建筑面积"]) # 同一个房源重复抓到的记录,按小区+户型+面积去重 df = df.drop_duplicates(subset=["小区", "户型", "建筑面积"]) df.to_csv("house_clean.csv", index=False, encoding="utf-8-sig")

errors="coerce"这个参数是重点,它把无法转换的值变成NaN而不是直接抛异常。爬虫数据里几乎必然混着"暂无""电话详询"之类的非数字值,不加这个参数,脚本会挂在类型转换这一步。dropna和drop_duplicates的顺序也有讲究:先去重再丢空值,能保留更多有效样本,因为完全相同的重复记录里缺失值可能互相补齐。反过来先丢空再查重也能跑,但保留的记录量通常会少一些。

小区、户型、朝向这些文本字段不用在这一步处理,它们留给特征工程阶段做编码。数据清洗的核心目标是让每个数值列都是干净可计算的float,这一步做扎实了,后续画箱线图、训练模型时才不会被一串类型报错刷屏。

3.2 数据理解:用describe和value_counts做第一轮摸底

数据清洗完不能直接建模,要先对数据集的体量和分布心里有数。数据理解.py这个脚本的作用是输出每一列的缺失值、数据类型和取值分布,相当于建模前的体检报告。核心操作可以概括为下面这段:

import pandas as pd df = pd.read_csv("house_clean.csv", encoding="utf-8-sig") # 每列的缺失值数量和数据类型 print(df.info()) # 总价、面积、单价的数值分布 print(df[["总价", "建筑面积", "单价"]].describe()) # 几个类别字段的取值统计 for col in ["朝向", "楼层", "装修程度", "电梯"]: print(f"\n{col} 取值分布:") print(df[col].value_counts()) # 总价有没有离群值,用分位数看 print(df["总价"].quantile([0.01, 0.25, 0.5, 0.75, 0.99]))

describe输出的25%、50%、75%分位数能快速暴露异常。比如总价75%分位是420万,但max是3500万,说明高端房源拉了整体分布,这时候要么保留观察,要么在模型里做分箱,直接删掉是有风险的——它们可能是真实的豪宅挂牌,只是样本量少。value_counts针对朝向这类字段,能看到"南北"占比过半、而"东西"只有个位数的情况,这类低频类别在后续编码时要么合并成"其他",要么直接丢弃。

这个阶段的产出是一份对数据的整体认知:有多少条有效记录、哪个字段缺失严重、总价分布是否偏斜。这些信息直接决定了可视化阶段要画哪些图、模型阶段要选哪些特征。项目里7.6-test2-数据理解.py和7.6-test3-数据清洗.py这两份过程脚本,正好对应了"理解 -> 清洗"的迭代顺序,实操中这两步经常来回跑,不是一条线走完的。

3.3 数据格式转换:类别文本到数值编码的过渡

房价预测模型里,朝向、装修程度、楼层这些文本字段不能直接参与计算。最常见的做法是先用LabelEncoder把类别变成整数编号,再根据后续特征重要性决定要不要做独热编码。项目里数据格式转换.py做的是前者,也是最省事的方案:

from sklearn.preprocessing import LabelEncoder # 只保留模型会用到的主要字段 cols = ["总价", "建筑面积", "朝向", "楼层", "装修程度", "建筑年代", "电梯", "户型"] model_df = df[cols].copy() # 类别字段统一编码 encoder_map = {} for col in ["朝向", "楼层", "装修程度", "电梯"]: le = LabelEncoder() model_df[col] = le.fit_transform(model_df[col].astype(str)) encoder_map[col] = list(le.classes_) # 建筑年代可能带"年"字,先清洗 model_df["建筑年代"] = model_df["建筑年代"].astype(str).str.replace("年", "", regex=False) model_df["建筑年代"] = pd.to_numeric(model_df["建筑年代"], errors="coerce") model_df = model_df.dropna() # 户型字段拆出室和厅两个数值 model_df["室"] = model_df["户型"].astype(str).str.extract(r"(\d+)室").astype(float) model_df["厅"] = model_df["户型"].astype(str).str.extract(r"(\d+)厅").astype(float) model_df = model_df.dropna(subset=["室", "厅"]) model_df.to_csv("model_data.csv", index=False, encoding="utf-8-sig")

LabelEncoder把"南北"编成0、"南"编成1,这种做法隐含了无序类别之间的距离,严格来说朝向这种无序变量更适合独热编码。但实训项目的数据量通常不大,用编号做基准模型完全够用,而且后面feature_importances_能直接看到每个编码类别对总价的影响,方便决定要不要换编码方式。

建筑年代的处理要注意:这个字段在爬取时有的带"年"字、有的是纯数字、还有个别是"暂无"被to_numeric转成NaN了,所以这一列必须走一遍清洗再dropna。户型拆室厅用正则提取,能把"2室1厅1卫"和"3室2厅"两种口径都拆成统一的室数、厅数,比直接对字符串计数可靠。

4. 可视化探查:十二张关系图在建模前做了什么

4.1 为什么要先画图:特征筛选靠图比靠猜靠谱

项目里可视化分析产出的一大堆PNG图片——探究朝向和总价的关系.png、探究楼层和总价的关系.png、探究建筑年代和总价的关系.png、探究电梯和总价的关系.png——看名字就知道是在做单变量与总价的关联分析。这一步不是凑图凑数量,而是建模前最重要的特征筛选依据。哪些字段值得进入模型,不是拍脑袋决定的,而是看图上变量和总价之间有没有可分的变化趋势。如果一个类别字段的不同取值下,总价分布几乎重叠,那它进了模型也只是噪声。

对每个类别字段,项目里的做法是画箱线图或柱状图,展示不同取值下的总价中位数和四分位距。比如朝向和总价的关系图,通常能看到南北朝向的中位数明显高于其他朝向;楼层和总价的关系图,中楼层往往比高楼层和低楼层贵一点;电梯和总价的关系图,有电梯的房源总价整体上移。这类肉眼可见的差异,就是模型可以学习的信号。数据可视化.py脚本把这些图批量生成并存到工程目录,后续迭代时直接回来看图找线索,比反复跑describe直观得多。

4.2 关系图逐个读:案例字段与总价的关联解读

房天下二手房的特征字段大致有行政区域、建筑年代、建筑结构、装修程度、楼层、建筑形式、朝向、建筑面积、户型、电梯、产权性质、住宅类别这些。项目里挑出来做探究的也主要是它们。下面这张表梳理了其中几张最有信息量的图:

图名变量主要观察点
探究朝向和总价的关系.png朝向南北通透是否带来明显溢价
探究楼层和总价的关系.png楼层中楼层是否比其他楼层更贵
探究装修程度和总价的关系.png装修程度精装与简装的价差有多大
探究电梯和总价的关系.png电梯有电梯是否为显著特征
探究建筑年代和总价的关系.png建筑年代楼龄与总价是否存在线性趋势
探究建筑面积和总价的关系.png建筑面积面积和总价的线性程度

这六张图看完,基本能定下模型的特征候选集。建筑面积和总价的图如果呈现出明显线性趋势,那它会成为模型里权重最高的特征;朝向、电梯如果组间差异明显,保留编码特征;建筑年代如果分布太分散没有趋势,可以考虑丢弃或分箱。

另外像探究单价、数量、总价和行政区域之间的关系.png这张图,把行政区域拉进来做多维对比,能看出不同区块的价格梯度。实训项目里这种多维图不用画太多,一两张辅助判断区域因素即可,重点是单变量与总价的关系要逐一过一遍。

4.3 绘图代码与参数细节:怎么把关系图存成文件

项目里的关系图全部落盘为PNG,核心绘图逻辑并不复杂,重点是几个参数要设置对。下面这段代码是绘制"朝向与总价关系"箱线图的示意:

import matplotlib.pyplot as plt import pandas as pd df = pd.read_csv("house_clean.csv", encoding="utf-8-sig") fig, ax = plt.subplots(figsize=(10, 6)) df.boxplot(column="总价", by="朝向", ax=ax) ax.set_title("朝向与总价关系箱线图") ax.set_xlabel("朝向") ax.set_ylabel("总价(万)") plt.xticks(rotation=45) plt.tight_layout() plt.savefig("探究朝向和总价的关系.png", dpi=150)

figsize=(10, 6)控制了画布比例,朝向取值多的时候窄画布会导致刻度过密看不清。plt.xticks(rotation=45)是给朝向这种多文本取值准备的,不旋转的话标签会重叠。dpi=150保证图片放大后依然清晰,实训报告里直接插入不会糊。

一个容易踩的细节是df.boxplot(column="总价", by="朝向")这类调用会把group信息自动加进标题,画完记得用ax.set_title覆盖,否则最终图片标题是系统生成的,不够规范。另外保存图片时用英文文件名更稳妥,项目里个别中文文件名在旧版系统上打开会出问题,自己保存时建议统一用拼音或英文命名。

4.4 箱线图的另一个身份:异常值过滤器

箱线图.png和箱线图分析-总价-建筑面积.png这两张图在项目里的作用容易被忽略。箱线图除了展示分布,更重要的是暴露离群值:上须之外的散点就是候选异常值。这背后有一个取舍——总价3000万的豪宅,是删掉还是保留?按我的习惯,先看它是不是真实挂牌。房天下的挂牌数据里确实存在江景豪宅、独栋别墅这类极端值,它们不是脏数据,删掉反而丢失了真实市场信息。更合理的做法是先用四分位距(IQR)识别它们,再单独观察它们对模型预测误差的影响。

如果是用于教学演示的实训项目,直接按IQR过滤掉最极端的1%也说得过去,因为目标是展示完整流程而不是落地商用模型。箱线图在这里的角色相当于一个可视化版的异常检测器,它告诉你去哪里看异常值,而不是替你做删不删的决定。项目里这两张箱线图文件放在根目录,正好可以作为这个判断过程的证据留存。

5. 爬虫与数据清洗常见问题:四条翻车记录和对应解法

5.1 高频封禁与请求中断:两个爬虫阶段的典型事故

第一条翻车记录:UA随机了还是被房天下拦截。现象是脚本跑了几十页后,requests 返回的HTML里房源节点数量突然变成零,甚至直接返回一个验证页。原因是频率特征没有隐藏住——虽然 User-Agent 在轮换,但请求间隔固定、每页请求都紧挨着发出,IP的访问特征早就被识别了。解决方式是把固定的time.sleep(1)改成random.uniform(0.8, 2.5)的随机区间,让相邻请求间隔无规律可循;同时加上每20页强制停10秒的节流策略。这个改动很小,但存活率提升非常明显。我从项目源码里看到爬虫-随机请求头.py自带随机休眠逻辑,就是为这个场景准备的。

第二条:爬虫跑到一半断掉,重跑又要从头来过。现象是CSV里只有前几百条数据,脚本抛异常退出后再启动又从第一页开始。原因是请求循环没有记录当前翻到第几页,数据写入用的也是覆盖模式。解决方式是改用追加模式写CSV,并把页码记录在一个文本文件里,重启后先读页码再继续。append_to_csv的写法就是为这个场景准备的——启动时扫一遍urls.txt,跳过已经抓过的页,总时间成本能省一半以上。实训项目里这种方法比数据库存储简单得多,也够用。

5.2 脏数据与编码问题:清洗阶段的两个高频翻车点

第三条:"暂无"把整列变成NaN。现象是to_numeric转换后某一列全空,dropna后数据量骤降,模型直接没有足够样本训练。原因是页面里部分房源的总价字段显示为"暂无"或"电话详询",爬虫原样存进了CSV,直接转数值会全部报错。解决方式分两步:先用value_counts看这一列到底有哪些非数字取值,再把它们统一替换成NaN或按业务理解填充。注意不要直接fillna(0),总价填0会严重污染训练集,宁可丢掉这些样本。项目里的house-test1.csv、house-test2.csv这类文件就是用来测试这一步的,跑一遍value_counts能立刻看到脏值全貌。

第四条:CSV用Excel打开乱码。现象是文件用pandas读没问题,但双击用Excel打开中文全是乱码。原因是写入时用了utf-8而不是utf-8-sig,Excel默认按ANSI解析。解决方式是写入时统一用encoding="utf-8-sig",这是带BOM的UTF-8,Excel能正确识别。这个坑虽然小,但实训项目交付时经常被扣分,建议从一开始就养成习惯。项目里所有CSV写入都带utf-8-sig,算是给了一个规范示范。

6. 房价预测模型:不到三十行代码跑出基准结果

模型预测.py和预测价格.py承担了最后一步:用清洗好的model_data.csv训练回归模型,预测总价。二手房挂牌价预测,基准模型用随机森林回归是最稳的选择——它对类别编码和量纲差异不敏感,不需要做标准化,训练速度快,效果通常不会太差。下面是核心流程的代码示意:

import pandas as pd from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, r2_score df = pd.read_csv("model_data.csv", encoding="utf-8-sig") X = df.drop(columns=["总价"]) y = df["总价"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) model = RandomForestRegressor( n_estimators=300, max_depth=15, min_samples_leaf=3, random_state=42, ) model.fit(X_train, y_train) pred = model.predict(X_test) print("MAE:", mean_absolute_error(y_test, pred)) print("R2:", r2_score(y_test, pred)) print("特征重要性:", dict(zip(X.columns, model.feature_importances_)))

n_estimators=300保证树的数量足够,max_depth=15防止过拟合,min_samples_leaf=3让叶子节点不至于太稀疏。这三个参数是随机森林最常用的调节旋钮,先跑一轮默认参数拿到基准,再按训练集和测试集的误差差来调深度。项目里的scoring.py就是用来打印这些评估指标的,跑完一眼能看出模型是欠拟合还是过拟合。

验证结果的技巧是同时看MAE和R2,别只盯一个。MAE给出误差的实际货币量级,比如28万意味着平均预测偏差约28万;R2说明模型解释了多少方差,0.8以上就算实训项目里相当好的结果。拿到特征重要性输出后,把重要性最低的字段删掉再跑一轮,如果R2没明显下降,就说明那个特征确实是噪声,这一步叫特征精简。

从那以后,我每拿到一份爬虫类的实训源码,都会先清洗、再画图、后建模,按这个顺序走一遍再下结论——不是因为算法不够好,而是多数预测误差其实来自数据环节的粗糙处理。这份项目源码值得下载的原因也在于此,它的价值在爬虫和清洗的可执行细节里,而不只是最后那个预测结果。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询