尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

链家二手房爬虫实战:requests与BeautifulSoup数据采集全解析

链家二手房爬虫实战:requests与BeautifulSoup数据采集全解析 1. 链家二手房爬虫项目概述1.1 为什么选择链家作为爬取目标链家是目前国内房源信息最规范、数据最透明的房产平台之一它跟很多信息聚合平台不一样每套房源的挂牌价、建筑面积、户型、朝向、楼层、装修情况、年代、关注人数这些字段基本都有统一的展示规范。尤其是“单价”这个字段链家按建筑面积计算后直接显示在房源卡片和详情页里省去了自己去算的麻烦。对于想练手爬虫的人来说链家的反爬强度属于“中等偏上”既不像一些政府网站那样完全不设防也不像电商平台那么变态。它主要靠 User-Agent 校验、请求频率限制、验证码这三种手段做防护正好适合用来学习 requests BeautifulSoup 这套最常见的爬虫组合。我见过很多新手一上来就去爬淘宝、拼多多结果被各种滑块验证码、签名参数折磨到怀疑人生最后连数据长什么样都没见到。相比之下链家这套页面结构简洁、接口稳定爬到数据的那一刻能获得很实在的正反馈。这篇文章我会从零开始把整个爬取思路、代码实现、字段说明、常见坑全部过一遍。你只要照着敲能把链家北京、上海、深圳、成都这类城市在售的二手房列表信息完整抓下来存成 CSV 或 Excel后续做价格分析、行情趋势、板块对比都没问题。1.2 爬虫的合规边界与使用说明写爬虫之前有几个底线问题必须先讲清楚。链家的《用户协议》明确禁止未经许可的批量抓取行为所以这个项目只能用于个人学习、技术研究、数据分析练习不能拿去商用也不建议用高并发去攻击对方的服务器。爬取过程中要注意三个原则第一控制请求频率我下面代码里会加time.sleep()随机延时每抓一页停留 1 到 3 秒这是对目标网站的起码尊重第二只抓公开页面数据不碰需要登录才能看的信息不提交表单不遍历接口漏洞第三爬下来的数据不要公开传播原始数据集自己做做分析没问题发到 GitHub 上最好只放代码不放数据。说白了爬虫本身是中性技术关键是用的人怎么用。我平时做数据采集项目最核心的一条经验就是把对方服务器当成自己的服务器来对待你不想自己的服务被人狂刷那就别去狂刷别人的。2. 技术选型与抓取思路拆解2.1 requests BeautifulSoup 还是 Scrapy爬虫技术栈的选择往往决定了整个项目的复杂度和维护成本。链家二手房信息爬取这个问题我强烈建议新手从 requests BeautifulSoup 入手而不是一上来就上 Scrapy。原因很简单链家的数据不需要执行 JavaScript 渲染所有房源信息都直接写在 HTML 里属于典型的服务端渲染页面。你打开链家二手房列表页右键“查看网页源代码”能直接看到房源标题、价格、户型这些文本。这种场景下 requests 发一个 GET 请求拿到 HTML然后用 BeautifulSoup 做解析就够了。Scrapy 虽然功能强大、自带并发和管道机制但它的学习曲线比较陡项目结构对新手来说有点重等你把 pipelines、middlewares、settings 这些概念搞明白半小时都过去了。等以后遇到需要登录、需要携带复杂 Cookie、需要分布式采集的项目再迁移到 Scrapy 也不迟。当前阶段简单直接就是最大的优势。2.2 链家页面结构与 URL 规律分析以我这次爬取的链家二手房列表为例每个城市的房源列表页 URL 规律非常清晰。北京的在售房源列表是https://bj.lianjia.com/ershoufang/pg2/bj是城市拼音缩写ershoufang是二手房频道pg2表示第 2 页。其他城市只要替换城市缩写就行比如上海是sh、广州是gz、成都是cd规则完全统一。第一页的 URL 可以不加pg1直接写https://bj.lianjia.com/ershoufang/。每个城市在售房源数量不一样北京一般在一万套左右按每页 30 条计算总共三百多页。链家最多开放 100 页的翻页限制所以实际能爬到的最多就是 3000 条左右再往后的页码会返回空白或者直接跳到错误页。这个限制我们后面会说如何应对。打开任意一个列表页每套房源的信息都在一个div标签里面class 为info。这里面包含标题、小区名称、位置、户型、面积、朝向、装修、楼层、年代、总价、单价、关注人数这些关键字段。我用浏览器开发者工具逐个定位了每个字段的 CSS 选择器后面代码部分会详细展示。2.3 反爬机制与应对策略链家的反爬目前主要有三道关卡。第一道是 User-Agent 校验。如果你不带浏览器标识直接请求服务器会返回 403 或者一个空页面。解决办法是构造一个常见的 Chrome 浏览器 UA 放进请求头里让请求看起来像真实浏览器发出的。第二道是 IP 频率检测。同一 IP 短时间大量请求会触发验证码或者 IP 封禁。应对方案是控制抓取速度每页间隔 1-3 秒随机延时尽量模拟人的浏览节奏。如果需要大批量抓取可以考虑代理池轮换但对链家这个项目来说单 IP 加上合理延时足够完成任务没必要一上来就上代理。第三道是验证码。触发频率检测后页面会跳转到验证码页面输入正确的验证码才能继续访问。遇到这种情况最有效的办法不是去搞 OCR 识别而是立刻停止程序、休息一段时间建议至少 30 分钟检查是否请求太快了。3. 完整代码实现与讲解3.1 依赖安装与项目结构开始写代码之前先准备好运行环境。我用的 Python 版本是 3.9如果是 3.7 及以上都没有问题。需要安装的第三方库只有两个requests 和 beautifulsoup4。用 pip 安装就行pip install requests beautifulsoup4如果你想把数据存成 Excel 而不是 CSV顺手装一个 pandaspip install pandas整个项目结构非常简单我习惯把文件组织成下面这样lianjia_spider/ ├── spider.py # 主爬虫脚本 ├── data/ # 存放爬取结果的目录 └── requirements.txt # 依赖清单requirements.txt内容requests2.28.0 beautifulsoup44.12.03.2 定义请求头和爬虫主类我现在直接给出一个完整的链家二手房爬虫代码代码里每一步都有注释。先定义请求头和主类import csv import random import time import requests from bs4 import BeautifulSoup HEADERS { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 ), Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, }关于请求头的构造有几个细节值得说。User-Agent字符串最好完整地从真实浏览器复制不要随便简写。有些网站的防护逻辑会检查 UA 的完整性和版本号新版 Chrome 的 UA 长得跟旧版不一样你要是拿一个 Chrome 80 的 UA 去访问有些服务器也会怀疑。另外Accept-Language这项访问国内网站时可以设置成zh-CN,zh;q0.9这会让服务器返回中文页面解析的时候更省事。3.3 编写页面下载与解析函数接下来是获取页面和解析数据的核心函数class LianJiaSpider: def __init__(self, city, max_pages100): self.city city self.city_code CITY_CODES.get(city, bj) self.base_url fhttps://{self.city_code}.lianjia.com/ershoufang self.max_pages max_pages self.session requests.Session() self.session.headers.update(HEADERS) def fetch_page(self, page): 获取指定页码的列表页 HTML if page 1: url f{self.base_url}/ else: url f{self.base_url}/pg{page}/ try: resp self.session.get(url, timeout10) resp.raise_for_status() resp.encoding utf-8 return resp.text except requests.RequestException as e: print(f[错误] 第{page}页请求失败: {e}) return None这里有个容易被忽视的小知识点resp.encoding utf-8。requests 库有时候会根据响应头猜测编码如果猜错了页面上中文会变成乱码。我建议对链家这种明确声明 UTF-8 编码的网站手动指定一下编码格式解析的时候就稳了。解析函数是整个爬虫的灵魂链家每个房源卡片的结构是有规律可循的。我拆开来说def parse_page(self, html): 解析列表页提取所有房源信息 soup BeautifulSoup(html, html.parser) houses [] # 每个房源卡片所在的容器 li_list soup.select(ul.sellListContent li) for li in li_list: # 标题信息 title_elem li.select_one(.title a) title title_elem.get_text(stripTrue) if title_elem else # 房源信息小区名、户型、面积、朝向、装修、楼层、年代 house_info_elem li.select_one(.houseInfo) house_info house_info_elem.get_text(stripTrue) if house_info_elem else info_parts house_info.split(|) if house_info else [] # 位置信息城区、板块 position_elem li.select_one(.positionInfo) position_info position_elem.get_text(stripTrue) if position_elem else # 总价 total_price_elem li.select_one(.totalPrice) total_price total_price_elem.get_text(stripTrue) if total_price_elem else # 单价 unit_price_elem li.select_one(.unitPrice) unit_price unit_price_elem.get_text(stripTrue) if unit_price_elem else # 关注人数 follow_elem li.select_one(.followInfo) follow_info follow_elem.get_text(stripTrue) if follow_elem else houses.append({ 标题: title, 小区: info_parts[0] if len(info_parts) 0 else , 户型: info_parts[1] if len(info_parts) 1 else , 面积: info_parts[2] if len(info_parts) 2 else , 朝向: info_parts[3] if len(info_parts) 3 else , 装修: info_parts[4] if len(info_parts) 4 else , 楼层: info_parts[5] if len(info_parts) 5 else , 年代: info_parts[6] if len(info_parts) 6 else , 位置: position_info, 总价: total_price, 单价: unit_price, 关注情况: follow_info, }) return houses3.4 数据存储与主流程控制解析完成后的数据需要写到 CSV 文件里。这里有个细节直接用 Python 内置的 csv 模块写入时如果直接用open()而不指定encodingutf-8-sig生成的 CSV 文件用 Excel 打开会出现中文乱码。utf-8-sig会在文件开头写入 BOM 头Excel 就能正确识别编码了def save_to_csv(self, all_data, filename): 将数据保存为 CSV 文件 if not all_data: print([提示] 没有数据需要保存) return fieldnames [ 标题, 小区, 户型, 面积, 朝向, 装修, 楼层, 年代, 位置, 总价, 单价, 关注情况, ] with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(all_data) print(f[完成] 数据已保存至 {filename}共 {len(all_data)} 条) def run(self): all_data [] for page in range(1, self.max_pages 1): print(f[抓取] 正在抓取第 {page} 页...) html self.fetch_page(page) if html is None: continue # 简单检查是否为验证码页面 if 验证码 in html: print(f[警告] 第 {page} 页触发验证码程序停止建议休息一会再试) break page_data self.parse_page(html) if not page_data: print(f[提示] 第 {page} 页没有解析到数据可能已经超出可访问的页码范围结束抓取) break all_data.extend(page_data) # 随机延时模拟真实用户浏览 delay random.uniform(1, 3) time.sleep(delay) self.save_to_csv(all_data, data/lianjia_houses.csv)主流程里有两个关键的保护机制。检查验证码 in html是为了及时止损一看到验证码就退出而不是继续硬刚导致 IP 被拉黑。另一个是if not page_data主动结束循环因为链家很多城市最多只开放前 100 页超过范围的页面是空的继续请求纯属浪费时间甚至可能触发风控。3.5 城市代码映射与入口函数最后补上城市代码映射表和入口函数# 城市代码映射表 CITY_CODES { 北京: bj, 上海: sh, 广州: gz, 深圳: sz, 成都: cd, 杭州: hz, 武汉: wh, 南京: nj, 天津: tj, 重庆: cq, } def main(): city input(请输入城市名称如北京、上海、广州、深圳: ).strip() max_pages input(请输入要抓取的页数默认 50 页: ).strip() if city not in CITY_CODES: print([错误] 暂时不支持该城市或者城市名称输入有误) return if max_pages.isdigit(): max_pages int(max_pages) else: max_pages 50 spider LianJiaSpider(citycity, max_pagesmax_pages) spider.run() if __name__ __main__: main()运行起来的效果大概是这样的pandas is installed? # 不用管这个直接跑 请输入城市名称如北京、上海、广州、深圳: 北京 请输入要抓取的页数默认 50 页: 20 [抓取] 正在抓取第 1 页... [抓取] 正在抓取第 2 页... [抓取] 正在抓取第 3 页... ... [完成] 数据已保存至 data/lianjia_houses.csv共 600 条4. 数据字段说明与清洗经验4.1 链家字段的真实含义爬到数据只是第一步理解每个字段的真实含义才是做分析的关键。我刚开始爬链家的时候看到“单价”和“总价”直接拿来用后来做数据可视化时发现有些房源单价高得离谱仔细看才发现是“车位”或者“别墅”混在里面。链家的字段其实有它自己的口径搞清楚之后再分析才有意义。我整理了一下关键字段的实际含义字段含义与说明标题房源的挂牌标题通常包含小区名和户型关键词小区房源所属小区名称来自houseInfo第一段户型如“3室2厅”表示房间数和客厅数面积建筑面积单位是平方米文本里带“平米”字样朝向如“南 北”表示主要采光方向装修如“精装”“简装”“毛坯”楼层如“低楼层/共6层”包含所在层和总层数年代建筑年代如“2008年建”位置城区和板块如“朝阳 望京”总价挂牌总价万为单位如“728万”单价按建筑面积计算的均价单位是元/平米如“56000元/平米”关注情况“100人关注 / 50次带看”这类数据反映房源热度这里面最需要清洗的是“面积”“总价”“单价”这三个字段因为它们是字符串类型需要转成数值才能做计算和可视化。4.2 数据清洗的必备代码写一段简单的数据清洗代码把字符串转成数值类型import re import pandas as pd df pd.read_csv(data/lianjia_houses.csv) # 提取面积数值 df[面积数值] df[面积].str.extract(r([\d.])).astype(float) # 提取总价数值万 df[总价数值] df[总价].str.extract(r([\d.])).astype(float) # 提取单价数值元/平米 df[单价数值] df[单价].str.extract(r([\d.])).astype(float) # 关注人数 df[关注人数] df[关注情况].str.extract(r(\d)人关注).astype(float) # 朝向处理统一格式 df[朝向] df[朝向].str.replace( , /, regexFalse) # 去掉面积异常值小于5平米的一般是车位或杂物间 df_clean df[df[面积数值] 5] print(df_clean[[标题, 小区, 面积数值, 总价数值, 单价数值, 关注人数]].head())清洗之后你可以直接按板块算均价、按小区统计挂牌量、按户型分布看市场结构。比如用 pandas 的groupby做个简单统计# 按区域板块统计平均单价 avg_price df_clean.groupby(位置)[单价数值].mean().sort_values(ascendingFalse) print(avg_price.head(10))这就是爬虫带来的乐趣——把网上散落的信息变成结构化的数据再用自己的方式去解读它。5. 常见问题与排查技巧实录5.1 爬虫运行中的高频问题速查表我把实际爬取过程中遇到的问题按发生概率排了个序做成一张速查表问题现象可能原因解决方案返回 403 ForbiddenUser-Agent 缺失或异常检查请求头使用完整的浏览器 UA中文乱码编码声明缺失设置resp.encoding utf-8触发验证码请求频率太高停止程序休息 30 分钟以上增大请求间隔翻页到第 100 页之后无数据平台限制最大访问页数接受该限制或通过其他城市的子区域 URL 细分抓取解析得到的列表为空页面结构变化重新用开发者工具检查选择器链家会不定期调整 class 名CSV 用 Excel 打开乱码编码不是utf-8-sig写入文件时指定encodingutf-8-sigKeyError: title某个房源卡片结构异常用select_one 判断空值的方式避免直接报错5.2 我踩过的几个典型坑第一个坑是链家改版导致的选择器失效。某次我的爬虫突然一个数据都解析不出来了排查了半天发现链家把房源卡片的 CSS class 从sellListContent改成了别的名字。这种问题没有根治的办法只能说爬虫本身是个维护型工具页面结构一变爬虫代码就得跟着改。常用的排查手段是在fetch_page后加一行print(resp.text[:500])看看返回的 HTML 长什么样再对着开发者工具调试选择器。第二个坑是houseInfo字段的分割问题。这个字段里面的内容是用竖线|分隔的正常情况下依次是小区、户型、面积、朝向、装修、楼层、年代。但我碰到过有些房源缺少“年代”信息导致info_parts长度为 6直接按索引取第 6 个元素就会IndexError。解决方法是像上面代码那样每次都用if len(info_parts) n做防护或者用try...except包裹。第三个坑是关于翻页限制的应对。链家很多城市只允许访问前 100 页约 3000 条数据。如果你想抓某个城市全量房源可以换个思路链家还有区域筛选、户型筛选、价格筛选这些维度通过组合筛选条件把大集合拆成多个小集合每个小集合的页数都在限制以内就能绕过 100 页的上限。实际做法是构造带筛选参数的 URL比如https://bj.lianjia.com/ershoufang/pg2rs%E6%9C%9B%E4%BA%AC/这里rs参数后面跟的是区域名的 URL 编码望京编码后是%E6%9C%9B%E4%BA%AC。这种方式既不算攻击也在平台允许的范围之内。5.3 如何优雅地处理请求失败网络请求不可能每次都成功所以我建议在爬虫里加一个简单的重试机制。下面这段代码放在fetch_page里请求失败后最多重试 3 次每次重试间隔递增def fetch_page(self, page, retries3): if page 1: url f{self.base_url}/ else: url f{self.base_url}/pg{page}/ for attempt in range(retries): try: resp self.session.get(url, timeout10) resp.raise_for_status() resp.encoding utf-8 return resp.text except requests.RequestException as e: wait 2 ** attempt print(f[错误] 第{page}页第{attempt 1}次请求失败{e}{wait}秒后重试...) time.sleep(wait) print(f[失败] 第{page}页在 {retries} 次重试后仍然失败跳过该页) return None重试的等待时间用指数退避策略第一次失败等 1 秒第二次等 2 秒第三次等 4 秒这样可以避免连续重试把对方服务器打爆。6. 结果分析与后续扩展思路6.1 数据分析示例北京二手房行情初探爬完数据存进 CSV就可以开始做有意思的分析了。我之前用这份数据做过一个简单的北京二手房行情统计代码很短但结论挺直观的import pandas as pd import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 解决中文乱码 df pd.read_csv(data/lianjia_houses.csv) # 清洗 df[面积数值] df[面积].str.extract(r([\d.])).astype(float) df[单价数值] df[单价].str.extract(r([\d.])).astype(float) df[总价数值] df[总价].str.extract(r([\d.])).astype(float) # 按城区分析平均单价 df[城区] df[位置].str.split( ).str[0] city_avg df.groupby(城区)[单价数值].mean().sort_values(ascendingFalse) plt.figure(figsize(12, 6)) city_avg.plot(kindbar) plt.title(各城区平均单价元/平米) plt.xticks(rotation45) plt.tight_layout() plt.savefig(beijing_avg_price.png)从结果能明显看出不同城区之间的价差结构比如核心城区的均价远超外围区域这个信息对了解市场很有帮助。你也可以按总价区间统计房源分布或者算一下“关注人数”和“单价”之间的相关性看看是不是越便宜的房源关注度越高。6.2 从列表页到详情页的进阶方向列表页的数据已经能满足很多分析需求但如果你想知道每套房源更详细的信息比如房屋产权年限、抵押情况、周边配套、历史挂牌记录那就需要进一步爬取房源的详情页。详情页的 URL 规律也很清晰列表页每一个房源卡片的标题链接里就包含了详情页的完整地址比如https://bj.lianjia.com/ershoufang/101123456789.html。你可以在parse_page里把title_elem[href]也抓下来存到 DataFrame 里后面再批量访问详情页。这里有个更稳妥的做法解析到详情页链接后不要立刻请求而是先存起来等列表页全部抓完再用另一个脚本、放慢速度去请求详情页。这样能避免列表和详情请求混在一起导致触发频率检测。详情页的数据结构比列表页复杂涉及更多标签嵌套用soup.select按字段逐个提取就好核心思路和列表页完全一致。6.3 数据可视化与图表展示技巧爬虫和数据可视化是天然的一对数据抓回来不分析就是一堆数字分析之后做成图才真正有价值。我常用的可视化方向有面积-总价散点图看整体市场价位分布面积和总价的关系是否线性户型占比饼图真实反映市场主力户型结构城区均价柱状图横向对比各区域差异单价分布直方图看价位段集中度了解大多数房源分布在什么价格区间关注人数 Top20 排行榜找出热门房源分析它们的共同特征用 matplotlib 或 pyecharts 都能实现。做这些图之前确保数据清洗做好了单位统一了否则出来的图表会带节奏。7. 写在最后的实操心得链家二手房爬虫这个项目是我带过很多朋友入门爬虫的经典案例原因在于它具备了“完整爬虫项目”的所有要素有真实的需求、有明确的反爬挑战、有清晰的数据结构、有可验证的成果。跑通这个项目你对 HTTP 请求、HTML 解析、数据存储、异常处理这四件事就有了整体概念再去看别的爬虫项目基本都能举一反三。有几个我自己的使用习惯分享一下第一爬虫脚本最好用虚拟环境管理依赖别直接往系统 Python 里装一堆包后面项目多了会乱第二跑完的 CSV 数据文件名里带上日期比如lianjia_houses_20250115.csv方便留存和分析因为房产数据跟时间强相关过两周再看同一个小区价格已经变了第三抓取中途程序崩了不要慌设计成“断点续跑”的思路比如通过参数指定从第几页开始抓而不是总从头来。如果你想把链家爬虫项目再往上走一步扩展方向也挺多用threading或asyncio提升并发效率用APScheduler做定时任务每日自动抓取用pymysql把数据存进 MySQL配合 Grafana 做一个房源价格走势看板。这些都是顺手的事关键在于先把最基础的 requests BeautifulSoup 这套搞扎实。最后再叮嘱一句爬虫写得好不如用得稳频率克制数据留念把项目当学习而不是薅羊毛。祝你们都能顺利跑通这个项目拿到自己想要的数据。
返回列表