Python爬虫实战:房价数据采集与分析
2026/9/8 4:32:12 网站建设 项目流程

简介:面向Python爬虫初学者的完整实践项目,围绕北京房价信息采集与可视化展开,覆盖requests发送请求、BeautifulSoup解析页面、正则提取数据、pandas清洗分析,以及matplotlib绘图等核心环节。资源包共8个文件,以3个Python脚本(爬虫、解析、分析)为主,附带2个已清洗的房价Excel数据表、2个可视化HTML图表和1个城市URL配置,整体仅483KB,轻量易上手。目前已有4220人学习下载。通过运行脚本,可直接复现从网页请求到最终图表输出的完整流程,同时代码中融入了异常处理及代理池、反爬策略的应对思路,适合希望以真实项目快速打通爬虫全链路的数据爱好者。

1. 项目思路与整体设计

1.1 爬取房价信息到底解决什么问题

我一直觉得,买房子或者租房这件事,最大的信息差不在房源本身,而在价格分布。同一个城市、同一个板块,挂牌价能差出百分之二三十,这中间的水分到底在哪,没有一个量化的认知,只能被中介带着节奏走。

爬取房价信息,本质上是把零散的挂牌数据汇总成一个结构化的数据集,然后从里面看出门道。比如某片区近期的均价、不同户型的价差、楼层朝向对价格的影响、挂牌价随时间的波动趋势。这些数据如果是手动去采集,一套房子一套房子地翻,工作量大到不现实,但用脚本自动跑,半小时就能把整个城市的挂牌信息拉下来。

这个项目最典型的应用场景有三个:第一,自住刚需的人,了解目标片区的真实价格区间,不被单套房源带偏;第二,做市场研究的人,用数据辅助判断板块热度;第三,想练手爬虫和数据清洗的开发者,这个项目规模适中,既有页面解析、又有反爬策略、还有数据处理和可视化,是一个非常完整的综合性练习。

1.2 技术选型背后的思考

说实话,爬房价信息的技术路线不算复杂,核心就是三步:请求页面、解析HTML、结构化存储。但每一步都有值得琢磨的地方。

请求库方面,我选择requests而不是scrapy。原因很简单,这个项目的数据量在几千到几万条的级别,并发要求不高,requests配合简单的重试机制就足够了,scrapy的框架复杂度在这个场景下反而显得笨重。解析层面,BeautifulSoup配合lxml解析器是我最常用的组合,它的CSS选择器语法足够直观,用chrome开发者工具定位元素结构也很顺手。存储层面,直接上pandas,把数据存成CSV和DataFrame,后期做统计分析几乎是无缝衔接。

整个项目我是按模块来组织的,爬虫脚本、清洗脚本、分析脚本分开写,这样后期改任何一个环节,都不会牵连到其他部分。而且这个结构可以套用到任意同类项目上,换个数据源,改改选择器和字段映射,就能复用到其他垂直信息站的采集。

2. 环境准备与工具选型

2.1 Python环境安装与库依赖

这个话题虽然基础,但确实值得单独说一说。二线城市的房价数据站点,页面结构老旧技术栈的占了相当比例,Python 3.8以上就完全兼容了,不建议用太新的版本,部分解析库对Python 3.12以上版本的支持还不算稳定。

环境安装上有两条路。一条是直接从python官网下载安装包,装的时候记得勾选Add Python to PATH,然后打开命令行输入python --version验证是否安装成功。另一条是我比较推荐的方式,安装Anaconda。

Anaconda本身自带了一个Python环境,还预装了两百多个常用库,最方便的是它的base环境里直接含pandas、numpy、matplotlib这些我们马上要用到的东西,省了逐个装的功夫。Windows下安装Anaconda基本就是一路Next,但需要注意安装到最后一个界面时不要勾选Register Anaconda as my default Python,否则后期在多环境管理时会产生路径冲突。

装完环境之后,还需要手动装几个库。先把命令行切到工作目录,然后执行:

pip install requests beautifulsoup4 lxml pandas matplotlib

如果因为网络原因下载速度很慢,可以临时换成国内镜像源:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests beautifulsoup4 lxml pandas matplotlib

2.2 代码编辑器的选择

编辑器这块,VSCode是我的主力。它免费、插件生态完善,对Python的支持在装完Python扩展之后体验相当流畅。配置方面,只需要在设置里指定Python解释器路径,如果是Anaconda环境,直接关联到安装目录下的python.exe即可。

有一个实用的配置建议,在VSCode的settings.json里加上:

{ "python.linting.enabled": true, "python.linting.pylintEnabled": true, "editor.mouseWheelZoom": true, "files.encoding": "utf8" }

第一项开启代码检查,写代码过程中就能实时发现语法问题,这对爬虫脚本调试特别有帮助。编码设置为utf8也很关键,这个项目要处理大量中文数据,如果文件编码不对,读出来全是乱码,后面所有的分析都无从谈起。

3. 爬虫核心实现与细节

3.1 目标站点分析与请求策略

房价信息的公开数据源不少,普通房产中介平台的挂牌公开信息是最常见的。这类站点的页面结构相对规整,新房和二手房分开列表页,每个列表页展示一定数量的房源卡片,下面有分页。

以链家这类典型平台为例,它的URL规律很清晰:

base_url = "https://{city}.lianjia.com/ershoufang/pg{page}/"

其中city是城市拼音,page是页码。这意味着只要循环改变page参数,就能遍历全城的二手房挂牌列表。

但实际请求过程中,服务器会对高频请求做限流。一个完全没有伪装的requests请求头,服务器端很容易识别为脚本并拒绝访问。我的做法是设置一个完整真实的浏览器请求头:

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": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Referer": "https://{city}.lianjia.com/ershoufang/" }

注意User-Agent的版本号要跟真实浏览器版本保持一致,不要用一个过时的UA。虽然部分平台有cookie校验,首轮请求可以先用requests.Session()自动管理cookie,有些情况下不添加cookie也能访问到正常数据。

3.2 页面解析与数据字段提取

拿到页面HTML之后,解析的重点是找到房源卡片的位置。用Chrome打开一个列表页,按F12进入开发者工具,选择元素面板,然后用左上角的箭头工具点中一张房源卡片,就能看到DOM结构。

这类平台的列表页通常长这样:外层是<ul class="sellListContent">,每个房源卡片是<li class="clear LOGCLICKDATA">。卡片内部包含标题、小区名称、位置、面积、朝向、装修、挂牌价和单价。

用BeautifulSoup提取的代码如下:

from bs4 import BeautifulSoup def parse_list_page(html): soup = BeautifulSoup(html, "lxml") house_list = [] items = soup.select("ul.sellListContent li") for item in items: title = item.select_one(".title a") if title is None: continue house_name = title.get_text(strip=True) house_url = title.get("href", "") # 位置信息 position_info = item.select_one(".positionInfo") if position_info: pos_text = position_info.get_text(strip=True) else: pos_text = "" # 总价和单价 total_price = item.select_one(".totalPrice span") unit_price = item.select_one(".unitPrice") house_list.append({ "title": house_name, "url": house_url, "position": pos_text, "total_price": total_price.get_text(strip=True) if total_price else None, "unit_price": unit_price.get_text(strip=True) if unit_price else None, }) return house_list

这里有个细节值得说道:一定不要用find_all一层一层去遍历,直接用CSS选择器效率更高,而且代码更简洁。定位元素时优先看class属性,如果目标控件的class名称里含有动态生成的随机字符串,就退回用父级容器的class配合层级选择器。

3.3 请求频率控制和异常处理

初学爬虫的人最容易犯的错误就是循环不加延迟,脚本像机关枪一样连发请求,结果就是触发反爬被封IP。我的经验是每次请求之间随机sleep 1到3秒,毕竟是一个长期运行的任务,稳定性优先于速度。

import time import random def fetch_page(session, url, retries=3): for attempt in range(retries): try: resp = session.get(url, headers=headers, timeout=10) resp.raise_for_status() # 随机延迟,避免触发反爬 time.sleep(random.uniform(1, 3)) return resp.text except Exception as e: print(f"第{attempt+1}次请求失败: {e}") time.sleep(5) return None

这个重试机制我加了一个3次的上限,不是越多越好。如果连续3次都被拒,说明大概率是IP被限制了,再多的重试只会浪费时间。这时候最合理的做法是停一段时间再继续,或者换个思路缩小采集范围。

4. 数据处理与房价分析

4.1 数据清洗的知其所以然

爬下来的原始数据是不能直接用的。比如总价字段,页面上显示的是"850万",但pandas要算平均值,必须先把它转成纯数字。

我的清洗思路是先统一字段格式,再处理缺失值,最后做类型转换。

import pandas as pd df = pd.read_csv("house_data.csv") # 去掉"万"字并转为浮点数 df["total_price"] = df["total_price"].str.replace("万", "").astype(float) # 单价可能是"61875元/平"这种格式 df["unit_price"] = df["unit_price"].str.replace("元/平", "").str.replace(",", "").astype(float)

截止到这步,数据就开始变得可分析了。但清洗过程中还会遇到很多特殊值,比如面积字段可能出现"暂无数据",朝向可能是"南北"、"东南"、"北"等等。对于有明确业务含义的字段,我的建议是:

  • 总价、单价这类数值型字段,出现异常值(比如总价为0)直接剔除
  • 朝向这种分类字段,如果缺失比例小于5%,可以填充为"未知",如果缺失比例高,就得考虑是不是页面解析失败了
  • 地址和小区名称出现重复或拼写不一致,用字符串的lstrip/rstrip处理空白字符,再用简单的模糊匹配合并

4.2 核心指标的计算逻辑

清洗完之后,就可以计算一些有意义的统计口径了。最基础的是片区均价:

# 按片区分组,计算均值和样本量 district_stats = df.groupby("district")["unit_price"].agg(["mean", "count"]) district_stats.columns = ["平均单价", "挂牌数量"] district_stats = district_stats.sort_values("平均单价", ascending=False)

这里有两个容易踩的坑。第一,mean值非常容易被个别天价楼盘带偏,比如某个片区有一套总价过亿的豪宅,会把该片区的均价拉高一截。为了更稳健,我会同时计算中位数。

district_stats["中位单价"] = df.groupby("district")["unit_price"].median()

中位数对极端值不敏感,能更真实地反映片区的主流价格水平。第二,挂牌数量少于5套的片区,统计上没有代表性,直接过滤掉。

这个项目的核心价值在于揭示数据背后的规律,平均值和中位数互为印证,才能得出真正有用的结论。

4.3 可视化呈现:把数据变成判断依据

数据算出来是一张表格,但人对表格不敏感,对图表敏感。用matplotlib画几个图,整个市场的状况就一目了然了。

首先是片区均价的横向柱状图:

import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams["font.sans-serif"] = ["SimHei"] # 解决中文乱码 matplotlib.rcParams["axes.unicode_minus"] = False plt.figure(figsize=(12, 8)) district_stats.head(15).plot(kind="barh", y="平均单价", legend=False) plt.title("各片区二手房平均挂牌单价") plt.xlabel("单价(元/平米)") plt.gca().invert_yaxis() plt.tight_layout() plt.savefig("district_price.png", dpi=150)

中文字体显示是个经典坑,matplotlib默认字体不包含中文,不设置的话图上的中文全是方块。在Windows上设SimHei黑体就可以解决,Linux上可以换成文泉驿或者Noto Sans CJK。

其次是价格分布的直方图,用来看数据覆盖的区间是否合理:

plt.hist(df["unit_price"], bins=50, edgecolor="white") plt.title("房源单价分布") plt.xlabel("单价(元/平米)") plt.ylabel("房源数量") plt.show()

这一步其实是在做数据质量的初步验证。如果直方图显示价格分布严重右偏或者出现了一个特别离群的长尾,说明采集数据可能存在异常,回头检查解析逻辑或者清洗步骤,而不是直接把图丢进报告里。

5. 全流程整合与常规注意事项

5.1 一个完整的脚本骨架

把上面所有部分串起来,完整项目的核心流程可以浓缩成一个主脚本,包含请求、解析、存储和统计四个阶段。

import requests import pandas as pd import time import random from bs4 import BeautifulSoup headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36" } BASE_URL = "https://{city}.lianjia.com/ershoufang/pg{page}/" def parse_list_page(html): # 上文已实现的解析函数 pass def main(city="bj", max_pages=100): session = requests.Session() all_data = [] for page in range(1, max_pages + 1): url = BASE_URL.format(city=city, page=page) resp = session.get(url, headers=headers, timeout=10) if resp.status_code != 200: break page_data = parse_list_page(resp.text) if not page_data: break all_data.extend(page_data) time.sleep(random.uniform(1, 3)) print(f"已完成第{page}页,累计{len(all_data)}条") df = pd.DataFrame(all_data) df.to_csv("house_data.csv", index=False, encoding="utf-8-sig") # 后续统计分析直接读取csv if __name__ == "__main__": main(city="sh", max_pages=50)

分页循环里做了一个提前终止的判断,如果某一页解析出来是空列表,说明已经翻到了列表末尾,继续循环没有意义,直接跳出能省下不少时间。这个细节在真实的爬虫场景里很常用,列表页返回空内容代表数据到头了。

5.2 频率、慢速与稳健性

爬虫脚本跑起来之后的稳定性,往往比速度更重要。我见过有人追求快,把每个请求之间的间隔降到0.1秒,结果跑不到20页IP就被封了,反而一无所获。我的经验是,在一线城市的数据量级下,即便每秒1次请求,两三个小时也能把整个城市的数据抓完。

如果项目需要长期运行,建议加上一个异常处理级别的断点续爬机制。最简单的做法是每爬完20页就把当前数据存一次盘,这样即便中途程序崩溃,损失也控制在20页的范围内。我通常用df.iloc把数据分批追加到CSV里,而不是在内存里攒到最后一并写盘。

另外,爬虫代码中处处都要有容错。网络请求阶段的超时、解析阶段的字段缺失,都需要单独的兜底分支。宁可在代码里多写几个if判断,也好过程序跑了一夜,第二天早上醒来发现数据少了大半。

6. 常见问题与排查技巧实录

6.1 遇到反爬策略怎么办

最常见的报错是请求返回401、403或者弹出一个验证码页面。处理思路分级推进:

第一级,先从请求头下手。确认User-Agent是完整的、最新的浏览器UA,加上Accept-Language。顺带检查Referer字段,有些站点会校验请求来源,如果Referer直接是别的域名,服务器就会拒绝。

第二级,降低请求频率。把延迟从1秒提高到3~5秒,随机化的范围拉大,让请求间隔更像人的浏览节奏。

第三级,处理验证码。如果下拉列表页面开始出现滑块验证码,说明当前IP已经被标记了。这种情况下换代理IP是最直接的办法,不过要注意代理的质量和稳定性,免费的代理池往往比被限速更坑。

6.2 解析结果为空

爬虫最常见的问题就是选择器写错了。页面改版或者内容采用动态加载都会导致选择器失效。判断方法是先打印一段HTML看看实际结构:

# 调试用:把页面保存到本地,再用浏览器打开检查 with open("debug_page.html", "w", encoding="utf-8") as f: f.write(html)

然后把这一段HTML用浏览器打开,重新定位元素的class路径。如果确认页面结构没问题,但解析结果还是空,那就要考虑数据是不是异步加载的。部分站点首页是框架页,房源数据是通过XHR接口异步返回的,这时候直接请求页面HTML是拿不到核心数据的。解决办法是打开浏览器的Network面板,找到真正的数据接口URL,把爬虫的目标从HTML页改成JSON接口。

6.3 乱码和数据错位

乱码大概率是编码问题。requests处理文本时,如果页面没有明确声明charset,默认按ISO-8859-1解码,中文就会变成一堆乱码。解决方案是手动指定:

resp.encoding = "utf-8"

数据错位往往是解析顺序错了,比如字段A的解析结果赋值给了字段B,或者在遍历时列表索引对不上。这类问题最好的排查方式是抽样打印:输出前3条记录的dict,用肉眼看字段值和中文是否对应得上。

我自己的习惯是在爬虫正式跑全量之前,先爬一个页面做demo,并打印JSON格式的结果。等到所有字段都确认正确了,再放开循环跑全量。这个习惯帮我省下了非常多的返工时间。

7. 从爬虫到选房决策的实践心得

实际做完这个项目,我最大的感触是:爬虫只是手段,数据分析才是目的。当整个城市的几千条房源数据铺开在面前时,你能看到很多平时靠感觉根本摸不透的规律。同一个小区,低楼层和高楼层的价差有多大;地铁沿线和远离地铁的房源单价能差出多少;同板块小户型和大户型每平米单价的走向差异。这些数据如果不主动去采集,靠线下跑盘,跑一个月也积累不到足够的样本量。

最后分享两个小建议。第一个,用总价筛选比用单价筛选更贴近真实选房的场景,因为总价预算决定了你实际能买到什么样的房子。第二个,跑一次完整流程之后,把结果输出成excel或csv,再用筛选功能手动看一眼数据里有没有异常值,工具只能帮你处理规则明确的数据,真正的判断力还是要落到人身上。

如果后续想往深入做这个项目,可以考虑引入时间维度,定期采集并对比挂牌价的涨跌走势,甚至结合成交数据做更贴近市场真实成交价的分析。但在那之前,先把现有这套流程跑通、跑稳,就已经能解决很大一部分信息不对称的问题了。

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

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

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

立即咨询