资讯动态

链家二手房爬虫实战:从页面解析到数据清洗的完整路径

发布时间:2026/9/9 4:26:24 来源:尧图企业网站定制
做爬虫这行久了你会发现一个挺有意思的现象很多初学者一上来就盯着反爬、验证码、IP池这些“硬核”操作却忽略了爬虫项目里最基础也最关键的一环——先把目标网站的数据结构摸清楚。链家二手房这个案例我前前后后做了好几轮从最早的简单requests加正则到后来引入BeautifulSoup做结构化解析再到配合Pandas做数据清洗和落库踩过的坑不算少。这篇文章就以“链家二手房房源数据抓取”为完整案例从页面分析、请求构造、数据解析、存储方案到反爬应对和常见问题排查一条线串下来把我实际跑通的思路和代码完整放出来。不管你是有Python基础想练手爬虫还是想通过真实项目理解网页解析的原理这篇都能给你一套可以复现的路径。整个项目代码量不大也就一百多行核心代码但其中涉及的请求头处理、Selector提取、频率控制、异常重试这些细节恰恰是爬虫实战里真正拉开差距的地方。我会尽量讲清楚每一步“为什么这么做”而不是只丢一份能跑的代码。1. 项目设计与技术选型思路1.1 为什么选链家二手房作为爬虫练手项目链家二手房页面有几个特点非常适合做爬虫教学和实战练习。首先它的房源列表页内容是服务端直接渲染的也就是HTML源码里就包含了完整的房源数据字段不需要额外分析XHR接口或者处理JavaScript动态加载这对于requests加BeautifulSoup这套轻量组合来说非常友好。其次它的数据结构非常规整小区名称、户型、面积、朝向、装修、楼层、总价、单价、关注人数这些字段在页面里有固定的DOM结构解析规则相对稳定。还有一个很重要的原因是链家平台的反爬策略强度适中。它有基础的请求头校验、访问频率限制和一定概率的验证码机制但不像某些平台那样动不动就上极验、滑块或者全站瑞数防护。这种“有一定挑战但通过正规技术手段能解决”的难度系数恰恰是学习爬虫的最佳区间——太简单学不到东西太难容易劝退新手。等你把链家这个项目完整跑通了再看其他同类型的信息展示类网站基本一眼就能判断出从哪下手。从数据维度来说二手房房源数据本身也很有分析价值。同一区域不同小区的均价对比、户型分布、面积与总价的关联关系、不同板块的挂牌量变化这些都可以基于抓取的数据做后续分析。你可以通过信息架构了解常规的爬虫采集思路再进一步把它复用到竞品信息监测、市场行情分析等真实业务场景里。1.2 技术栈选型分析requests BeautifulSoup还是Scrapy做爬虫项目第一个要过的决策关就是技术选型。我见过不少人一上来就直接上Scrapy理由无非是“业界主流”“分布式能力强”“扩展性好”。但说实话对链家二手房这种规模的项目来说Scrapy是杀鸡用了牛刀。Scrapy的异步框架、中间件机制、Item Pipeline、Downloader Middleware这些概念本身就有学习成本如果你还不熟悉Python的装饰器、生成器、回调机制直接上手Scrapy很容易被框架本身搞晕出了问题都不知道该查哪里。我的建议是第一版用requests加BeautifulSoup把核心逻辑跑通等你明确知道自己需要并发抓取、需要自动重试、需要统一的存储管道时再考虑迁移Scrapy也不迟。requests负责发HTTP请求拿到HTML源码BeautifulSoup负责从HTML里提取结构化数据这两个库加起来的学习曲线非常平缓而且足够覆盖90%的中小型爬虫场景。在解析库的选择上BeautifulSoup的lxml解析器在性能和容错性之间取得了很好的平衡。正则表达式虽然也能提取数据但面对HTML这种结构不严谨的文档正则是典型的反面教材——维护成本极高一个标签嵌套层级变了就全线崩溃。而XPath虽然性能好但可读性不如BeautifulSoup的CSS选择器风格直观。链家页面结构清晰用BeautifulSoup的select方法配合CSS选择器写出来的代码几乎是半自动文档化的。数据存储方面第一版我推荐直接用Pandas加CSV或者SQLite。CSV的好处是直观Excel可以直接打开查看SQLite的好处是可以做SQL查询方便后续的数据筛选和分析。如果你想把爬虫升级成一个定时运行的采集任务SQLite是更稳的选择写入不会因为文件被占用而出错。后续我会给出两套存储方案的代码。1.3 整体项目架构设计整个项目的架构可以拆成四个模块请求模块、解析模块、存储模块、调度模块。请求模块负责构造HTTP请求、设置请求头、处理连接超时和重试解析模块接收HTML源码提取房源字段清洗异常数据存储模块负责把结构化数据写入CSV或数据库调度模块控制爬取的总页数、并发数和访问频率。这四个模块的边界要清晰不要混在一起写。很多人写爬虫喜欢把所有逻辑堆在几个函数里看起来代码量少但一旦数据字段调整或者页面改版改起来就是一团乱麻。我习惯用类来组织代码每个模块一个类类与类之间通过参数传递数据这样单测、调试、复用都方便。关于抓取范围链家二手房列表页有100页的上限。这意味着即便某个区域有几千套在售房源通过列表页翻页最多只能看到前100页的3000套每页30套。这是一个值得提前了解的边界限制。我们做数据采集时通常的做法是按区域拆分子任务比如朝阳区、海淀区、丰台区各跑一遍而不是只抓北京总列表的前100页。这样既能绕过100页上限又能让后续数据分析多一个“区域”维度。这个细节在我的项目里会体现得很明显。访问频率控制是另一个必须提前设计的环节。链家对请求频率的敏感度中等偏上单次请求如果太快连续翻页十几页就可能触发封禁。我实测下来的稳妥方案是每次请求之间sleep 3到5秒每抓完一个区域的任务后暂停30秒左右。这个节奏下抓取一个区的全部房源大概需要10到15分钟虽然不算快但胜在稳定。如果你非要追求速度那就必须准备多套User-Agent轮换、代理IP池这些东西投入产出比未必划算。2. 核心细节解析与实操要点2.1 页面结构分析与数据定位方法在写任何爬虫代码之前第一步永远是打开浏览器进入链家二手房列表页右键点击“检查”在Elements面板里找到目标数据所在的HTML节点。这个步骤不能跳因为它直接决定你后续解析规则的写法。链家二手房列表页的结构我梳理过每一套房源是ul.sellListContent下的一个li.clear节点每个li内部又包含多个子模块。房源标题在div.title a节点的文本里格式通常是“小区名称 户型描述 面积描述”比如“南北通透 精装两居室 安贞西里 板楼中间层”。这个字段的内容很杂但恰恰是判断房源质量的重要文本来源。房源基础信息在div.address div.flood节点里拆成了小区名称和具体位置两段户型信息在div.address div.houseInfo里包含室厅卫数量、面积、朝向、装修、楼层等字段彼此用|符号分隔。价格信息分布在两个位置div.totalPrice span是总价单位是万元div.unitPrice是每平方米单价单位是元/平。关注数据在div.followInfo里包含关注人数和发布时间格式是“xxx人关注 / 30天以前发布”。这些字段的位置和格式在不同页面之间基本保持一致所以解析规则一旦写好整站通用。我的习惯是先抓一个页面保存为本地HTML文件然后用BeautifulSoup在Jupyter Notebook里反复测试选择器和数据清洗逻辑。这样做的原因是开发阶段频繁请求目标站很容易因为短时间内的密集访问触发反爬策略而且一旦触发调试环境就和目标网站“物理隔离”了效率反而更高。把页面存到本地之后你可以无限次地测试解析代码直到数据清洗逻辑完全正确再回到线上环境跑全量抓取。2.2 请求头构造与Session保持的注意事项requests库设置Header这一步很多爬虫初学者容易忽略但这是整个爬虫工程里最开始也最基础的一道“门禁”。链家的服务端会校验User-Agent默认的python-requests/x.x.x这种UA直接就会被识别为爬虫。实验环境下我用的UA是完整的Chrome浏览器的标识串包括浏览器版本、操作系统、内核信息。此外常见的做法是设置Accept、Accept-Language、Accept-Encoding但要注意Accept-Encoding如果设置为gzip, deflate, br那么服务端响应的内容可能是压缩过的requests会自动解压gzip但brBrotli格式需要额外依赖所以我一般直接删掉br。Session的使用也很关键。requests.Session可以自动管理Cookie保持与服务端的会话状态。链家虽然不强依赖Cookie来维持登录态但保持同一个Session访问多个页面服务端看到的请求序列更接近一个真实用户的浏览行为触发反爬的概率会降低不少。还有一个细节是Referer头的设置。翻页请求的第二页及以后的页面Referer应该指向上一页的URL。虽然链家目前没有强校验Referer但这个习惯要养成因为很多网站对Referer的校验很严格你从搜索页跳详情页、从列表页翻页、从城市站切换区域Referer的值都是有规律的。关于超时设置requests请求一定要加timeout参数不能让它无限等待下去。我习惯设成(connect_timeout, read_timeout)的元组形式比如(5, 10)连接等待5秒读取等待10秒。如果目标服务器响应异常慢直接放弃这次请求做重试不要浪费线程空等。2.3 数据清洗中的高频坑点数据解析完成不等于数据能用中间还隔着一个清洗环节。链家二手房页面上有些数据是格式化过的展示文本直接入库会导致后续分析出问题。举几个我实际遇到的高频问题。总价字段页面显示“560万”它带一个“万”字入库前需要去掉非数字字符并转成整数或浮点数。单价字段更特殊页面显示“56000元/平”其中“元/平”要去掉而数字部分可能带“,”千分位分隔符比如“56,000”清洗时要把逗号也去掉。这些细节如果不处理读取数据时会出现字符串类型无法参与数学运算的尴尬。户型字段houseInfo的原始文本例如“2室1厅 | 89.45平米 | 南 北 | 精装 | 高楼层(共6层) | 1998年建”处理时按竖线|分隔成列表分别提取室厅数、面积、朝向、装修、楼层、年份。这里有两个值得记录的坑第一个坑是“室”和“厅”的数字需要单独提取比如“2室1厅”要用正则(\d)室(\d)厅取值而不是保留整个字符串第二个坑是楼层信息格式不统一有的是“低楼层(共6层)”有的是“地下室(共3层)”还有的是“高楼层(共22层)”我在做楼层归一化时只提取“低楼层”“中楼层”“高楼层”“地下室”这四个类别作为统一标签。关注人数的格式是“89人关注 / 30天以前发布”用正则(\d)人关注提取关注数即可。发布时间字段虽然有“30天以前”“昨天”“1小时以前”这种不统一的值但如果你后续要做“近30天新增房源”这类分析这个字段其实价值不大我一般直接丢弃只保留能准确反映数据状态的字段。2.4 分页机制与去重策略链家分页URL的构造规律是第一页是https://[city].ls.com/ershoufang/pg1/第二页是pg2依次类推累计到pg100。这个地方我踩过一个大坑——链家的分页URL并不是从pg1开始的你直接访问不带页码的URL其实等同于pg1而翻页请求如果从pg2开始跳跃访问有一部分旧数据会在某个页码之后重复出现。我实测下来这种现象不算明显但为了稳妥我在解析每一条房源数据时都附带一个house_id。这个ID可以从房源详情页URL中拿到列表页里每个房源的链接格式是https://[city].ls.com/ershoufang/[一串数字].html那串数字就是房子在该平台的唯一ID。程序运行过程中我会把保存在内存里的Set结构不断添加已经解析过的house_id当遇到重复ID时直接跳过。如果爬虫中断需要续爬也可以把已采集CSV里的ID列加载进Set实现断点续爬。链家每页30套房源100页最多3000套去重逻辑在这类数据量级下完全在内存里就能搞定不需要引入Redis这种外部依赖。3. 实操过程与核心环节实现3.1 环境准备与依赖安装项目运行环境我用的是Python 3.9以上的版本操作系统不限Windows、Linux、macOS均可。如果你还在纠结怎么装Python环境直接去Python官网下载对应你系统的安装包安装时勾选“Add Python to PATH”免得后面在命令行里找不到python命令。装完之后打开终端输入python --version确认版本号。接下来在项目目录里创建虚拟环境这是隔离项目依赖的好习惯防止不同项目之间互相污染mkdir lianjia_spider cd lianjia_spider python -m venv venv # Windows环境激活 venv\Scripts\activate # macOS / Linux环境激活 source venv/bin/activate然后安装依赖包。本项目核心依赖是requests、beautifulsoup4、lxml、pandaspip install requests beautifulsoup4 lxml pandas这里说一下为什么解析库要配lxml。BeautifulSoup本身只是一个HTML/XML解析器的封装层它的html.parser是Python标准库自带的解析器解析速度一般容错能力一般而lxml是一个C语言实现的解析库解析速度快数倍对HTML语法错误的容错性也更好。在爬虫场景下网页源码里不可避免会有标签闭合不规范的情况lxml在这种环境下表现稳定得多。如果上面pip install指令安装lxml失败通常是缺少编译环境Windows用户直接去pip install lxml之前先升级pip试试一般能解决。如果后续想把数据从CSV迁移到SQLite还需要引入sqlite3不过它是Python标准库的一部分不用额外安装。编辑代码我推荐VS Code加Python插件或者直接用PyCharm都是免费的按个人习惯选。3.2 请求模块完整实现首先定义一个请求类负责封装Session、请求头、超时、重试和频率控制。这个类承担的是整个爬虫的“访问”职责解析逻辑不应该掺和进来。import time import random import requests from fake_useragent import UserAgent class LianjiaFetcher: def __init__(self): self.session requests.Session() # 这里使用fake_useragent库随机生成UA替代硬编码字符串 self.ua UserAgent() self.session.headers.update({ Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, Upgrade-Insecure-Requests: 1, }) self.max_retries 3 def _random_ua(self): return {User-Agent: self.ua.random} def fetch(self, url, refererNone, retries0): headers self._random_ua() if referer: headers[Referer] referer try: resp self.session.get(url, headersheaders, timeout(5, 10)) if resp.status_code 200: resp.encoding resp.apparent_encoding return resp.text elif resp.status_code 403: # 403通常是触发了反爬这种情况下最重要的是停下来 print(f[警告] 状态码403疑似触发反爬url: {url}) time.sleep(30) return None else: print(f[警告] 状态码{resp.status_code}url: {url}) return None except requests.RequestException as e: print(f[错误] 请求异常: {e}, 剩余重试次数: {self.max_retries - retries}) if retries self.max_retries: time.sleep(2 ** retries) # 指数退避1s, 2s, 4s return self.fetch(url, referer, retries 1) return None def close(self): self.session.close()关于编码处理有一个细节值得展开。链家页面的编码是UTF-8resp.encoding resp.apparent_encoding这一步在大多数情况下是多余的但写成这样更安全。requests库从响应头推断编码时如果服务端没有明确返回charset字段可能推断错误导致中文乱码。apparent_encoding是requests基于内容自动探测的编码比盲目信任响应头可靠。代价是每次请求会多做一次探测性能上略有损耗但对于低频爬虫来说完全可以接受。频率控制我放在调度逻辑里而不是请求模块里。原因是请求模块只负责单次请求成功与否而频率控制是全局策略两者的职责边界要清晰。调度模块每发完一次请求就让程序sleep一段时间这个间隔我建议设置在[2.5, 4.5]秒之间随机取值。为什么用随机区间而不是固定3秒因为服务端的反爬系统会对固定时间间隔的请求序列做模式识别随机化之后请求序列的特征更像真人操作触发率明显降低。3.3 解析模块完整实现解析模块负责从HTML源码中提取结构化数据。我定义一个Parser类接收HTML字符串返回房源字典列表。import re from bs4 import BeautifulSoup class LianjiaParser: def __init__(self, html, city): self.soup BeautifulSoup(html, lxml) self.city city # 记录城市便于后续多城市汇总 def _parse_price(self, total_text): 从560万解析总价万元 match re.search(r(\d\.?\d*), total_text) return float(match.group(1)) if match else None def _parse_unit_price(self, unit_text): 从56000元/平解析单价元/平 cleaned unit_text.replace(,, ).replace(元/平, ) match re.search(r(\d\.?\d*), cleaned) return float(match.group(1)) if match else None def parse_list_page(self): items [] li_list self.soup.select(ul.sellListContent li.clear) for li in li_list: # 房源ID detail_link li.select_one(div.title a) href detail_link.get(href, ) if detail_link else hid_match re.search(r/(\d)\.html, href) house_id hid_match.group(1) if hid_match else None # 标题 title detail_link.get_text(stripTrue) if detail_link else None # 位置与小区 flood_node li.select_one(div.address div.flood) if flood_node: pos_node flood_node.select_one(div.positionInfo) community pos_node.get_text(stripTrue) if pos_node else None else: community None # 户型、面积、朝向等信息 house_info_node li.select_one(div.address div.houseInfo) house_info house_info_node.get_text(stripTrue) if house_info_node else info_parts [part.strip() for part in house_info.split(|)] # 总价与单价 total_node li.select_one(div.totalPrice span) total_price self._parse_price(total_node.get_text(stripTrue)) if total_node else None unit_node li.select_one(div.unitPrice) unit_price self._parse_unit_price(unit_node.get_text(stripTrue)) if unit_node else None # 关注量 follow_node li.select_one(div.followInfo) follow_text follow_node.get_text(stripTrue) if follow_node else follow_match re.search(r(\d)人关注, follow_text) follow_count int(follow_match.group(1)) if follow_match else 0 items.append({ house_id: house_id, title: title, community: community, house_info: house_info, total_price: total_price, unit_price: unit_price, follow_count: follow_count, city: self.city, }) return items解析模块写完后记得单测。把之前保存到本地的HTML文件喂给这个Parser用assert检查解析结果的数量和字段类型。这一步能在开发阶段就发现问题极大降低线上调试成本。3.4 调度与存储完整实现调度模块是整个爬虫的入口负责构造URL列表、调用Fetcher和Parser、管理数据存储。存储模块第一版用Pandas写CSV第二版加了SQLite支持。这里两套都放出来看你需求选。import time import pandas as pd import sqlite3 class LianjiaSpider: def __init__(self, citybj, districtNone, max_pages100): self.city city self.district district self.max_pages max_pages self.fetcher LianjiaFetcher() self.parser LianjiaParser self.data [] self.visited_ids set() def build_url(self, page): base fhttps://{self.city}.ls.com/ershoufang/ if self.district: base f{self.district}/ return base fpg{page}/ def run(self): try: for page in range(1, self.max_pages 1): url self.build_url(page) print(f[信息] 正在抓取第{page}页: {url}) html self.fetcher.fetch(url, refererself.build_url(page - 1) if page 1 else None) if html is None: print([信息] 本页获取失败跳过) time.sleep(10) continue parser self.parser(html, cityself.city) items parser.parse_list_page() if not items: print([信息] 列表页没有解析出数据可能页面结构变化或遇到验证码停止翻页) break new_count 0 for item in items: if item[house_id] and item[house_id] in self.visited_ids: continue self.visited_ids.add(item[house_id]) self.data.append(item) new_count 1 print(f[信息] 新增{new_count}条累计{len(self.data)}条) # 随机延时控制访问频率 delay random.uniform(2.5, 4.5) time.sleep(delay) else: print([信息] 全部页面抓取完成) finally: self.fetcher.close() def save_to_csv(self, filenamelianjia_houses.csv): df pd.DataFrame(self.data) df.to_csv(filename, indexFalse, encodingutf-8-sig) print(f[信息] 数据已保存至 {filename}共{len(df)}条) def save_to_sqlite(self, db_pathlianjia.db): conn sqlite3.connect(db_path) df pd.DataFrame(self.data) df.to_sql(houses, conn, if_existsreplace, indexFalse) conn.close() print(f[信息] 数据已保存至SQLite: {db_path})encodingutf-8-sig这个参数值得专门提一下。如果用默认的utf-8编码写CSV生成的文件用Excel打开时中文会乱码。原因在于Excel默认用ANSI编码读取CSV而utf-8-sig会在文件开头写入BOM头字节顺序标记Excel识别到BOM头后就会自动按UTF-8解析。这个坑在Windows环境上尤其常见办公室同事拿你给的CSV打开一看全是乱码你折腾半天发现是编码问题真的很影响效率。如果遇到抓到一半程序中断的情况断点续爬的逻辑可以这样加在run()方法之前从已有CSV加载house_id到visited_ids集合同时把self.data初始化为已有DataFrame的内容。这样即使中断了程序也能接着往下跑不用从头再来。链家列表页前几页翻完也就几分钟中断重启的成本不算高但断了从头跑是真的难受尤其是已经跑到第80页的时候。3.5 基于区域的多任务拆分之前提到链家列表页有100页上限为了采集完整数据我在生产环境是按区域拆任务的。北京链家二手房的一级区域比如东城、西城、朝阳、海淀、丰台、石景山、通州、昌平、大兴、顺义、房山、门头沟、怀柔、密云、平谷、延庆等每个区域作为独立的URL前缀。districts [dongcheng, xicheng, chaoyang, haidian, fengtai, shijingshan, tongzhou, changping, daxing, shunyi, fangshan, mentougou] all_data [] for district in districts: print(f[信息] 开始抓取区域: {district}) spider LianjiaSpider(citybj, districtdistrict, max_pages100) spider.run() all_data.extend(spider.data) # 每个区域任务结束后休息30秒避免对目标站点造成压力 time.sleep(30) df pd.DataFrame(all_data) df.to_csv(bj_all_districts.csv, indexFalse, encodingutf-8-sig)这种按区域拆分的方式还有一层好处即使某个区域触发了反爬导致任务失败其他区域的数据已经安全落盘不需要整体重跑。我实际跑下来触发反爬往往是全局性的一旦某个段IP被标记后续所有请求都会受影响所以真的遇到403正确操作是立即停止程序等30分钟到1小时再继续而不是换UA硬怼。4. 数据分析扩展思路与结果验证4.1 数据质量校验方法抓完数据别急着分析先做一版数据质量校验。简单粗暴的方式是统计总记录数、检查关键字段的空值率、验证价格字段的数值范围是否合理。比如总价字段如果出现小于10万的数值基本可以判定是解析异常或者是车位/地下室等特殊房源需要人工确认。单价字段如果出现低于5000元/平的情况大概率是数据清洗环节出了问题。另一种校验方式是我个人很推荐的“抽样人工核对”。从CSV里随机抽10条记录打开链家网页找到对应的房源人工对比字段是否与页面展示一致。这个步骤虽然原始但能在第一时间发现解析规则的隐性错误。我记得有一次解析出来的小区名称和实际小区差了两个字原因是最新页面结构里把positionInfo的子节点层级改了列表页上显示的是“小区简称”详情页里才有全名。我是通过抽样核对发现的当时如果直接拿去分析整个区域均价都会被拉偏。字段类型也要验证。Pandas读入CSV后检查total_price和unit_price是否是float64类型如果显示为object说明数据里混入了无法转数字的脏数据。用pd.to_numeric配合errorscoerce强制转换然后重新检查空值情况。4.2 典型分析场景区域房价对比采集到结构化数据之后最直接的分析就是按区域统计平均挂牌单价和平均总价。用Pandas一行groupby就搞定了df[unit_price] pd.to_numeric(df[unit_price], errorscoerce) df[total_price] pd.to_numeric(df[total_price], errorscoerce) area_stats df.groupby(city).agg( 平均单价(unit_price, mean), 平均总价(total_price, mean), 房源数量(house_id, count) ).round(1) print(area_stats.sort_values(平均单价, ascendingFalse))如果需要按更细粒度分析可以把house_info字段里的朝向、装修、楼层等字段拆出来分别统计。比如“南北通透”的房子单价是否显著高于“全朝南”的房子、精装房源和简装房源的挂牌总价差多少、不同楼层的房源在单价上有无规律。这些都是常见的二手房市场研究问题数据在手就能快速得到基于真实挂牌数据的答案。4.3 从单机任务升级为定时采集的思考如果你想让这个爬虫长期运行定时抓取每天的挂牌数据事情的性质就变了但不止是加个while True这么简单。你需要考虑三点一是如何检测页面结构变化链家改版不算频繁但每隔几个月会有一次你需要有一个“页面结构异常检测”机制比如对比解析字段数量、关键节点的CSS选择器是否还能定位到元素二是如何做增量更新每天抓的数据里只有少量新上架或价格调整的房源需要以house_id为主键做upsert操作三是数据版本管理每天的数据快照要有日期字段方便追溯历史变化。这三件事单独哪一件都不算难但组合在一起就需要一个相对完整的数据管道设计。从个人练手项目的角度先把当天增量抓取和去重更新做好就够用了。更复杂的调度系统等你真正有长期数据需求的时候自然会知道怎么设计。5. 80%的人都会踩的坑常见问题与排查实录5.1 状态码403与验证码问题链家触发反爬的最典型表现是返回403状态码或者页面内容里出现验证码组件。如果你的程序连续跑了几十页突然在某一页返回403大概率是访问频率过快被服务端识别了。我实测下来的直观感受是单IP下访问间隔小于1秒连续5到10页就会触发间隔小于2秒有时候能撑到20页以上间隔大于3秒基本稳定。公众号文章里很多人的经验是“把sleep调到3秒”这个数字不是拍脑袋定的而是基于实际操作统计出来的经验值。真遇到403我的建议是先停30分钟不要换UA立刻重试。重启程序前检查一下当前的IP是否已经被链家列入限制名单最简单的检测方法是用同IP的浏览器手动访问链家二手房列表页如果浏览器里出现了验证码说明IP被标记了这个时候再跑爬虫只会加重封禁。等浏览器能正常访问了再考虑重跑。5.2 翻页过程中出现重复数据列表页翻页抓取时链家偶尔会出现相邻两页之间存在重复的房源。原因在于房源数据是动态变化的用户在浏览你抓取的短时间内提交了新房源或者下架了旧房源分页数据的边界发生移动导致同一套房源同时出现在第N页和第N1页。解决方案就是我前面提到的house_id去重这种方法在任何情况下都能保证数据集合的唯一性。如果你是重跑中断任务去重逻辑也能避免重复入库。5.3 HTML解析为空或字段缺失列表页能打开、状态码正常但select(ul.sellListContent li.clear)一个元素都没筛出来这种问题在页面改版后最容易出现。排查步骤固定的三步第一步把该页HTML保存到本地搜索一下sellListContent这个class是否存在第二步如果class不存在搜一下li节点里的>

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价