简介:面向Python爬虫入门者的实战资源,以链家网租房数据为目标,演示从构造请求、解析HTML到清洗存储的完整流程,适合学习Requests、BeautifulSoup以及Pandas数据处理技能的读者。压缩包共8个文件,包含5个Python脚本和3个pyc编译文件,整体仅4KB;脚本覆盖爬虫主程序、异常处理、登录模拟、数据保存等模块,pyc文件则用于快速加载或发布场景,结构紧凑便于逐模块研读。目前已有6663人学习下载,足见该实例的受欢迎程度和参考价值。通过这份资源,可具体看到如何处理分页、设置请求头伪装、利用延时规避反爬,并使用pandas和csv将租金、面积、位置等字段整理为结构化数据;登录模拟和异常处理部分也提供了可复用的抓取思路。资源中的Python脚本各有分工,从发起请求到保存结果形成了清晰模块,便于按需修改,尽管包体小巧,但请求、解析、存储链路完整,非常适合作为爬虫入门练习与二次开发的基底。 每隔一段时间,我都会用python爬取链家网租房数据,给城市租房市场做个快照。这个习惯坚持了很久,起因很朴素——不是想批量抓全站,而是想搞清楚真实挂牌价、户型分布和区域热度,这比听中介口述或者看新闻稿靠谱。如果你正在研究租房市场、写毕业设计,或者单纯想拿爬虫练手,这篇能让你少走不少弯路。我会把完整链路过一遍:页面分析、请求伪装、解析清洗、数据存储、第一轮统计,以及路上踩过的反爬坑。需要先说明,链家的页面结构和风控策略经常调整,下面所有选择器和地址都以你实际抓取时的页面为准,我的代码提供的是思路,不是万年不变的“神器”。
1. 先拆需求:租房数据到底要哪些字段
1.1 为什么不是上来就写代码
很多人拿到标题第一反应是打开编辑器写requests.get,我劝你先花十分钟把字段定下来。抓链家租房,最常用的字段大概是:标题、价格、户型、面积、朝向、所在区域、商圈、小区、楼层、标签(比如近地铁、随时入住)、发布时间、房源详情链接。如果只抓标题和价格,后面做区域热度分析会发现自己少了一堆关键维度,只能重头再爬一遍。这个教训我自己就踩过,第一版脚本只抓了标题、价格、链接,结果想按商圈排序时发现没有区域字段,只能返工。我的建议是,先想清楚拿到数据后用来做什么:如果做租金统计,价格、区域、面积是核心;如果做户型偏好分析,户型字段必须单独拆出来,而不能和描述混在一起。字段清单定下来,代码结构也就清晰了。
1.2 链家租房页面的URL规律
链家租房分城市、分页。以北京为例,租房列表页是https://bj.lianjia.com/zufang/,第二页是https://bj.lianjia.com/zufang/pg2/,以此类推。gz、sh、sz这些城市代码就是拼音首字母,换城市只需要替换前缀。建议你第一站先手动访问列表页,按 Ctrl+U 查看网页源代码,在源码里搜“content__list”或者“租金”这类关键字,确认数据是直接渲染在 HTML 里的,还是通过异步接口加载的。不同时期链家的做法有变化,根据我的观察,租房列表页大体上是服务端渲染,但结构改过好几轮,网上很多老教程里的选择器拿过来直接跑是会扑空的。拿到 HTML 之后先保存到本地或 print 前几百个字符,确认里面有房源标题,再做下一步。这一段分析看起来琐碎,却能帮你省掉后续几十次无效请求。
1.3 用开发者工具确认数据是HTML还是接口返回
另一个判断方法是用浏览器开发者工具。打开 Network 面板,刷新页面,过滤 XHR/Fetch,看有没有返回 JSON 的接口。如果找到返回房源数据的接口,那解析效率会高很多,直接请求接口拿 JSON 即可。如果没找到,就说明列表数据在 HTML 里。这里说个不太顺的体验:有些异步接口在第一次请求时能通,但连续翻几页之后会返回异常,接口可能带签名或者频率限制。遇到“接口被封”的情况,不要死磕,退回去解析 HTML 反而更稳。不少做爬虫的朋友都遇到过类似尴尬,我的经验是永远留一条 HTML 解析的后备路径。另外,链家页面结构偶尔会在某个城市单独改版,北京能用的选择器,换到成都可能就不一样,所以每次换城市都要重新验证一次。
2. 请求环节的细节处理:从Headers到重试
2.1 环境准备与依赖安装
Python 环境建议 3.8 以上,依赖装四个包就够了:requests、beautifulsoup4、lxml、pandas。安装命令是:
pip install requests beautifulsoup4 lxml pandaslxml 是解析器,比 Python 自带的 html.parser 快,解析大页面时能明显感觉到差距。Windows 下 lxml 一般有预编译的 wheel 包,直接 pip 装基本不会出问题;真要碰到编译类报错,换用 Python 官方最新版大概率也能绕过。我一般在项目目录下先建一个虚拟环境,避免把全局环境搞乱。等你装了四五个项目以后就会发现,虚拟环境是最省心的习惯。如果只想快速跑通,不装 pandas 也可以,但后面做分析会麻烦,建议一次装齐。依赖安装这一步本身没有太多花样,真正的功夫在请求头的细节里。
2.2 请求头的伪装与编码处理
requests 默认的 User-Agent 是python-requests/2.x,这种特征太明显,目标站点识别起来毫无压力。至少要换成一个真实浏览器的 UA,最好再把 Referer、Accept-Language 带上,让请求看起来像一个人正常打开页面。参考配置如下:
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", "Referer": "https://bj.lianjia.com/", "Accept-Language": "zh-CN,zh;q=0.9", }有一个容易忽略的点是编码。requests 拿到的响应到底用什么编码,有时候判断不准,我的处理是显式设置resp.encoding = resp.apparent_encoding,中文乱码基本可以解决。链家页面一般是 UTF-8,但碰到过个别情况,别偷懒,统一处理一下。另外,不建议搞一套疯狂随机切换 UA 的逻辑,那反而会触发风控。真实的做法是固定用一个比较新的 Chrome UA,保持低调,别频繁更换。请求头是服务器判断爬虫的第一道关卡,做得到位,后面会省心很多。
2.3 重试与随机延时:慢就是快
反爬的第一道防线就是请求频率。很多新手写完循环不加延时,几十个请求出去,没多久就被限制了。我习惯写一个带重试的请求函数,加上随机延时,代码大致是这样:
import time import random import requests def fetch(url, headers, retries=3): for i in range(retries): try: resp = requests.get(url, headers=headers, timeout=10) if resp.status_code == 200: resp.encoding = resp.apparent_encoding return resp except Exception as exc: print(f"第{i+1}次请求失败: {exc}") time.sleep(2 + random.random()) return None两个页面之间再加一句time.sleep(random.uniform(1, 3)),这个随机延时是防反爬最基础也最有效的动作。别觉得这样慢,爬租房数据不是抢竞拍,慢一点换来稳定。我自己的标准是单页请求间隔不低于 1 秒;如果是连续跑几百页,间隔还会放大到 2-4 秒。这里再提醒一句:一旦发现解析结果为空,先打印原始 HTML 片段,不要猜。我见过太多人对着一个被反爬拦下的空页面改了半天解析逻辑,最后才意识到问题根本不在代码,而在请求频率上。
3. 解析与清洗:把页面文本变成结构化表格
3.1 BeautifulSoup解析列表项
列表页每页大约 30 条房源,整个页面解析其实不复杂。先创建一个解析单条的函数,传入一条房源节点的 BeautifulSoup 对象,返回一个 dict:
from bs4 import BeautifulSoup import re def parse_item(item): title_tag = item.select_one("div[class*='content__list--item--title'] a") desc_tag = item.select_one("p[class*='content__list--item--des']") price_tag = item.select_one("span[class*='content__list--item-price']") if not title_tag or not price_tag: return None return { "title": title_tag.get_text(strip=True), "link": title_tag.get("href", ""), "desc": desc_tag.get_text(" ", strip=True) if desc_tag else "", "price_text": price_tag.get_text(strip=True), }注意选择器里的*=表示模糊匹配,这样即使 class 带了一串动态后缀也能命中。链家页面改版过几次,最稳妥的方式是抓一次页面后,用浏览器的 Copy selector 生成目标元素的选择器,再拿去程序里验证。写代码时不要一次性把全部字段都写完,先只解析标题和价格,打印前 3 条,确认无误后再补其他字段。这样调试成本最低。网上的老教程选择器经常失效,不是代码写错了,是页面变了,判断这个问题的唯一办法就是打印真实 HTML 去对。
3.2 字段清洗的几个细节
HTML 里拿到的文本通常会带空格、换行、单位,必须做清洗。价格可能是“5600元/月”,面积是“45.32㎡”,户型是“2室1厅”,区域是“朝阳-望京-融科橄榄城”。我建议统一用正则提取核心数字,再做类型转换:
price_match = re.search(r"\d+", item.get("price_text", "")) price = float(price_match.group()) if price_match else None area_match = re.search(r"[\d.]+", item.get("area_text", "")) area = float(area_match.group()) if area_match else None然后把区域字段按“-”拆开,分别存到 district、bizcircle、community 三列。标签字段可能有多个,比如“近地铁”“随时看房”,我习惯用竖线连接。这里有个坑:文本里可能混着“整租”“押一付三”这些说明,不要统统塞进面积字段,先打印一段原始文本看清楚再写正则。字段清洗是爬虫里最费时间的环节,但做好之后,后续分析会顺滑很多。我第一次跑的时候没仔细看,把“2室1厅”里的数字当成了面积,统计出来的数据惨不忍赌,后来加了一堆校验才恢复正常。
3.3 多页翻页逻辑
翻页逻辑也很直接:从第一页开始,把 URL 里的 pg1 换成 pgN。页面上通常有翻页控件,部分版本的 HTML 里带了 JSON 格式的 page-data 属性,里面有 totalPage 字段,可以优先读取。拿不到就循环抓,直到解析出来的房源列表为空为止。建议加一个最大页数限制,比如最多 100 页,避免程序失控。每爬完一页打印一条日志,记录页码、条数和这一页第一条的价格,这样中途出问题也能快速定位。我在实跑中发现,链家有时候会返回“已为您暂时跳过部分房源”的提示,说明列表被截断,这时候可以按区域拆分后再爬,而不是死磕一个 URL。比如按朝阳、海淀、丰台分别抓一遍,既能绕过列表截断,又能让数据更稳定。
4. 数据落地与快速验证:CSV、SQLite和第一轮统计
4.1 CSV直接保存
数据量不大时,CSV 是最方便的交付格式。把所有解析结果收集成 list of dict,用 pandas 写出去:
import pandas as pd df = pd.DataFrame(all_items) df.to_csv("lianjia_zufang.csv", index=False, encoding="utf-8-sig")用utf-8-sig编码是给 Excel 看的,否则直接用 Excel 打开 CSV 中文容易乱码。这个细节我每次都会讲,因为它能在你朋友面前省掉很多尴尬。缺点也明显:重复运行会覆盖,如果想追加,需要改成mode='a',但 CSV 在追加时不能方便去重。所以如果只是想临时跑一两次,CSV 够用;如果打算长期采集,我更推荐 SQLite。CSV 还有一个问题:字段内容里如果包含逗号或换行,容易让表格错位,所以前面清洗时我会把标签字段里的逗号替换成竖线,这个习惯帮我避过很多莫名其妙的 file parsing 报错。
4.2 SQLite便于去重和增量
SQLite 是 Python 自带的数据库,不需要额外安装服务,很适合这种小规模爬虫。建表时给房源链接加 UNIQUE 约束,插入时用INSERT OR IGNORE,天然去重:
import sqlite3 conn = sqlite3.connect("lianjia.db") conn.execute(""" CREATE TABLE IF NOT EXISTS house ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, district TEXT, bizcircle TEXT, layout TEXT, area REAL, price REAL, link TEXT UNIQUE ) """) rows = [ (h["title"], h["district"], h["bizcircle"], h["layout"], h["area"], h["price"], h["link"]) for h in all_items ] conn.executemany( "INSERT OR IGNORE INTO house (title,district,bizcircle,layout,area,price,link) VALUES (?,?,?,?,?,?,?)", rows, ) conn.commit() conn.close()这样重复爬同一条房源时,UNIQUE 约束会阻止重复插入。我个人的习惯是 CSV 做最终交付,SQLite 做过程存储。给非技术背景的朋友看数据,直接丢 Excel 文件;自己后续写分析脚本,则从 SQLite 读数据。两不耽误。增量爬取的关键就是这个 UNIQUE 约束,没有它,第二次跑的时候数据会翻倍,最后做统计全是错的。
4.3 落库后的快速验证:先看一眼区域统计
数据落库之后,别急着去画图,先用一两行 pandas 代码验证一下数据合理性。把价格、面积转成数值类型,然后按区域统计:
df = pd.read_csv("lianjia_zufang.csv") df["price"] = pd.to_numeric(df["price"], errors="coerce") df["area"] = pd.to_numeric(df["area"], errors="coerce") df.dropna(subset=["price", "area"], inplace=True) result = df.groupby("district")["price"].agg(["count", "median", "mean"]) print(result.sort_values("count", ascending=False))这一步能立刻发现解析函数有没有问题。如果某个区域房源数异常少,或者价格中位数是 0,多半是清洗逻辑有遗漏,回头去改。我每次跑完都会先看这个统计,确认数据合理再继续做更复杂的分析。用 pandas 的to_numeric配合errors='coerce',遇到转换不了的值会变成 NaN,非常省心。从爬数据到验证数据,这才是完整的闭环。
5. 链家反爬实测:空页面、验证码与降级方案
5.1 最常见的三种反爬表现
这个章节下面的经验,全是我实际跑出来的,不是道听途说。最常见的表现有三种。第一种是 HTTP 状态码 200,但页面列表区域是空的,或者房源数量为 0,这种最阴,因为状态码完全正常,代码也不报错。第二种是页面直接弹出访问过于频繁的提示,要求拖动拼图验证。第三种是返回 403 或 521 这类异常状态码,一看就知道被拒了。我之前犯过一个错:用并发池同时发几十个请求,跑了几分钟,所有页面突然全是空的,退回去看请求日志,发现服务器返回了“当前访问人数过多,请稍后再试”。当时没有打印响应内容,对着空 HTML 瞎折腾了很久,浪费了大半天。所以遇到空页面第一件事是打印响应文本,而不是改选择器。
5.2 降级处理策略
遇到反爬,最正确的做法是停手,而不是硬刚。验证码这一层我不建议去搞什么自动识别,既不稳定,也不符合规矩。我的降级策略很朴素:先停止脚本,休息 20-30 分钟,再把并发改成串行,加长延时到 3-5 秒,最后把任务拆小。比如原来一次性爬整个城市的租房列表,改成按行政区分批跑,每批只跑 100-200 条,跑完休息几分钟再跑下一批。还可以给脚本加一个总时长上限,比如最多运行 20 分钟,到点自动退出,避免你睡觉时脚本在那儿一顿猛跑,第二天起来发现 IP 被限制得更狠。另一个小技巧是设置合理的运行时段,比如工作日的晚上或者周末的白天,更像真实用户的行为,也不至于给服务器造成压力。
5.3 合规边界问题
虽然标题是爬取,但我还是要说几句守规矩的话。数据只用于个人学习研究和非商业用途,这是底线。请求频率一定要控制,别给目标服务器造成压力。如果网站的服务条款或 robots.txt 明确表示不允许抓取这类数据,那就停手,改从公开数据集或官方渠道获取。爬虫这个领域技术本身是中性的,但用在什么场景、怎么用,决定了它安不安全。我自己写这类脚本只会做有限量级的数据采集,拿到结果做统计分析就停,不会把数据再打包出售,也不会做商业推荐之类的衍生用途。这种边界感,比任何技术技巧都重要。尤其是租房数据这种涉及大量个人生活信息的列表,哪怕字段是公开展示的,也要谨慎使用。
最后分享两个小习惯。第一,每次跑完把 CSV 按日期归档,类似zufang_20250101.csv,连续积累几周,就能对比同一商圈的租金变化,甚至能发现某些小区的挂牌价波动;第二,把脚本参数化,城市、最大页数、延时范围都用命令行参数传,换个城市不用改代码。爬虫写到能稳定出数据只是开始,能坚持积累数据并从中看出规律,才算真正玩明白了。
本文还有配套的精品资源,点击获取