Python爬虫实战:北京租房数据采集分析与可视化全流程解析
2026/9/15 4:26:28 网站建设 项目流程

简介:基于 Python 网络爬虫的租房数据采集分析与可视化项目源码,是一个面向高校学生课程设计与期末大作业的完整可运行项目,已获导师指导并通过 97 分高分。项目聚焦北京租房市场数据,完整覆盖爬虫采集、数据清洗、存储、分析与可视化链路,适合希望快速掌握网页解析、数据表格处理与图表展示的 Python 学习者参考。压缩包共 4 个文件,包含 Python 爬虫主程序、Jupyter Notebook 分析文档、Excel 数据文件和 gitignore 配置,整体约 199KB,结构清晰,便于按步骤查看爬虫逻辑与分析结论。目前已有 1231 人学习或下载;使用时可依据自身城市或平台调整请求参数,也可参考其评分较高的项目组织方式,完成从数据源选择到可视化呈现的综合实训任务。对于希望系统练习网络爬虫与数据分析的开发者,这也是一个不错的实战参照。

1. 一次能跑通的爬虫很简单,难的是一周后还能跑通

课程设计里最常见的场景是:答辩前夜爬了一千条数据,截图存好,代码一交就算完事。但真正把这份基于 Python 网络爬虫的租房数据采集分析与可视化项目拆开看,会发现它的价值不在“爬到数据”这个结果,而在一条完整的链路——Requests 发请求、XPath 解析 DOM、pandas 清洗规约、Matplotlib 与 Pyecharts 出图。所以这不是一个只写 50 行就收工的 demo,而是一个能直接当数据采集实战范本的工程骨架。适合正在做期末大作业的本科生、想学爬虫但一直停留在“跟着教程照抄”阶段的自学者,以及想快速搭一套城市租房价格观察工具的从业者。文章会顺着采集层、清洗层、分析层、可视化层一路拆,最后落到反爬验证和把爬虫移植到其他城市的改造点。

2. Requests + XPath:北京租房爬虫的采集层设计与反爬应对

2.1 目标站点结构与翻页规律

租房信息页的 HTML 结构与普通静态页面不太一样。列表页的 DOM 里,每一条房源通常挂在同一个 class 或>import requests import time from lxml import etree 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" } base_url = "https://bj.lianjia.com/zufang/pg{}/" max_page = 10 session = requests.Session() session.headers.update(headers) for page in range(1, max_page + 1): url = base_url.format(page) try: resp = session.get(url, timeout=10) resp.encoding = "utf-8" if resp.status_code == 200: page_source = resp.text # 这里交给解析模块处理,采集层只负责拿 HTML 源码 else: print(f"第{page}页请求失败,状态码:{resp.status_code}") break except requests.RequestException as e: print(f"第{page}页请求异常:{e}") break time.sleep(1 + page % 3)

代码逻辑上,这里用requests.Session()而不是每次单独requests.get(),主要是为了复用底层的 TCP 连接,维持 session 级的 Cookie,对模拟浏览器行为有帮助。time.sleep(1 + page % 3)是一个简单的随机延时策略,避免机器似的固定间隔被服务端风控模型识别。超时时间设成 10 秒,避免网络抖动时脚本长时间卡死。

2.2 XPath 解析与字段抽取的踩坑点

请求拿到的 HTML 只是基础原料,真正精细的活是 XPath。包括小区名称、户型、面积、朝向、楼层、租金、所在区县这些字段,在 DOM 里的相对位置并不完全一样,但都在列表项节点内部。常用的做法是先定位到每个房源卡片节点,再在该节点的上下文里做相对 XPath 查询。用//div[@class='content__list--item']可以一次性拿到当前页的全部房源节点列表,随后遍历每个节点,再取标题、价格、描述信息里的各项数据。

XPath 有几个容易出错的地方值得写出来。第一,text()获取的是当前节点直接文本,如果目标文本在子节点里,要改用string(.//div[@class='des'])这类字符串函数。第二,链家房源描述里的“2室1厅 | 60平米 | 南 北 | 低楼层”是一整段拼接文本,需要用split("|")再按索引拆分,拆分前先做strip()去掉首尾空格。第三,价格节点里“8500元/月”的“元/月”需要replace掉,否则进 DataFrame 后是字符串,没法做数值统计。

def parse_page(html): tree = etree.HTML(html) items = tree.xpath("//div[@class='content__list--item']") rows = [] for item in items: try: title = item.xpath(".//p[@class='content__list--item--title']/a/text()") desc = item.xpath(".//p[@class='content__list--item--des']//text()") price = item.xpath(".//span[@class='content__list--item-price']/em/text()") desc_text = " ".join([t.strip() for t in desc if t.strip()]) parts = [p.strip() for p in desc_text.split("|")] row = { "title": title[0].strip() if title else "", "region": parts[0] if len(parts) > 0 else "", "layout": parts[1] if len(parts) > 1 else "", "area": parts[2] if len(parts) > 2 else "", "orient": parts[3] if len(parts) > 3 else "", "floor": parts[4] if len(parts) > 4 else "", "price": price[0].strip() if price else "", } rows.append(row) except Exception as e: print(f"解析单条数据失败:{e}") return rows

这里的try-except不是装饰,是爬虫解析的必要防线——某个房源缺字段会导致整个列表页解析中断。解析单条失败时打印日志并继续循环,保证单页数据不因一处脏数据而全部丢失。//text()拿到的是该节点下全部文本," ".join()可以避免“描述字段被拆成多个文本节点”导致的错位。

2.3 请求封禁的边界识别

爬虫写着容易,封禁来得也快。当服务端识别出异常流量时,返回内容不会直接报错,而是返回一个验证页,页面里没有房源节点,XPath 匹配结果为空列表。因此判断封禁不能只看status_code,还要看解析后items的长度。如果连续两页拿到的房源节点数量都是 0,脚本应该主动终止而不是继续空转。这个项目源码里没有写得很细,我建议你加一个empty_page_count计数器来兜底。

另外,登录态的维持也是关键时刻。部分城市或部分站点在翻到 10 页以后会要求登录,特征是响应内容里出现二维码图片节点。遇到这种边界,采集脚本要做的是停止翻页、保存已采数据、打印可读性提示。不要试图绕过登录态,课程设计场景里采集前 10 页已经足够支撑数据分析。

3. data_house.xls 只是结果物:清洗层决定数据能不能用

3.1 源码里的 Excel 结构

项目压缩包里有一个data_house.xls文件,这是爬虫采集完成后的持久化产物。打开看列,会发现它和解析模块里的 row 字段一一对应:标题、区域、户型、面积、朝向、楼层、价格。这里藏着一个很典型的问题:XLS 文件里的“面积”列是类似 “60平米”“88.5平” 这样的字符串,“价格”列也带着单位后缀。pandas 读进来后,dtype全是object,别说做均值,连排序都是按字符串字典序排的。所以拿到项目后第一件事,不是去调爬虫,而是先读这个 Excel 看字段类型。

3.2 pandas 字段清洗与类型规约

清洗的核心思路是:从字符串里用正则把数字抠出来,再做类型转换。面积字段可能是“60平米”“60㎡”“60平”三种写法混在一起,价格字段偶尔还会出现“押一付三”这类包含汉字的噪音。统一步骤是先str.extract提取连续数字和小数点,再pd.to_numeric转换,忽略无法解析的值。

还要注意的是,data_house.xls是旧版 Excel 格式,读取需要依赖xlrd库。如果你的环境里没装,pd.read_excel会报错。解决办法是安装xlrd>=1.2.0,或者用openpyxl先转成 xlsx 格式再读。这个项目里还有一个data_house.xls的坑:它是人工导出或脚本写入的,列名可能带前后空格,读取时应显式指定列名而不是依赖列表位置。

import pandas as pd df = pd.read_excel("data_house.xls", engine="xlrd") df.columns = [c.strip() for c in df.columns] df["area_num"] = df["area"].astype(str).str.extract(r"(\d+\.?\d*)")[0] df["price_num"] = df["price"].astype(str).str.extract(r"(\d+\.?\d*)")[0] df["area_num"] = pd.to_numeric(df["area_num"], errors="coerce") df["price_num"] = pd.to_numeric(df["price_num"], errors="coerce") # 单价(元/平米/月),用于区域间的横向对比 df["unit_price"] = (df["price_num"] / df["area_num"]).round(2) df = df.dropna(subset=["area_num", "price_num"]) df = df[df["area_num"] > 0]

extract(r"(\d+\.?\d*)")的匹配逻辑要先解释一下:\d+匹配至少一位数字,\.?匹配可能出现的小数点,\d*匹配小数点后面的零位或多位数字。这个组合在爬虫后期拿到错乱文本时远比先replaceastype(float)稳。errors="coerce"的作用是把无法转换的值变成NaN,而不是中断报错,这样就能用dropna统一过滤脏记录。

3.3 清洗时最容易忽略的重复值

源站点列表页里偶尔会出现同一套房被重复采集的情况,原因是该房源同时挂了多个关键词标签。去重时不能用整行比较,要用“标题 + 面积 + 价格”三字段联合判断。这比包含地址或小区的判重要安全,因为标题里可能包含促销信息,而“小区 + 面积 + 租金”的组合在同一个站点内几乎不可能重复。

df = df.drop_duplicates(subset=["title", "area_num", "price_num"], keep="first")

这个操作看似简单,但会直接影响后续所有聚合结果的真实性。答辩时如果评委问“你怎么保证数据质量”,这条加上前面的正则提取逻辑,至少能说明你有数据质量意识。

4. 租房数据分析.ipynb 里的变量关系:统计维度怎么选

4.1 描述统计与分布判断

拿到清洗后的 DataFrame,不要急着画图。先执行df.describe(),看面积、价格、单价的均值、中位数、标准差、分位数。这个项目的核心是“北京市租房价格分析”,分析结论的可靠性取决于你对异常值的容忍度。比如某套房源面积 5 平米、月租 3000 元,这可能是个隔断单间,但如果面积被解析成 0.5,那显然是脏数据。一般会把面积低于 8 平米或高于 300 平米的数据剔除,也可以按百分位置信区间过滤,用quantile截取 1%~99% 的区间。

分布判断是这章里最有价值的步骤。直方图能反映租金是不是长尾分布,如果直接对原始租金做均值,几套高价公寓会把整体水平拉高。常见的处理是对租金取log1p,输出“对数租金”后再观察分布。当评委问“为什么用中位数不用均值”,你可以回答因为租金分布右偏,中位数更能代表普通房源水平。

4.2 分组聚合:区域、朝向、户型的横向对比

数据分析部分的经典输出是“哪个区租金最高”“哪个朝向最贵”“一居室性价比在哪”。这对应 pandas 里的groupbyagg操作。区域字段在清洗时已经拆出来了,朝阳、海淀、丰台、通州这些分组的粒度足够支撑课程设计的分析章节。

region_stats = ( df.groupby("region")["price_num"] .agg(["count", "mean", "median", "std"]) .round(2) .sort_values("median", ascending=False) )

agg里对同一列同时算四个统计量,是为了在投影到可视化时能画误差条或者标注样本量。排序用median而不是mean,是为了避免个别超高价房源把排名带偏。

接着可以做交叉分析:面积区间和价格的透视表。先把面积切成几个桶,再对每个桶内的价格做中位数统计。这种分析比单纯的“正相关”更有说服力,因为它能看出面积区间对租金的边际影响。

bins = [0, 40, 60, 90, 120, 200, 500] labels = ["0-40", "40-60", "60-90", "90-120", "120-200", "200+"] df["area_bucket"] = pd.cut(df["area_num"], bins=bins, labels=labels) pivot = pd.pivot_table( df, index="area_bucket", columns="orient", values="price_num", aggfunc="median", )

pd.cut是按区间均匀切分还是按分位数切分,直接决定分析视角。这里用固定边界,方便业务理解;如果想要每个桶里样本量均衡,可以改用qcut。运行 Notebook 时建议两个版本都跑一下,对比差异后选一个写进报告,这属于“分析过程有对比”的加分项。

4.3 相关性与异常点归因

面积和租金之间通常呈现正相关,但散点图里总有几个点游离在拟合线上方。这些点往往对应“学区房”“精装修”“地铁步行五分钟”。可以从数据中把残差大的样本挑出来,单独打印标题字段,看看是什么让它们贵出平均线。这个分析动作是把数据分析从“会跑 pandas”抬升到“会做归因”的关键一步。

import numpy as np # 线性回归,粗略看面积对租金的解释力度 x = df["area_num"].values.reshape(-1, 1) y = df["price_num"].values

如果你不想引入 sklearn 全家桶,用numpy.polyfit(x, y, 1)也能算斜率和截距。重点是残差分析:residual = y - (slope * x + intercept),把残差按降序排列,取前 10 条,再回原表查这些房源的行政区和描述。这一步很容易在答辩时展开讲,因为它展示了“数据分析不是只跑通一个函数”。

5. 可视化层选型:Matplotlib 静态图与 Pyecharts 交互图的边界

5.1 什么时候用 Matplotlib

图表的选择要看使用场景。课程设计报告里需要的是导出后不依赖浏览器就能看懂的图,Matplotlib 是更稳的选择。它的默认主题虽然不时尚,但胜在可控性强,图片尺寸、字体大小、坐标轴刻度都能精确指定。比如区域均价柱状图,用 Matplotlib 画出来可以直接savefig成 300dpi 的图片插到 Word 里;Pyecharts 渲染出的 HTML 也可以截图,但图片分辨率受浏览器窗口影响,批量生成时每张图的大小不一定统一。

画区域均价柱状图时有一个值得注意的参数:如果直接用matplotlib.rcParams["font.sans-serif"] = ["SimHei"],在 Mac 和 Linux 上会找不到 SimHei 字体导致中文变方块。稳妥的做法是用matplotlib.font_manager.FontProperties(fname="字体文件路径")指定字体文件,或者使用系统自带的Arial Unicode MS(Mac 上常见)。课程设计项目里常看到的另一个中文乱码来源是savefig时没带dpi参数,默认 100dpi 导出后标题出现锯齿。

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False fig, ax = plt.subplots(figsize=(10, 6)) x_pos = range(len(region_stats)) ax.bar(x_pos, region_stats["median"], color="#4C72B0", alpha=0.85) ax.set_xticks(list(x_pos)) ax.set_xticklabels(region_stats.index, rotation=30) ax.set_ylabel("租金中位数(元/月)") ax.set_title("北京市各区域租房租金中位数对比") fig.tight_layout() plt.savefig("region_median.png", dpi=300)

axes.unicode_minus=False必须设置,否则负号渲染成方块。tight_layout()负责让轴标签和标题不互相遮挡。

5.2 Pyecharts 交互图的配置项

Pyecharts 的价值在于展示多维度信息:鼠标悬停能看到具体数值、区域可以点击缩放、图表可以联动筛选。适合答辩现场把 HTML 放到浏览器里演示,视觉效果远好于静态图。但它的配置也比 Matplotlib 繁琐,图表的宽度、高度、字体、颜色都需要在初始化时显式设置。

项目中的 Notebook 如果用了 Pyecharts,常见做法是生成区域租金的地图热力图。地图需要安装pyecharts和对应的地图扩展包,新版本里地图数据已经内置,但echarts-countries-js这类扩展在 2.x 版本里需要单独处理。如果你只是画柱状图和折线图,不需要地图依赖,BarLine模块开箱即用。

from pyecharts.charts import Bar from pyecharts import options as opts bar = ( Bar(init_opts=opts.InitOpts(width="900px", height="500px", theme="light")) .add_xaxis(region_stats.index.tolist()) .add_yaxis("租金中位数", region_stats["median"].round(0).tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="北京各区域租金中位数"), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=30)), ) ) bar.render("region_median.html")

add_yaxis里的数据必须传 Python 列表,numpy 的ndarray需要先.tolist(),否则某些版本会报序列化错误。坐标轴标签旋转 30 度是为了避免“石景山区”“密云区”这些四字区域名重叠。theme="light"明确指定亮色主题,防止默认主题在深色背景演示时看不清。

5.3 图表的取舍逻辑

同一个数据集,既可以用 Matplotlib 画面积-租金散点图,也可以用 Pyecharts 画可缩放的散点图。但课程设计里不需要所有图都做成交互式。报告文档里的图用 Matplotlib,现场演示的图用 Pyecharts,这样的组合能兼顾阅读性和展示性。再说得直白一点:你的导师大概率会打开 PDF/word 版报告看内容,只有答辩现场才会点开 HTML,所以静态图是报告里必须有的,交互图是加分项。

另外,如果发现 Notebook 里画图耗时很久,先检查是不是在循环里反复render()。Pyecharts 每个图都调用一次render会写多个 HTML,最好先收集全部图表对象,最后统一渲染。数据量大时,Bardataset配置比逐条add_yaxis更高效,但对于课程设计的数据量,差异不大。

6. 把爬虫跑起来的关键检查点与时间换空间的改进

6.1 运行前置:python 环境与依赖安装

这份源码的主文件是“北京租房爬虫.py”和“北京租房数据分析.ipynb”。拿到后先别急着python 北京租房爬虫.py,先确认依赖。爬虫部分至少需要requestslxmlpandasopenpyxl(写 xls 时可能还需要xlwtxlrd)。Notebook 部分需要jupytermatplotlibpyecharts。用pip install requests lxml pandas openpyxl xlrd matplotlib pyecharts jupyter一条命令装齐即可。如果你的 python 环境里同时存在 2.x 和 3.x,命令可能要换成pip3,这是 python 安装后最常见的环境坑。

跑爬虫前建议手动打开一次目标网站,确认页面结构没变。如果结构变了,XPath 会失配,脚本不会报语法错误,而是解析出全空的 rows,最后写进 Excel 的数据全是空行。验证方法是跑完脚本后立刻打印df.shape,如果行数明显少于预期,说明目标页面的 DOM 类名发生了变动,需要重新检查选择器。

6.2 高频报错与定位方式

爬虫运行中遇到AttributeError: 'NoneType' object has no attribute 'xpath',这说明上一级节点没匹配到。去看源代码里对应层级的 class 名称是否带空格或版本号。链家的 class 名称偶尔会带content__list--item这种 BEM 风格前缀,复制时容易漏掉双下划线。把报错信息里的元素打印出来,或者直接把resp.text写到本地 HTML 文件用浏览器打开检查,是定位这类问题最快的方式。

Notebook 里KeyError多半是列名不匹配,原因是之前清洗阶段已经改过列名,后面的代码还在用旧列名。比如area清洗后变成了area_num,后续如果写df["area"]就直接报错。建议在 Notebook 的开头单独放一个单元格,输出df.columns.tolist(),确认列名再往后走。这是数据科学导论里最基础但最实用的习惯。

6.3 改造成其他城市租房爬虫的关键步骤

换城市的核心是把base_url里的bj换成目标城市的拼音缩写,比如上海换sh、深圳换sz。但不要以为这样就完了,很多城市的房源列表页可能默认筛选条件不同,导致部分字段缺失。改造完先跑 1 页,确认字段非空再放开翻页。数据量需求不大时,每页延时从 1 秒调到 2 秒更安全,请求频率低一些,触发风控的概率会明显下降。这里不讨论任何变更出口 IP 的手段,单靠 UA 伪装和延时控制,只采集少量公开列表数据足够支撑课程设计。

6.4 增量采集与定时任务

如果想把爬虫做成每周自动跑一次的任务,可以写一个简单的main()函数,把采集、清洗、保存三步串起来,再用系统自带的 cron(Linux/macOS)或任务计划程序(Windows)定时执行。这样能积累一个长周期的数据集,之后做“租金环比变化”或者“毕业季租金波动”分析,会比一次性采集更有分析深度。这也是能明显拉开与普通课程设计差距的真实工程做法。

if __name__ == "__main__": all_rows = [] for page in range(1, 6): # 测试阶段只跑5页,验证稳定后放开 html = fetch_page(page) rows = parse_page(html) print(f"第{page}页解析出{len(rows)}条") all_rows.extend(rows) time.sleep(1.5) df = pd.DataFrame(all_rows) df.to_excel("data_house_test.xls", index=False)

最后检查输出文件的行数、列数、是否有空值,再决定是否扩大页数。这样做的理由很简单:把爬虫跑通只是第一步,能稳定复现并且知道每一步的边界在哪里,才是这份源码真正值得借鉴的部分。

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

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

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

立即咨询