资讯动态

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

发布时间:2026/10/27 18:23:42 来源:尧图企业网站定制
简介这是一份基于Python的链家二手房信息爬取与数据库存储源码包面向爬虫学习者、房地产数据研究人员及需要二手房信息批量采集与备份的开发者。系统以Python脚本自动抓取链家二手房列表页中的标题、价格、地理位置、房屋详情等关键信息再配合SQL脚本完成建表与数据导入实现从采集到存储的完整闭环。压缩包共有三十三个文件包含一个Python爬虫脚本、一个SQL数据库脚本、三十张抓取到的房源图片以及一份说明文档整体大小约八百一十六KB结构紧凑便于快速部署和学习。目前已有174人学习下载适合用来理解爬虫解析、数据清洗、图片保存与关系型数据库设计的整体流程也可作为二手房市场分析的原始数据来源。1. 链家二手房爬虫先想清楚这三点再动手写代码链家二手房信息爬取与数据库存储是Python爬虫入门到进阶之间最经典的一个实战项目目标网站结构清晰、字段丰富、反爬强度适中既能练到请求与解析又必须面对登录态、滑块校验和字体反爬这些真实问题。很多人照着视频课把代码敲完就扔了等自己换个城市、换套字段马上翻车——问题大多出在数据库设计表和请求频率控制上。这篇文章会直接把一套能跑的方案拆开讲从页面结构分析、字段提取到MySQL表结构设计、增量更新策略再落到具体的避坑清单适合已经能写基础requests脚本、想系统做完一个完整项目的读者。看完你不需要再去找免费源码大全自己能写出一份可维护的版本。2. 爬虫设计与工具选型为什么是requests加BeautifulSoup而不是Scrapy2.1 轻量方案和框架方案的分界线接这个项目之前先别急着写代码要回答的问题是用什么工具组合。链家二手房这个场景数据量级属于“单城市几千到几万条”采集频率通常是一天一次或两天一次根本不是高并发海量采集。常见做法是用requests加BeautifulSoup就能扛住没有必要上Scrapy——Scrapy的Middleware、Item Pipeline、Selector这套体系能解决大规模分布式采集的问题但代价是项目结构重、调试链路长新手一上来就被框架绑住反而看不清请求和解析本身。我一般这样选型目标页面少于十万级、采集频率低、不需要增量爬取的时候直接requests加BeautifulSoup等哪天你需要采集几十个城市、每分钟上千次请求、要断点续爬和分布式调度再迁到Scrapy那时候你已经知道自己要什么了。这个项目按单城市起步就够先把主流程跑通比一开始就铺框架更容易落地。2.2 环境准备与依赖清单基础环境是Python 3.8以上建议3.10或3.11太老的版本在类型注解和zoneinfo这类标准库支持上有差别。虚拟环境用venv就行别用Anaconda链家爬虫依赖少没必要背一个几百兆的发行版。依赖就四个requests是HTTP客户端beautifulsoup4是HTML解析器lxml是解析引擎BeautifulSoup的lxml模式比默认的html.parser快得多pymysql是MySQL驱动。安装命令pip install requests beautifulsoup4 lxml pymysql注意beautifulsoup4的导入名是bs4安装名是beautifulsoup4这两者不一致是常见的低级坑。装完之后顺手验证一下版本python -c import requests, bs4, lxml, pymysql; print(requests.__version__, bs4.__version__, lxml.__version__, pymysql.__version__)能看到版本号就说明环境没问题。这一步卡住的人不少多数是pip源的问题国内网络环境建议先配好镜像源再装不然下载lxml的预编译包会慢到你想砸电脑。2.3 项目目录结构一开始就按可维护的方式组织代码不要一个main.py从顶写到尾。等你要加字段、加城市、改存储逻辑的时候一个文件几百行会变成一场灾难。常见做法是把项目拆成spider、parser、storage、config四块各自的职责说清楚lianjia_spider/ ├── config.py # 请求头、目标城市、数据库连接串 ├── spider.py # 页面抓取构造URL、带重试的请求 ├── parser.py # 页面解析列表页和详情页字段提取 ├── storage.py # 数据入库MySQL表操作、去重与批量插入 ├── main.py # 调度入口控制采集流程和日志 └── logs/ # 运行日志目录这样的好处是解耦。反爬策略变了只改spider.py页面结构变了只动parser.py表结构调整只碰storage.py调试的时候能快速定位问题而不是在一个千行文件里CtrlF找函数。3. 链家页面结构分析与爬取实现列表页、详情页、字段提取3.1 列表页URL规律与分页参数链家二手房列表页的结构是稳定的https://{城市代码}.lianjia.com/ershoufang/pg{页码}/。城市代码是拼音缩写北京是bj上海是sh深圳是sz成都是cd这个在链家首页看地址栏就能确认。第1页的URL可以省略pg1直接访问https://bj.lianjia.com/ershoufang/。分页有个上限逻辑同一个筛选条件下最多只能翻到100页。这个数字不写死在文档里是爬的时候发现的——第101页会返回一个空列表。所以如果要爬的数据超过一百页正确做法是用筛选条件去缩小范围常见做法是加l2楼层、l3面积这类参数拆分查询而不是硬翻。比如按面积段拆https://bj.lianjia.com/ershoufang/pg1l3l2/这种带筛选的URL能翻到的总页数会重新计算。列表页的解析目标是从每个房源卡片里拿到两样关键信息房源的唯一ID和详情页链接。链家的房源ID是URL路径里ershoufang/后面那串数字格式像101234567890.html。这是整个数据模型的主键来源必须从这里开始就抓准。3.2 详情页才是完整字段的来源列表页能拿到的字段有限标题、小区名、总价、单价、几室几厅、面积、朝向。但你要做数据库存储光有这些不够——建成年代、楼层、装修情况、产权年限、挂牌时间这些有价值的信息都在详情页里。所以完整方案是两级采集先翻列表页拿到所有房源ID和基础字段再按ID逐个请求详情页补全字段。这里有个常见的效率误判觉得详情页要一个个请求太慢想直接从列表页的JSON接口拿全部数据。链家确实是接口返回JSON的但接口里的字段比页面渲染出来的少而且接口加了签名参数和访问频率限制新手去逆接口反而比解析HTML更容易封IP。我做过对比同样的采集任务直接爬页面能稳定跑完逆接口爬到一半就被滑块拦了。所以老老实实两级爬取反而是最稳的路径。3.3 列表页字段提取从卡片结构里抠数据下面这段代码是列表页解析的核心逻辑用BeautifulSoup从HTML里提取房源卡片信息。注意选择器是基于链家当前页面结构写的如果你在自己的环境里跑先F12确认一下class名有没有变化。def parse_list_page(html: str, city: str) - list[dict]: 解析链家二手房列表页提取房源基础信息。 返回的每条记录包含房源ID、标题、小区、总价、单价、基础户型字段 soup BeautifulSoup(html, lxml) items [] # 链家列表页每个房源卡片的容器是 li.clear这个结构多年没变 for card in soup.select(li.clear): try: # 详情页链接从 a 标签的 href 里提取房源ID link_tag card.select_one(a) if not link_tag or ershoufang not in link_tag.get(href, ): continue house_id link_tag[href].split(/)[-1].replace(.html, ) # 小区名.community 下的 span 内文本注意不是 a 标签 community_tag card.select_one(div.community span) community community_tag.get_text(stripTrue) if community_tag else # 户型信息.houseInfo 的文本是 2室1厅 | 89.5平米 | 南 北 | ... 这样的管道符分隔格式 house_info_tag card.select_one(div.houseInfo) house_info_text house_info_tag.get_text(separator|, stripTrue) if house_info_tag else parts [p.strip() for p in house_info_text.split(|) if p.strip()] # totalPrice 的文本是 580万 这样的格式 price_tag card.select_one(div.totalPrice) total_price_text price_tag.get_text(stripTrue) if price_tag else 0 items.append({ house_id: house_id, community: community, room_count: parts[0] if len(parts) 0 else , area: parts[1].replace(平米, ) if len(parts) 1 else , orientation: parts[2] if len(parts) 2 else , total_price: total_price_text.replace(万, ), city: city, }) except (AttributeError, IndexError) as e: # 单条解析失败不影响整体流程记录一下继续走 print(f列表页解析单条失败: {e}) continue return items这段代码需要说明几个关键选择。首先是get_text(separator|)这个用法链家的.houseInfo节点里有多个span和a标签直接get_text()会把它们粘连在一起变成一坨没法拆的字符串用separator|把它们按管道符隔开再split就能稳定拿到每个字段。其次是community字段只取span的文本因为链家在这里还会渲染一个a标签指向小区页面取错了会把HTML标签文本一起拿进来。3.4 详情页字段提取核心配套信息都在这里详情页解析比列表页多两步一是要处理页面上挂载的JSON数据二是要应对链家的字体反爬后面避坑章会单独说。这里先给一个稳定提取基础字段的版本覆盖建成年代、楼层、装修、挂牌时间这几个数据库里常用的字段def parse_detail_page(html: str, house_id: str) - dict: 解析详情页提取列表页拿不到的配套字段。 soup BeautifulSoup(html, lxml) # 基础信息区一个 dl 里包含多个 dt/dd 对遍历提取 base_info {} for dl in soup.select(div.baseInfo dl): dt dl.select_one(dt) dd dl.select_one(dd) if dt and dd: key dt.get_text(stripTrue).replace(, ) value dd.get_text(stripTrue).replace( , ) base_info[key] value # 交易信息区挂牌时间、房屋年限等通常和基础信息区的选择器不同 transaction_info {} for dl in soup.select(div.transactionInfo dl): dt dl.select_one(dt) dd dl.select_one(dd) if dt and dd: key dt.get_text(stripTrue).replace(, ) value dd.get_text(stripTrue).replace( , ) transaction_info[key] value return { house_id: house_id, build_year: base_info.get(建成年代, ), floor: base_info.get(楼层, ), decoration: base_info.get(装修情况, ), list_date: transaction_info.get(挂牌时间, ), last_deal_price: transaction_info.get(小区均价, ), }这个函数踩过不少坑关键提醒是dt的文本末尾带全角冒号“”不替换掉会污染数据库字段dd的内部可能有多个文本节点用get_text(stripTrue)并去掉空格比逐个清理子节点快很多。还有一点链家有些房源详情页会渲染“该房源已下架”的占位页此时baseInfo区不存在解析出来全是空值——所以这个函数要配合一个检查如果house_id对应的页面里找不到baseInfo节点直接把该条标记为失效而不是把空数据写进库。3.5 请求封装UA、Cookie、超时与重试请求封装是整个爬虫能不能长跑的核心。链家对请求头的校验不算严格但会盯请求频率和Cookie有效性。一个可用的请求函数要处理四件事随机UA、超时、重试、频率控制。import random import time import requests HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, } def fetch_url(url: str, session: requests.Session, max_retries: int 3) - str | None: 带重试的请求。链家偶尔会返回503或验证码页面重试能绕过大部分临时抖动。 for attempt in range(max_retries): try: resp session.get(url, headersHEADERS, timeout10) resp.raise_for_status() # 链家反爬会返回一个200但内容是验证码引导页的情况这里做内容校验 if 验证码 in resp.text or verify in resp.url: print(f触发验证码: {url}, 第 {attempt 1} 次重试) time.sleep(5 attempt * 5) continue return resp.text except requests.RequestException as e: print(f请求失败: {url}, 错误: {e}, 第 {attempt 1} 次重试) time.sleep(2 * (attempt 1)) return None这里的session从外部传入很好用同一个Session能复用连接和Cookie比每次请求新建一个requests对象要省不少握手开销。重试逻辑里睡了递进时间第一次重试等5秒第二次等10秒给反爬系统一个冷却窗口。还有一个细节链家有些反爬是在响应里直接写“访问过于频繁”或“验证码”字样所以用resp.text的内容检测比只看状态码更靠谱——很多反爬页面返回的是200但内容根本不是你要的数据。4. 数据库存储设计MySQL表结构、去重策略、批量插入4.1 表结构设计字段怎么定、索引怎么建数据库选MySQL是链家爬虫项目的默认选项理由很直接数据量在百万以内MySQL单表完全扛得住且Python生态里pymysql稳定可靠不需要引入MongoDB这种文档型数据库来增加学习成本。但表结构设计不会因为数据量小而随便——恰恰因为要反复更新唯一键和索引必须在建表时就定好。CREATE TABLE house_info ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 自增主键, house_id VARCHAR(32) NOT NULL COMMENT 链家房源ID业务主键, city VARCHAR(16) NOT NULL COMMENT 城市代码如bj, community VARCHAR(128) DEFAULT COMMENT 小区名称, room_count VARCHAR(16) DEFAULT COMMENT 户型如2室1厅, area DECIMAL(8,2) DEFAULT 0.00 COMMENT 建筑面积单位平米, orientation VARCHAR(32) DEFAULT COMMENT 朝向如南/北, total_price DECIMAL(10,2) DEFAULT 0.00 COMMENT 总价单位万, unit_price DECIMAL(10,2) DEFAULT 0.00 COMMENT 单价单位元/平米, build_year VARCHAR(16) DEFAULT COMMENT 建成年代, floor VARCHAR(32) DEFAULT COMMENT 楼层描述, decoration VARCHAR(16) DEFAULT COMMENT 装修情况, list_date DATE DEFAULT NULL COMMENT 挂牌日期, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 首次抓取时间, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 最后更新时间, PRIMARY KEY (id), UNIQUE KEY uk_house_id (house_id), KEY idx_city (city), KEY idx_list_date (list_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT链家二手房房源信息表;这里有几个设计决策值得说清楚。第一house_id用VARCHAR(32)而不是BIGINT虽然链家的房源ID看起来是数字但未来保不齐有字母或其他字符用字符串更安全。第二house_id上建唯一索引是整张表的命脉——去重全靠它。第三area和total_price用DECIMAL而不是FLOAT因为价格数据做加减和比较时FLOAT的精度漂移会给你带来意想不到的对不上账的问题。第四list_date建成DATE类型而不是VARCHAR这样你能直接按月份做SQL聚合比如统计最近三个月挂牌量变化。4.2 去重与增量更新用INSERT ... ON DUPLICATE KEY UPDATE爬虫重复跑是必然的问题是怎么处理重复。全表删除再重插太暴力等你有两千条数据时无所谓但等到两万条时就会发现这个是纯浪费且还会把主键搞得乱七八糟。正确做法是用MySQL的INSERT ... ON DUPLICATE KEY UPDATE语法让数据库自己判断house_id已存在就更新价格和挂牌时间不存在就插入新纪录。import pymysql INSERT_SQL INSERT INTO house_info (house_id, city, community, room_count, area, orientation, total_price, unit_price, build_year, floor, decoration, list_date) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE community VALUES(community), total_price VALUES(total_price), unit_price VALUES(unit_price), updated_at NOW() def upsert_houses(cursor: pymysql.cursors.Cursor, houses: list[dict]) - int: 批量写入房源数据。重复的house_id只做字段更新不重复插入。 if not houses: return 0 rows [( h[house_id], h[city], h[community], h[room_count], float(h[area] or 0), h[orientation], float(h[total_price] or 0), float(h.get(unit_price, 0) or 0), h.get(build_year, ), h.get(floor, ), h.get(decoration, ), h.get(list_date) or None ) for h in houses] cursor.executemany(INSERT_SQL, rows) return cursor.rowcount用了executemany批量执行而不是在Python循环里逐条execute——前者走的是MySQL的二进制协议握手效率和速度完全不是一个级别。2000条数据的批量插入逐条可能要几秒executemany几十毫秒就能完成。4.3 数据库连接与事务控制数据库连接不能每个请求都新建那是灾难性的性能开销。常见做法是写一个简单的连接管理模块在爬虫启动时建立一个连接在整个采集周期内复用结束时再关闭。import pymysql def get_connection(): 创建数据库连接。注意charset要指定utf8mb4不然中文会乱码。 conn pymysql.connect( host127.0.0.1, port3306, userspider_user, passwordyour_password_here, databaselianjia_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor, ) return conn事务的处理原则是一个完整页面的数据列表页拿到的基础信息加详情页的补充信息作为一个事务提交。不要每插一条就commit一次也不要等全量采集完才commit一次——前者慢后者一旦中途崩了已经采集的数据全丢。实操里我一般是一批房源比如50条commit一次这样崩溃回退的损失可控写库的效率也够。4.4 增量更新策略每天只抓变化的数据链家二手房不是天天都有几千套新房源每天真正变化的是价格调整、新挂牌、下架。全量重抓在数据量小时无所谓但数据量上万以后每次都全量爬等于每天在浪费大量请求次数也会加速封IP。增量策略是这样的库里已有的house_id集合先查出来爬列表页时遇到已存在的ID直接跳过详情页请求只对不存在的ID发起详情页抓取。def get_existing_ids(cursor) - set: 把库里已有的house_id一次性查回来用于增量爬取时跳过详情页。 cursor.execute(SELECT house_id FROM house_info) return {row[house_id] for row in cursor.fetchall()}这个集合的大小在十万级时完全放得进内存不用做分批查询。真正的增量难点在“已存在但价格变了”的房源——列表页本身是有价格字段的所以即使跳过详情页列表页的解析结果还是能通过ON DUPLICATE KEY UPDATE把最新价格覆盖进库里这样就能做到只请求列表页就完成价格更新只有新房源才需要额外请求详情页补全字段请求量直接降一半以上。5. 链家爬虫避坑指南反爬、字体加密、封IP、数据错乱、断点续爬5.1 字体反爬页面看起来正常但数字全部错乱现象用浏览器打开链家详情页总价显示“580万”但爬回来的HTML里对应的文本却不是“580”而是乱码或者一个莫名其妙的字符。原因链家用了CSS字体反爬把数字0-9映射到了私有字体文件的非标准编码上浏览器能正渲染是因为页面加载了WOFF字体文件做字符映射你的requests拿到的是原始HTML没有加载字体自然看到的就是乱码。解决这是链家爬虫里最出名的一个坑普通爬虫教程根本不会提。常见做法是拿到页面后先查font-face声明的WOFF文件URL下载后用fontTools库解析cmap表建立“Unicode码点→真实数字”的映射关系再对页面里的乱码字符做替换。不过这套方案的工程量不小对新手来说性价比不高。我的建议是如果只是做数据库存储列表页的数字是明文未加密的你要的核心字段从列表页拿就行详情页里如果碰到加密数字直接在解析层跳过先保证能入库后面有这个需求再上字体解析方案。5.2 IP被封怎么判断封禁和有效规避现象爬到一半突然连续大量超时或返回验证码页甚至出现“当前访问人数过多”的提示页面。原因请求频率太高超过了链家的频率阈值。解决最有效且便宜的做法是控制并发和请求间隔。单线程加随机sleep就够用关键是每次请求之间至少间隔2到4秒而且间隔时间不能固定——固定间隔会被识别为爬虫特征。具体做法是time.sleep(random.uniform(2, 4))。千万避免用多线程并发去爬链家短时间高并发是账号被封的头号原因。如果你要批量抓多个城市建议一个城市一个城市地跑而不是同时开几个进程并行请求。5.3 数据错乱字段解析串位现象某些房源入库后面积字段的值跑到了朝向字段里或者build_year变成了乱码。原因链家页面里不同房源的信息结构并不是100%一致的——有的房源没有“建成年代”这个dt有的朝向是“南 北”带空格有的houseInfo分割后字段数不够导致解析时按固定索引取值取错了位置。解决解析时用dict.get()而不是下标索引并且一定要做字段类型校验。比如面积字段解析出来经过float()转换转换失败就存0而不是让程序崩溃朝向字段清洗掉所有空格再入库。写一个简单的clean_field函数统一处理这些杂数据def clean_field(value: str, field_type: str str) - str | float: 清洗爬虫字段的通用函数。链家的文本经常带空格、全角字符、nbsp。 if not value: return if field_type str else 0 text value.replace(\xa0, ).replace( , ).strip() if field_type float: try: return float(text.replace(万, ).replace(,, )) except ValueError: return 0 return text5.4 断点续爬跑了一半挂了怎么办现象爬了两千多条程序突然因为网络异常抛异常退出再启动时从头开始爬已经入库的数据又不能重复请求。解决不要让整个爬虫是“从第1页到第100页”的单一大循环而是把分页和详情页的进度状态打到日志里同时利用数据库本身做断点判断——重启后首先查house_info表拿已有ID集合列表页的爬取从上次日志里记录的最后页码开始。最简单的方式是维护一个crawl_state.txt每完成一页就覆写一次页码启动时先读它def get_last_page(state_file: str crawl_state.txt) - int: 读取上次爬到的页码用于断点续爬。没找到就从第1页开始。 try: with open(state_file, r, encodingutf-8) as f: return int(f.read().strip()) except (FileNotFoundError, ValueError): return 1 def save_page(page: int, state_file: str crawl_state.txt): 每完成一页就写入页码崩溃后可以从这里恢复。 with open(state_file, w, encodingutf-8) as f: f.write(str(page))这个文件方案比数据库记录状态更轻量且任何时候都能一眼看到爬到了哪里排查问题时非常好用。5.5 请求头校验少了Accept-Language会怎样现象有些时候请求返回200但内容为空页面解析出来是零条房源。原因链家对请求头里的Accept-Language做校验没带这个字段的请求会被当作异常流量返回空白页或异常页面。解决在请求头里补上Accept-Language: zh-CN,zh;q0.9。这个细节特别容易忽略因为浏览器开发者工具里复制的请求头会自动带上但你自己手写headers时往往会漏掉。还有Referer也很重要详情页的请求要带上列表页的URL作为Referer链家会校验站内跳转逻辑。6. 主流程组装与最终验证把整个爬虫串成一条可靠的数据管道前面的模块各司其职最后一步是把它们组装进一个可执行的主流程里。这里给一个完整的main.py骨架包含日志输出和异常兜底你也可以按自己的数据量调整批量大小和sleep范围。import logging import random import time import requests from spider import fetch_url, get_session from parser import parse_list_page, parse_detail_page from storage import upsert_houses, get_connection, get_existing_ids logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(logs/spider.log, encodingutf-8), logging.StreamHandler() ] ) def main(): city bj base_url fhttps://{city}.lianjia.com/ershoufang/pg{{}}/ # 启动时建立数据库连接、恢复断点信息 conn get_connection() cursor conn.cursor() existing_ids get_existing_ids(cursor) session requests.Session() start_page get_last_page() for page in range(start_page, 101): logging.info(f开始爬取第 {page} 页) url base_url.format(page) html fetch_url(url, session) if not html: logging.warning(f第 {page} 页请求失败跳过) time.sleep(random.uniform(8, 12)) continue page_items parse_list_page(html, city) logging.info(f第 {page} 页解析到 {len(page_items)} 条房源) # 增量过滤ID不在库里才请求详情页 new_items [] for item in page_items: if item[house_id] in existing_ids: continue detail_html fetch_url(item[detail_url], session) if detail_html: detail_info parse_detail_page(detail_html, item[house_id]) item.update(detail_info) new_items.append(item) time.sleep(random.uniform(1.5, 3)) # 写入数据库批量提交 upsert_houses(cursor, page_items) conn.commit() existing_ids.update(i[house_id] for i in page_items if i[house_id]) save_page(page) time.sleep(random.uniform(3, 6)) cursor.close() conn.close() logging.info(本次采集完成) if __name__ __main__: main()这个主流程有几个设计点值得复盘。existing_ids集合是增量爬虫的过滤器每次爬完一页就更新它避免同一个ID在本次运行中重复抓详情页。conn.commit()放在每一页结束后也就是说一页的数据是一个事务单元即使程序在下一页挂掉最多丢一页的数据量。断点文件在每页完成后写入让整个爬虫可以随时中断、随时恢复。验证这套爬虫是否健康我会关注三个指标列表页的解析成功率和详情页的字段完整度、日志里有没有出现“验证码”关键字、入库后house_info表的记录总数是否与列表页的实际房源数量级相符。建议每次采集完成后跑一条SQL看今天的入库量SELECT city, COUNT(*) AS total_count, SUM(list_date CURDATE()) AS today_new FROM house_info GROUP BY city;如果today_new长期为零说明增量逻辑没有生效或者列表页ID和详情页ID不一致导致重复请求。最后说一个我自己的习惯爬虫跑通了只是第一步真正决定这个项目能不能长期用下去的是你对数据库表结构的维护意识和日志的意识——每次改动字段都要给迁移脚本留档每次采集异常都要能从日志里三分钟内定位到是网络问题、反爬拦截还是解析规则失效。如果你按这篇文章的模块划分去组织代码这个定位过程会非常快。数据管道稳定顺滑你后续做房价分析、小区对比、挂牌周期统计都只是在SQL层加查询的问题。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑