简介:这是一套面向Python爬虫与数据分析学习者的实战项目源码包,聚焦链家广州二手房市场,帮助读者掌握从网页数据采集到可视化呈现的完整流程。项目以requests、BeautifulSoup、lxml、pandas等库为核心,覆盖房源地址、面积、价格、楼层、朝向等字段的抓取与清洗,并针对请求头、代理IP、Cookies等反爬策略给出应对思路。资源包共13个文件,包含9个Python脚本、2个Markdown说明文档、1个gitignore及1份CSV数据文件,压缩后约146KB,脚本按功能拆分为数据获取、清洗、统计与多类图表绘制模块,目录结构清晰。数据可视化部分涵盖总价与面积散点关系、朝向饼图、装修类型分布、各小区在售数量及均值中位数统计等分析维度,可直观呈现区域差异与价格波动规律。目前已有187人学习下载,适合希望积累爬虫与可视化项目经验、需要完整可运行案例的初中级开发者参考。
1. 从一份广州二手房 CSV 说起:这套源码到底能跑出什么
很多人第一次做房价分析,卡住的不是画图,而是拿不到干净数据。这份guangzhou-second-hand-room-crawl-master.zip解决的就是这个前置问题:它把链家广州二手房列表页的抓取、字段抽取、落盘 CSV、再到多张可视化图表的脚本打包在一起,解压后直接能看到获取网页内容.py、数据清洗.py、广州房价信息.csv以及一批按分析维度拆分的绘图脚本。适合两类人:一类是想跑通「爬取—清洗—可视化」完整链路的 Python 入门者,另一类是需要一份可改字段、可换城市的二手房分析脚手架的数据从业者。它不追求分布式调度,也不做实时增量,胜在结构清晰、脚本粒度细,每个分析点单独成文件,改起来不用翻几百行。下面按「资源是什么 → 怎么用 → 坑在哪」的顺序拆开讲。
2. 拆包看结构:requests + BeautifulSoup + pandas 三段式怎么落地
2.1 目录里每个脚本对应哪一步
解压后先别急着运行,把文件按职责分一下类,后面排错会快很多。核心文件大致是这么分工的:
| 文件 | 职责 | 依赖 |
|---|---|---|
获取网页内容.py | 发请求、翻页、解析列表页、写 CSV | requests、BeautifulSoup、lxml |
数据清洗.py | 去重、单位换算、缺失值处理 | pandas |
广州房价信息.csv | 已抓好的样本数据 | 无 |
各类数据项的平均值及其中位数.py | 描述性统计 | pandas |
总价与面积散点关系.py | 面积与总价相关性 | matplotlib |
朝向饼图.py/装修类型.py | 分类占比 | matplotlib |
查看各小区现存出售数目.py | 分组计数 | pandas |
多图.py | 多子图汇总 | matplotlib |
这套结构的好处是「一个分析问题一个文件」,坏处是没有统一入口,得手动按顺序跑。常见做法是先跑抓取,再跑清洗,最后跑各分析脚本。
2.2 抓取脚本的核心逻辑与参数
获取网页内容.py走的是最经典的 requests + BeautifulSoup 路线。链家列表页是服务端渲染,房源信息直接在 HTML 里,所以不需要浏览器自动化,这也是它比 Playwright 方案轻的原因。核心骨架大概是这样:
import requests from bs4 import BeautifulSoup import csv, time, 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", "Referer": "https://gz.lianjia.com/ershoufang/", } def fetch_page(url): # timeout 必设,否则网络抖动时脚本会一直挂着 resp = requests.get(url, headers=HEADERS, timeout=10) resp.encoding = "utf-8" # 链家页面是 utf-8,不设会乱码 if resp.status_code != 200: print("状态码异常:", resp.status_code) return None return resp.text def parse_list(html): soup = BeautifulSoup(html, "lxml") rows = [] for li in soup.select("ul.sellListContent > li"): title = li.select_one("div.title a") total = li.select_one("div.totalPrice span") unit = li.select_one("div.unitPrice span") rows.append({ "标题": title.get_text(strip=True) if title else "", "总价(万)": total.get_text(strip=True) if total else "", "单价(元/平)": unit.get_text(strip=True) if unit else "", }) return rows if __name__ == "__main__": all_rows = [] for pg in range(1, 101): # 链家列表页最多到 100 页 url = f"https://gz.lianjia.com/ershoufang/pg{pg}/" html = fetch_page(url) if not html: continue all_rows.extend(parse_list(html)) time.sleep(random.uniform(1, 3)) # 随机间隔,别用固定 sleep with open("广州房价信息.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["标题", "总价(万)", "单价(元/平)"]) writer.writeheader() writer.writerows(all_rows)逻辑说明:fetch_page负责请求和编码,parse_list用 CSS 选择器定位每个房源卡片。参数上,timeout=10是防止单页卡死拖垮整轮;encoding="utf-8"必须显式设,requests 有时会猜成 ISO-8859-1 导致中文乱码;range(1, 101)是链家列表页的硬上限,超过 100 页会返回空列表,不是报错,容易被误判成「抓完了」。sleep用random.uniform而不是固定值,是因为固定间隔的请求节奏太规律,容易被识别。
2.3 清洗脚本要处理的四类脏数据
抓下来的 CSV 不能直接进分析,数据清洗.py主要干四件事。第一是单位统一:总价字段带「万」,单价带「元/平米」,得用正则把数字抠出来转 float。第二是面积字段,有的写「89.5平米」,有的写「89.5㎡」,符号不统一。第三是缺失值,楼层、朝向、装修这些字段经常为空,要决定是填充还是丢弃。第四是异常值,比如单价个位数或上百万的,基本是解析错位。
import pandas as pd import re df = pd.read_csv("广州房价信息.csv", encoding="utf-8-sig") def to_num(x): # 抠出数字部分,兼容 "89.5平米" "1200万" 这类混合字符串 if pd.isna(x): return None m = re.search(r"[\d.]+", str(x)) return float(m.group()) if m else None df["总价"] = df["总价(万)"].apply(to_num) df["单价"] = df["单价(元/平)"].apply(to_num) # 面积字段若单独抓取,同样处理 if "面积" in df.columns: df["面积"] = df["面积"].apply(to_num) # 丢弃关键字段缺失的行,而不是无脑 fillna(0) df = df.dropna(subset=["总价", "单价"]) # 用分位数卡异常值,比 3σ 更抗偏态分布 q1, q99 = df["单价"].quantile([0.01, 0.99]) df = df[(df["单价"] >= q1) & (df["单价"] <= q99)] df.to_csv("广州房价信息_clean.csv", index=False, encoding="utf-8-sig")参数说明:quantile([0.01, 0.99])卡掉首尾各 1% 的极端值,比均值±3 倍标准差稳,因为房价分布本身右偏,标准差会被高价房源拉大。dropna(subset=...)只对关键列去空,避免把朝向为空但价格正常的房源一起删掉。encoding="utf-8-sig"是为了 Excel 打开不乱码,纯 utf-8 在 Windows 上会显示 BOM 头。
3. 可视化脚本逐个跑:从散点图到朝向饼图的参数调法
3.1 总价与面积散点图怎么看出相关性
总价与面积散点关系.py是最能说明问题的一张图。二手房总价和面积理论上强正相关,但实际数据里会因为地段、楼层、装修出现大量离群点。画的时候有两个参数值得调:alpha控制点的透明度,样本量大时不设会糊成一团黑;s控制点大小,默认偏大。
import pandas as pd import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # 中文标签必须设,否则方块 plt.rcParams["axes.unicode_minus"] = False df = pd.read_csv("广州房价信息_clean.csv", encoding="utf-8-sig") plt.figure(figsize=(10, 6)) plt.scatter(df["面积"], df["总价"], alpha=0.4, s=15, c="steelblue") plt.xlabel("建筑面积(平米)") plt.ylabel("总价(万)") plt.title("广州二手房总价与面积散点关系") # 顺手算个皮尔逊系数,判断线性相关强度 corr = df["面积"].corr(df["总价"]) plt.text(df["面积"].min(), df["总价"].max(), f"相关系数 r = {corr:.2f}", fontsize=12) plt.tight_layout() plt.savefig("scatter.png", dpi=150) plt.show()逻辑说明:alpha=0.4让重叠点颜色叠加,密度高的区域自然变深,比单纯计数更直观。corr()默认算皮尔逊系数,如果数据里有极端值,可以换成method="spearman"看秩相关。dpi=150是保存清晰度,屏幕显示用默认 100 就够,写进报告建议 150 以上。
3.2 朝向饼图与装修类型:分类字段的清洗前提
朝向饼图.py和装修类型.py逻辑类似,都是对分类字段做value_counts()再画饼图。但这两个脚本能不能跑出合理结果,取决于清洗阶段有没有把分类字段归一化。原始数据里朝向可能是「南」「南北」「东南」混着,装修可能是「精装」「精装修」「简装」多种写法。
import pandas as pd import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False df = pd.read_csv("广州房价信息_clean.csv", encoding="utf-8-sig") # 归一化:把同义写法合并,否则饼图会碎成十几块 df["朝向"] = df["朝向"].str.replace("南北", "南北通透") counts = df["朝向"].value_counts().head(8) # 只取前 8 类,长尾合并 plt.figure(figsize=(8, 8)) plt.pie(counts, labels=counts.index, autopct="%1.1f%%", startangle=90) plt.title("广州二手房朝向分布") plt.savefig("orientation.png", dpi=150) plt.show()参数说明:head(8)是控制饼图可读性,分类超过 8 个饼图就没法看了,长尾可以合并成「其他」。autopct="%1.1f%%"显示一位小数百分比。startangle=90让第一块从正上方开始,视觉上更符合阅读习惯。如果value_counts()结果里出现大量空字符串,说明清洗阶段没处理缺失,得回去补。
3.3 小区在售数量:分组计数的排序陷阱
查看各小区现存出售数目.py用的是groupby().size(),但直接画出来往往很难看,因为小区名长短不一,横坐标会挤成一团。常见做法是取 Top 15 并横向画条形图。
import pandas as pd import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False df = pd.read_csv("广州房价信息_clean.csv", encoding="utf-8-sig") top = df["小区"].value_counts().head(15).sort_values() plt.figure(figsize=(10, 8)) plt.barh(top.index, top.values, color="coral") plt.xlabel("在售房源数") plt.title("广州在售房源数量 Top 15 小区") plt.tight_layout() plt.savefig("community.png", dpi=150) plt.show()逻辑说明:sort_values()升序是为了横向条形图从上到下递减,barh比bar更适合长标签。tight_layout()防止小区名被裁掉。如果小区字段在原始数据里是空的,说明抓取时没定位到对应节点,得回抓取脚本补选择器。
4. 避坑与排查:跑这套源码最容易翻车的五个地方
4.1 抓回来是空列表,但状态码是 200
现象:脚本不报错,CSV 里一行数据都没有。原因:链家对高频请求会返回一个结构相同但列表为空的页面,状态码仍是 200,不是 403。解决:在parse_list里加一行判断,如果rows为空就打印页码并sleep更长时间,同时检查HEADERS里的User-Agent是不是太旧。
4.2 中文全部变成乱码
现象:CSV 打开是「ä½ å¥½」这种。原因:requests 默认按响应头猜编码,链家有时不返回 charset,就猜成 ISO-8859-1。解决:请求后手动resp.encoding = "utf-8",写 CSV 时用utf-8-sig。这两个地方要同时改,只改一个还是会乱。
4.3 单价字段抠出来是空
现象:清洗后单价列大量 NaN。原因:列表页的单价和详情页的单价 DOM 结构不同,选择器div.unitPrice span在列表页可能取到的是「单价」两个字而不是数字。解决:打印一条li的 HTML 看实际结构,把选择器改成div.unitPrice span下的具体文本节点,或者改用li.select_one("div.unitPrice").get_text()再正则抠数字。
4.4 饼图中文显示成方块
现象:标题和标签全是方框。原因:matplotlib 默认字体不含中文。解决:plt.rcParams["font.sans-serif"] = ["SimHei"],Mac 上换成["Arial Unicode MS"],Linux 上如果没有 SimHei 要装中文字体或换WenQuanYi。另外axes.unicode_minus = False要一起设,否则负号也会出问题。
4.5 翻页到后面返回重复数据
现象:CSV 里同一房源出现多次。原因:链家列表页在筛选条件变化或请求被限时,可能返回缓存页,导致相邻页内容重复。解决:抓完后按「标题 + 总价」去重,df.drop_duplicates(subset=["标题", "总价"], keep="first")。更稳的做法是每抓一页就把 URL 记下来,重复 URL 直接跳过。
5. 进阶玩法:把这份脚手架改成可复用分析模板
跑通默认流程后,这套源码真正的价值在于它的可扩展性。我一般会做三件事把它变成自己的模板。第一,把抓取脚本里的城市前缀抽成变量,gz.lianjia.com换成sz、sh就能抓其他城市,但要注意不同城市的列表页 DOM 可能有细微差异,选择器得重新验证。第二,把清洗逻辑封装成函数,输入原始 CSV 输出干净 DataFrame,这样换数据源时只改解析部分。第三,给可视化脚本加一个统一的config.py,把字体、配色、图片尺寸、dpi 集中管理,避免每个文件重复设rcParams。
验证数据质量有个简单办法:清洗后打印df.describe(),看单价的均值和分位数是否落在合理区间。广州二手房单价大致在 2 万到 8 万之间,如果均值只有几千或者几十万,基本可以确定是解析错位或单位没换算。另一个检查点是df["面积"].between(20, 500).mean(),正常应该在 0.95 以上,低于这个值说明面积字段脏得厉害。
还有一个容易被忽略的技巧:抓取时把原始 HTML 也存一份到本地,用页码.html命名。这样调试选择器时不用反复请求,直接读本地文件解析,既快又不会触发限流。等选择器调稳了再开真实抓取。我早期做这类项目时,因为没存原始页面,选择器一改就得重抓,白白浪费了一下午。从那以后我每次抓取都强制先落一份 HTML 快照,再跑解析。希望帮到你。
本文还有配套的精品资源,点击获取