ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

链家二手房爬虫实战:requests+BeautifulSoup高效采集房源数据

链家二手房爬虫实战:requests+BeautifulSoup高效采集房源数据 1. 项目概述与爬虫设计思路做爬虫这行有个朴素的真理真正难的不是写代码而是确定“要什么”和“怎么要”。链家二手房这个项目算是我接触过的所有爬虫练习里性价比最高的一个——页面结构规整、字段丰富、数据量充足而且自带反爬机制非常适合拿来练手。我当初做这个项目的原因很简单帮一个做房产研究的朋友整理某个城市的二手房挂牌数据需要连续抓取几个月的小区均价变动。先说结论链家二手房信息爬取的核心价值在于它能帮你拿到结构化的真实房源数据包括小区名称、户型面积、朝向装修、楼层年代、总价单价等十几个字段。这些数据不管是做市场分析、价格监控还是写量化策略的输入特征都非常有用。整个项目用到的技术栈也很干净Python 3 requests BeautifulSoup csv不需要 Selenium 这类重型工具就能跑通。适合谁来参考如果你正在学爬虫想要一个“能完整跑通、有真实数据落地”的项目或者你是做数据分析的需要一份可靠的房价数据源又或者你只是想在 GitHub 上找一个能改改就能用的代码骨架这个项目都很合适。我会把完整代码、踩过的坑、以及背后“为什么要这么写”的逻辑全部讲清楚。1.1 核心需求解析在动笔写代码之前我先列了一张需求清单。这是整个项目中最重要的环节——很多人一上来就写 requests.get结果爬到一半发现要么被反爬拦截要么解析出来的字段对不上其实就是需求梳理没做透。我的核心需求拆解下来是这样的数据源链家二手房在售房源列表页city.lianjia.com/ershoufang/pg{n}/目标字段小区名称、所在区域、户型、面积、朝向、装修情况、楼层、建筑年代、总价、单价共 10 个关键字段另外顺手把房源标题也抓下来做文本分析用翻页逻辑链家二手房每页 30 套房源单城市总页数在几十到上百页之间需要遍历全部页码数据存储本地 CSV 文件方便直接用 Excel 或 pandas 做后续分析频率控制每请求一页后随机休眠 1-3 秒避免给目标服务器造成压力这个需求清单看似简单但每一个点背后都有对应的技术决策。比如为什么选 BeautifulSoup 而不是正则表达式因为链家的 HTML 层级结构比较规整用 CSS 选择器提取比写十几个正则要可维护得多。为什么用 requests 而不是 Scrapy因为 Scrapy 的爬虫框架对新手来说有点重这个项目用 requests 加循环就完全够用代码量短一半逻辑也更直观。1.2 链家页面的数据特征我花了一点时间人工浏览了链家的二手房页面这里分享几个对写代码有直接影响的观察结果。第一个特征是列表页的信息密度很高。链家的列表页每套房源都展示在一个 class 为“info”的 div 里里面有标题、位置信息、房屋信息、关注人数、发布时间、总价、单价。其中“房屋信息”是一整段文本包含户型、面积、朝向、装修、楼层、年代用换行符和竖线分隔——这一步给解析提供了很大的便利但也埋了一个坑不同房源的字段顺序并非完全一致单纯按位置索引取值会出问题。第二个特征是分页结构很清楚。链家网页底部有一个分页条每一页用 href 方式跳转URL 里带 pg2、pg3 这样的页码参数。有个隐藏规则需要注意链家的页面上会显示总共有多少套房源我实测下来每页 30 条总页数等于总房源数除以 30 向上取整。但链家对最大翻页深度有限制超过某个页码后继续翻页会拿到重复数据或直接返回空内容。第三个特征是反爬机制虽然存在但不算极端。链家主要通过 User-Agent 检测和访问频率控制来识别爬虫。如果你用默认的 Python-requests UA大概率第一次请求就会撞上验证码页面。但只要设置了正常的浏览器 UA再加上每次请求之间的随机延迟基本可以稳定爬取。另外IP 封禁也是存在的不过链家的封禁策略相对宽松我之前用单个 IP 连续爬取几百页也没有被永久封禁最多是中途出现几次验证码过几分钟自己就恢复了。2. 技术选型与核心实现方案这个项目的技术选型是我反复权衡过的这里详细讲讲每个领域的决策逻辑方便你在类似项目里直接套用。2.1 为什么用 requests BeautifulSoup 组合现代爬虫圈子里工具的选择多到让人眼花缭乱requests、httpx、aiohttp、Scrapy、Selenium、Playwright、Pyppeteer……每一个都有自己的适用场景但不代表每个项目都要上最复杂的工具。我选择 requests BeautifulSoup 的原因有三层第一层目标网页是服务端渲染。链家的二手房列表页是典型的服务端渲染页面所有房源数据都直接写在 HTML 源码里。这意味着不需要浏览器执行 JavaScript不需要 Selenium也不需要渲染引擎。既然 requests 就能把完整 HTML 拿回来为什么要用重量级工具增加稳定性和性能负担有个很简单的判断标准打开网页后右键“查看页面源代码”如果能在源码里搜到你要的数据那用 requests 就够了如果搜不到才考虑后续的渲染方案。第二层解析复杂度适中。BeautifulSoup 的 CSS 选择器对链家这种层级清晰的页面来说写起来非常顺手。举个例子要提取每套房的标题只需一行代码soup.select(div.info a.title)。如果是正则你可能得写一长串带各种分组和转义的模式还要考虑换行符和大小的差异维护成本高得多。第三层学习曲线和排错成本都很低。requests 的 API 就那十几个方法BeautifulSoup 的核心概念就四个Tag、NavigableString、BeautifulSoup、Comment。出问题时网上能搜到的资料最多。Scrapy 虽然功能强大但对于这种单站单页面的小项目它的中间件、管道、Item 定义等概念会变成不必要的负担。2.2 基于规则解析与字段提取策略爬虫解析页面有两种大方向一种是把页面当作文本来处理用正则表达式或者字符串操作抽取信息另一种是把页面当作树形结构用 DOM 解析库如 BeautifulSoup、lxml来定位节点。这个项目我选择了后者的“CSS 选择器 结构化提取”策略。我的具体做法是先把每个房源卡片看作一个独立的 div 节点然后在每个节点内部按 class 名称提取子节点。这种做法的好处在于即使某个字段在个别的房源上缺失也不会影响其他字段的解析——因为节点定位是相对独立的。这一点比“用列表去按位置索引字段”要稳健得多。字段提取还有一个关键细节原始数据清洗。链家房源信息里有很多零碎文本例如“3室1厅·96.7平米·南 北·简装·低楼层(共6层)·2003年建”。直接把这个字符串存进 CSV 也能用但如果要做数据分析最好还是拆分成单独的结构化字段。我在代码里用了 split() 和 strip() 的组合再配合几个条件判断来处理不同字段的边界。这部分的完整实现代码下面第三章会单独展示。2.3 常见架构误区避免把全部代码塞进一个函数我在看别人写的爬虫代码时最常见的问题就是把所有逻辑都堆在 main 函数里发请求、解析、翻页、存数据全在一起代码是能跑但改起来要命。这个项目我希望做一个稍微工程化一点的示范所以按职责拆分了四个函数fetch_listing_page(url)负责请求列表页并返回 BeautifulSoup 对象parse_listing_card(card)负责从单个房源卡片中提取结构化字段parse_page(soup)负责遍历一整页的所有房源卡片返回列表save_records(records)负责把数据写入 CSV这种拆分的好处是显而易见的。每个函数只需关注一件事出了问题直接定位到具体函数。比如拿到的一页数据里全是空的那就先看 parse_listing_card 里的选择器是否匹配如果请求被拦截那就检查 fetch_listing_page 的请求头。四个函数加一个主循环整个代码 150 行以内就能写完还有一个额外的好处就是后续如果要增加新字段只需要改 parse_listing_card 内部的几行不需要动其他函数。3. 完整代码实现与逐段解析现在进入正题我把完整代码贴出来然后逐段解析里面的关键逻辑。代码结构遵循上面提到的四个函数加一个主循环全部用 Python 标准库加第三方库实现依赖项只有 requests 和 beautifulsoup4。3.1 环境准备与依赖安装在写任何代码之前先确保你的 Python 环境是干净的。我推荐用 Python 3.8 以上的版本因为 f-string 和类型注解在这之后的版本里才比较好用。创建虚拟环境不是必须的但在做爬虫项目时我强烈建议用因为爬虫经常会装 lxml、pandas 这类重依赖不隔离环境容易把系统 Python 弄乱。安装依赖只需要两行命令pip install requests beautifulsoup4 pip install lxml # 可选BeautifulSoup 使用 lxml 作为解析器时速度更快这里有个小建议requests 和 beautifulsoup4 都有纯 Python 的依赖urllib3、soupsieve 等安装速度很快。lxml 是一个 C 扩展库编译安装有时会遇到坑在 Windows 上推荐直接从 pip 下载预编译的 wheel在 macOS 上用 Homebrew 的 Python 通常也没问题。3.2 核心代码requests 请求模块import random import time import csv 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/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://www.lianjia.com/ }这一段定义了一个请求头字典。这里的核心是 User-Agent 和 Referer。UA 直接决定了服务器认为你是一个“真人浏览器”还是“Python 脚本”。Chrome 120 这个版本信息是我在写代码那段时间实测能生效的版本UA 的具体版本号其实没那么重要关键是格式要符合浏览器的特征。如果哪天链家升级了 UA 检测策略换一个最新的 Chrome UA 就行。Referer 也很关键。链家有些页面会校验来源直接从别的域名跳转过来的请求会算作盗链。我虽然不确认链家是否启用了 Referer 校验但从经验出发带上 Referer 总比不带好。在爬虫领域低成本地模仿真人的行为细节能规避掉很大一部分反爬策略。def fetch_listing_page(url, retries3): for attempt in range(retries): try: resp requests.get(url, headersHEADERS, timeout15) if resp.status_code 200: resp.encoding utf-8 return BeautifulSoup(resp.text, lxml) else: print(f请求失败状态码{resp.status_code}) except requests.RequestException as e: print(f请求异常第 {attempt1} 次尝试{e}) time.sleep(2 random.random() * 3) return Nonefetch_listing_page 这个函数包含了三个工程化细节值得单独说明。第一是 retries 重试机制。网络请求的失败率远比你想象得高超时、连接被重置、临时断网都可能导致请求失败。我设置 3 次重试并且每次重试之间等待 2 到 5 秒的随机时长这能有效避免因为瞬时网络抖动导致整个任务崩溃。第二是 timeout15。requests 库如果不在请求时指定 timeout默认是永不超时。这意味着如果链家服务器接收了连接但没有返回数据你的脚本会一直挂在那里既不报错也不往下跑。设一个 15 秒的超时时间请求超过 15 秒就主动放弃然后走重试逻辑。第三是 resp.encoding utf-8。链家页面的 charset 是 utf-8但有时候 requests 会从响应头里没有获取到编码信息默认使用 ISO-8859-1 来解码导致中文变成一坨乱码。强制指定 utf-8 可以稳妥地避免这个问题。3.3 核心代码HTML 解析与字段提取模块def parse_listing_card(card): title_tag card.select_one(div.title a) position_tag card.select_one(div.positionInfo a) house_info_tag card.select_one(div.houseInfo) total_price_tag card.select_one(div.totalPrice span) unit_price_tag card.select_one(div.unitPrice span) follow_info_tag card.select_one(div.followInfo) title title_tag.get_text(stripTrue) if title_tag else position position_tag.get_text(stripTrue) if position_tag else follow_info follow_info_tag.get_text(stripTrue) if follow_info_tag else total_price total_price_tag.get_text(stripTrue) if total_price_tag else unit_price unit_price_tag.get_text(stripTrue) if unit_price_tag else # 房屋信息3室1厅·96.7平米·南 北·简装·低楼层(共6层)·2003年建 house_text house_info_tag.get_text( , stripTrue) if house_info_tag else parts [p.strip() for p in house_text.split(·)] return { title: title, position: position, house_type: parts[0] if len(parts) 0 else , # 户型 area: parts[1] if len(parts) 1 else , # 面积 orientation: parts[2] if len(parts) 2 else , # 朝向 decoration: parts[3] if len(parts) 3 else , # 装修 floor: parts[4] if len(parts) 4 else , # 楼层 year: parts[5] if len(parts) 5 else , # 年代 total_price: total_price, unit_price: unit_price, follow_info: follow_info, }这一部分是整个爬虫的核心逻辑。第一步是初始化 CSS 选择器定位各个子元素。链家页面上每个房源卡片是一个 div默认有 class 为“info”卡片内各字段的 class 名称我在注释里标清楚了。这里面最容易搞混的就是 totalPrice 和 unitPrice前者是“总价 620 万”后者是“单价 58000 元/平米”都是一样的 class 前缀但不同层级用 select_one(div.totalPrice span) 和 select_one(div.unitPrice span) 正好分别定位。第二步是拆解房屋信息。链家的 houseInfo 是一整段文本格式很规整但不固定。我实测中发现大多数房源是 6 个字段但偶尔有 5 个的没有装修或没有年代的。直接用 split(·) 分割然后按索引取值有可能会出现错位。所以我在这里做了一个容错用列表 parts 保存分割结果然后靠索引去取但默认值设为空字符串确保即使 6 个字段不全程序不会报 IndexError。这部分可以做得更细例如对“高楼层/中楼层/低楼层”做归一化这里先留一个可扩展的空间。第三步是 get_text 的参数。BeautifulSoup 的 get_text() 默认会返回所有子节点的文本但如果标签内部有换行返回的字符串可能带很多空白字符。传一个分隔符参数 可以统一用空格连接多个文本节点然后 strip 掉首尾空白解析出的内容就干净很多。def parse_page(soup): cards soup.select(div.info) records [] for card in cards: try: record parse_listing_card(card) records.append(record) except Exception as e: print(f解析房源卡片失败{e}) continue return recordsparse_page 这个函数很简单就是遍历一页里所有 class 为 info 的 div对每个卡片调用解析函数。我用 try-except 把单个卡片的异常拦下来并 continue这样即使某个卡片因为特殊格式解析出错也不会中断整页的处理。3.4 核心代码CSV 存储模块与主循环def save_records(records, filenamelianjia_ershoufang.csv): if not records: return fieldnames [title, position, house_type, area, orientation, decoration, floor, year, total_price, unit_price, follow_info] write_header not os.path.exists(filename) with open(filename, a, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) if write_header: writer.writeheader() writer.writerows(records)存储这一层我选择了 append 模式而不是一次性覆盖写入。原因很实际爬虫任务跑几十甚至上百页如果中途挂了已经抓到的数据不应该丢失。只需要给 CSV 文件追加内容下次再运行时数据就会接在后面。如果是第一次运行文件不存在就先用 os.path.exists 判断一下再写入表头。编码这里有个细节UTF-8 编码写入后再用 Excel 打开会乱码因为 Excel 需要 BOM 头。Python 里把编码设置为 utf-8-sig写入的 CSV 就能直接在 Excel 里正常显示中文。这是很多人踩过坑之后才知道的细节。def main(): csv_filename lianjia_ershoufang.csv base_url https://bj.lianjia.com/ershoufang/pg{page}/ start_page 1 end_page 50 for page in range(start_page, end_page 1): url base_url.format(pagepage) print(f正在抓取第 {page} 页...) soup fetch_listing_page(url) if soup is None: print(f第 {page} 页抓取失败跳过) continue records parse_page(soup) if not records: print(f第 {page} 页未解析到任何房源可能是反爬拦截或页面结构变化) continue save_records(records, filenamecsv_filename) print(f第 {page} 页解析完成共 {len(records)} 条记录) time.sleep(random.uniform(1, 3)) print(全部抓取完成) if __name__ __main__: main()主循环的代码非常直白构造 URL、抓取页面、解析数据、保存数据、随机休眠。这里我设置了 1 到 50 页对应大约 1500 套房源作为演示足够了。如果你想爬全量数据可以先把首页解析一下从页脚拿到总页数再动态调整 end_page。休眠这一步很重要。random.uniform(1, 3) 的意思是每次请求之间等待 1 秒到 3 秒之间的随机时长。这个随机区间考虑了防盗和任务耗时之间的平衡——间隔太短容易被封间隔太长整个任务要跑很久。对于中小规模的采集来说1 到 3 秒是比较合理的区间。3.5 完整代码汇总为了方便大家直接抄作业我把上面的所有函数拼接成一个完整的脚本。具体的各函数注释和说明已经分散在前面这里就直接把全量代码贴出来import os import csv import time import random 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/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://www.lianjia.com/ } def fetch_listing_page(url, retries3): for attempt in range(retries): try: resp requests.get(url, headersHEADERS, timeout15) if resp.status_code 200: resp.encoding utf-8 return BeautifulSoup(resp.text, lxml) else: print(f请求失败状态码{resp.status_code}) except requests.RequestException as e: print(f请求异常第 {attempt1} 次尝试{e}) time.sleep(2 random.random() * 3) return None def parse_listing_card(card): title_tag card.select_one(div.title a) position_tag card.select_one(div.positionInfo a) house_info_tag card.select_one(div.houseInfo) total_price_tag card.select_one(div.totalPrice span) unit_price_tag card.select_one(div.unitPrice span) follow_info_tag card.select_one(div.followInfo) title title_tag.get_text(stripTrue) if title_tag else position position_tag.get_text(stripTrue) if position_tag else follow_info follow_info_tag.get_text(stripTrue) if follow_info_tag else total_price total_price_tag.get_text(stripTrue) if total_price_tag else unit_price unit_price_tag.get_text(stripTrue) if unit_price_tag else house_text house_info_tag.get_text( , stripTrue) if house_info_tag else parts [p.strip() for p in house_text.split(·)] return { title: title, position: position, house_type: parts[0] if len(parts) 0 else , area: parts[1] if len(parts) 1 else , orientation: parts[2] if len(parts) 2 else , decoration: parts[3] if len(parts) 3 else , floor: parts[4] if len(parts) 4 else , year: parts[5] if len(parts) 5 else , total_price: total_price, unit_price: unit_price, follow_info: follow_info, } def parse_page(soup): cards soup.select(div.info) records [] for card in cards: try: record parse_listing_card(card) records.append(record) except Exception as e: print(f解析房源卡片失败{e}) continue return records def save_records(records, filenamelianjia_ershoufang.csv): if not records: return fieldnames [title, position, house_type, area, orientation, decoration, floor, year, total_price, unit_price, follow_info] write_header not os.path.exists(filename) with open(filename, a, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) if write_header: writer.writeheader() writer.writerows(records) def main(): csv_filename lianjia_ershoufang.csv base_url https://bj.lianjia.com/ershoufang/pg{page}/ start_page 1 end_page 50 for page in range(start_page, end_page 1): url base_url.format(pagepage) print(f正在抓取第 {page} 页...) soup fetch_listing_page(url) if soup is None: print(f第 {page} 页抓取失败跳过) continue records parse_page(soup) if not records: print(f第 {page} 页未解析到任何房源可能是反爬拦截或页面结构变化) continue save_records(records, filenamecsv_filename) print(f第 {page} 页解析完成共 {len(records)} 条记录) time.sleep(random.uniform(1, 3)) print(全部抓取完成) if __name__ __main__: main()到这里一个完整可运行的链家二手房爬虫已经完成了。接下来我重点讲一下我在这个项目里遇到的实际问题和排查过程。4. 常见问题与排查技巧实录任何爬虫项目跑起来和跑好是两个层次的事。这里我把自己在实际运行中遇到的高频问题、排查路径、以及最终解决方案整理成速查表方便你快速对照解决。4.1 问题速查表现象可能原因解决方案返回状态码 302需要登录或访问被重定向检查是否被跳转到验证码页加入 Referer 和更完整的请求头页面返回但无数据页面结构变动或列表选择器失效手动打开页面审查元素用浏览器开发者工具重新定位 class 名中文乱码默认编码判断错误强制设置 resp.encoding utf-8部分字段为空该房源缺失对应信息用三目运算保证每个字段至少返回空字符串不要抛异常请求异常超时目标临时限流或网络问题加入重试机制和随机延迟避免高频请求被封 IP请求频率太高延长休眠时间或使用代理池注意合规CSV 打开后中文乱码编码没有 BOM 头写入时使用 utf-8-sig爬了几页后全是重复数据触发了分页限制检查页面是否返回了同一页内容增加请求头 Cookie 字段这 8 个问题基本覆盖了我个人在爬取链家时遇到的 90% 的异常场景。下面挑几个典型的详细说明排查过程。4.2 验证码与访问拦截的应对策略第一次跑脚本时我碰到最多的就是验证码问题。具体表现是前几页数据正常到后面突然某一页返回的不是房源列表而是一个验证码页面。BeautifulSoup 在解析验证码页面时自然找不到任何 div.info于是该页返回空列表。排查思路是这样的先在浏览器里手动打开对应页码看看网页是否正常展示。如果浏览器正常而 requests 不行说明不是页面本身的问题而是请求被识别了。然后检查请求头对比浏览器请求和爬虫请求的差异。我最终发现UA 和 Referer 都带上之后验证码出现的频率从“每 5 页一次”降到“每 100 页不到一次”。另一个实用技巧是处理已经出现的验证码。如果某次请求返回了验证码页面可以打印出页面标题或者 URL让脚本识别到异常状态然后暂停更长时间比如 60 秒再重试。代码里加一段判断if 验证码 in resp.text or resp.url.startswith(https://captcha): print(触发验证码等待 60 秒后重试...) time.sleep(60) continue这个策略很朴素但很有效。验证码拦截本质上是频率问题主动降速是可以解决的。但要注意降速是暂时的如果长时间不停地爬IP 还是可能被临时封禁。真被封了也没事停个十几分钟再继续链家的封禁时长通常是可控的。4.3 页面结构变化带来的解析失败爬虫最怕的不是被反爬而是网站改版。一旦目标页面的 HTML 结构发生变化之前写的 CSS 选择器全部失效脚本就会返回空数据。我遇到过两次这样的情况每次的排查路径都类似。第一步用浏览器打开链家二手房页面按 F12 打开开发者工具选中任意一个房源卡片查看它现在用的 class 是什么。第二步把新的 class 名替换到代码里的选择器中。第三步手动解析一页看看输出是否正常。这种改版通常是小规模的比如把 div.info 改成了 div.infoWrapper或者把某个价格从 span 换成了 div。真正大规模改版很少见所以只要定期关注爬虫日志发现连续多页空数据时立刻检查页面结构基本都能快速修复。4.4 关于动态加载页面的一些思考这里有一个需要澄清的点。有些读者可能会问链家二手房是不是动态加载的为什么我用 requests 拿到的是空页面我的实测结果是链家二手房列表页是服务端渲染即使关闭浏览器 JavaScript页面依然能展示完整的房源信息。所以 requests 可以直接拿到数据。但如果你用的是链家的“地图找房”或者其他交互式页面那就是另一回事了——那些页面依赖 JavaScript 动态渲染requests 拿到的 HTML 里确实没有数据。针对动态渲染页面通常的解决方案有几种一是分析浏览器发出的 XHR/AJAX 请求找到真实的数据接口直接用 requests 去请求接口这是最高效的方式二是使用 Selenium 或 Playwright 这类无头浏览器直接渲染出完整页面再解析三是使用 pyppeteer 这类异步化的浏览器工具。但这些都已经超出了本项目的范围属于进阶话题以后可以单独写。5. 合规、道德与风险控制每次写爬虫相关的内容我都必须专门拿出一章来聊合规和道德问题。这不是套话而是因为爬虫天然游走在灰色地带规则不明确边界不清晰。5.1 robots.txt 与数据使用边界先看 robots 协议。链家网站的 robots.txt 明确写有对爬虫的访问限制比如禁止爬取部分路径。虽然 robots.txt 在法律上没有强制约束力但作为从业者我认为尊重站点的爬虫策略是基本功。本项目仅做个人学习与技术演示用途爬取数据量控制在很小的规模且不对目标站点造成明显压力这是我给自己设定的红线。数据使用边界更重要。爬下来的链家房源信息包含小区名称、户型、价格、经纪人联系方式等这些数据涉及企业数据权益和个人信息保护。用于个人学习分析是一回事用于商业变现是另一回事后者的法律风险要高得多。我在实际项目中只把数据用于内部市场趋势分析数据也基本是匿名的都不涉及任何个人信息。5.2 控制频率与负责任爬取我在整个代码里最看重的就是请求频率的控制。很多爬虫出问题的根源并不是代码写得不好而是频率太高。你想想链家的服务器每天要服务多少真实用户你每秒发 10 个请求和正常用户的使用模式完全不符被拦截是必然的。负责任爬取的具体实践包括单线程访问不搞并发协程这个项目本身就不需要每次请求后随机休眠 1 到 3 秒单次运行不超过几百页不在高峰期如节假日促销活动时爬取抓到数据就停不反复抓同一页这套行为准则比我写过的任何一个爬虫代码都重要。它决定了你的 IP 会不会被封也决定了目标网站是否愿意继续开放数据给所有人。做技术的人应该有这个自觉。5.3 后续可以怎么扩展这个项目做完之后我根据自己的实际需求做了两个方向的扩展可以给大家参考。第一个方向是定时抓取加增量更新。把主循环包在一个 while True 里面每天凌晨运行一次只抓取前一天新上架的房源然后合并到历史数据里。这样就能构建一份时间序列数据用于分析某个小区的挂牌价走势。这个功能的关键在于去重链家同一套房源的链接是稳定的可以以链接指纹为 key 去重。第二个方向是加上数据可视化。把 CSV 读进 pandas按区域做均价排行画出箱线图或者热力图能直观看出哪个区域的价格波动大、哪个小区的性价比高。如果你在做房产相关的研究这些图表会比单个表格有价值得多。我这里也想过要不要再加一套代理池但仔细想过之后决定不加。原因有两个一是链家对这个项目的访问压力不大单 IP 加上频率控制完全够用二是免费代理池的质量参差不齐很多代理 IP 本身就被目标站点标记了用了反而更慢还可能带来数据污染。真要稳定使用付费代理又是一笔成本对学习项目来说不划算。6. 写在最后的一点体会这个项目看似简单但对我来说每一次重写都有新收获。最早我写的爬虫版本把所有逻辑都堆在 main 函数里请求失败不会重试编码不对就乱码字段不对就报错。后来慢慢地学会了重试机制、异常处理、数据清洗、CSV 编码这些细节才发现写爬虫其实是在写工程不是在写一次性脚本。有一个经验想特别分享判断一个爬虫写得好不好不是看它能不能把数据抓下来而是看它在各种意外情况下能不能自己恢复。链家这个项目里如果没有 retries、没有字段容错、没有随机休眠一百页跑下来大概率会在中途挂掉。加了这些机制之后我跑完整套流程几乎没有人为干预。如果你正准备动手跑这个项目我给你三个建议第一先把单页解析做到完美再批量跑不要急着循环一百页发现全是空数据第二第一次运行时把 end_page 设小一点比如 5 到 10 页确认 CSV 文件内容正常再放大量第三数据抓下来花点时间看看里面有很多有趣的细节比如同一个小区的单价差异、不同朝向的均价差这些比代码本身更能让你理解数据的价值。技术是工具数据是资源怎么用好它们才是我们真正要思考的问题。
返回列表