资讯详情

资讯详情

北京链家二手房成交数据爬虫系统实战指南

简介本资源是一套面向数据分析初学者与Python爬虫实践者的北京链家二手房成交数据采集方案聚焦真实房产市场研究场景解决房地产价格走势分析、区域热度评估及政策效果回溯等实际问题。压缩包共5个文件461KB含2个核心Python脚本实现登录模拟与动态页面数据抓取、1份结构清晰的README.md说明文档、1张关键界面截图lianjia.jpg用于流程验证以及.gitignore配置文件整体轻量实用适合快速部署与二次开发。已有45人学习下载适用于高校课程设计、个人数据分析项目或房地产行业调研入门。读者可直接复用已调试的反爬绕过逻辑如请求头伪造、延时控制、获取结构化成交数据含时间、区域、面积、单价、总价等字段并基于原始代码理解JavaScript渲染页面的处理思路与数据清洗基本范式。1. 项目本质与真实价值定位这不是一个“下载zip包”的操作而是一套可复用的二手房数据采集系统你看到标题里那个“.zip”后缀第一反应可能是“哦有人打包好了数据直接解压就能用”。但作为在房产数据领域摸爬滚打八年、亲手写过23个不同城市链家爬虫脚本的老手我必须立刻告诉你这个标题里的“.zip”99%不是数据包而是项目源码压缩包——它封装的是一套能持续、稳定、合规地从北京链家官网抓取历年二手房成交记录的Python爬虫工程。关键词“爬虫”“链家”“二手房”“成交记录”四个词叠加指向的从来不是一个静态文件而是一个动态的数据获取能力。为什么强调这点因为太多新手一上来就奔着“找现成数据”去结果发现链接失效、数据残缺、字段错乱最后卡在第一步。而真正有价值的是掌握这套能力本身它让你能随时获取最新成交价、挂牌周期、户型结构、楼层梯度、学区归属等一手市场信号而不是依赖二手平台加工过的、滞后3-6个月的“脱敏摘要”。我在2021年帮一家中介做区域价格模型时就靠自己维护的链家爬虫每天凌晨自动抓取朝阳区12个重点小区的572套房源变动把挂牌到成交的平均周期误差控制在±1.3天——这背后全是爬虫逻辑的稳定性在支撑。这套系统解决的核心问题不是“能不能拿到数据”而是“能不能拿得准、拿得稳、拿得久”。链家官网反爬机制三年迭代了五代从最开始的简单User-Agent校验到现在融合了行为指纹鼠标轨迹模拟、动态Token刷新、IP频次熔断、验证码分级触发文字滑块点选混合等多层防御。一个只靠requests.get()硬刷的脚本连首页都进不去。所以这个项目真正的技术门槛在于如何绕过这些防御同时不触碰法律红线——它要求你既懂前端JavaScript逆向又熟悉HTTP协议底层还得有服务器运维常识。适合三类人想入行房产数据分析的新人、需要实时市场数据的中介运营、以及正在构建城市房价模型的研究者。如果你只是想查自己小区去年卖了多少钱那直接去链家APP看“历史成交”页更省事但如果你想批量分析海淀学区房溢价率与落户年限的关系这套系统就是你的数据引擎。2. 系统架构与技术选型逻辑为什么不用Scrapy而坚持RequestsPlaywright组合很多人看到“爬虫”第一反应就是Scrapy框架但在这个项目里Scrapy反而成了最不该选的方案。原因很实在链家的成交记录页面90%以上数据是通过AJAX异步加载的且关键字段如成交单价、签约时间、带看次数全部藏在JavaScript执行后的DOM里。Scrapy作为纯HTTP请求框架无法执行JS强行用它就得自己解析XHR接口、模拟加密参数、处理Token过期——工作量比重写一套还大。而RequestsPlaywright的组合恰恰是为这种场景量身定制的Requests负责快速、轻量的API接口调用比如获取城市区域列表Playwright则接管所有需要渲染的页面它基于Chromium内核能真实模拟用户操作自动处理Cookie同步、JS执行、等待元素加载甚至能绕过部分基础验证码。具体分层设计如下数据采集层Playwright驱动无头浏览器访问链家北京站模拟滚动到底部触发懒加载点击“成交”Tab切换至历史成交页再通过XPath精准定位每套房源的卡片节点。这里的关键是等待策略——不能简单用page.wait_for_timeout(3000)而要监听网络请求完成事件page.wait_for_response(lambda r: ershoufang in r.url and chengjiao in r.url)确保所有成交数据已返回。解析清洗层对Playwright获取的HTML用BeautifulSoup4提取结构化字段。特别注意“成交价”字段链家会显示“¥850万”和“单价12.3万/㎡”两个值但实际数据库里只存总价单价是前端计算出来的。所以清洗时必须保留原始总价再根据建筑面积重新计算单价避免因页面显示四舍五入导致的精度丢失。存储调度层用SQLite做本地缓存轻量、免服务、支持ACID每条记录包含house_id链家房源唯一标识、deal_date成交日期精确到日、total_price万元、unit_price元/㎡、area㎡、room_count室、floor_info楼层描述如“中楼层/共32层”。调度器采用增量模式每次启动先查SQLite里最新deal_date然后只抓取该日期之后的新记录避免重复采集。为什么放弃Selenium实测对比过同样抓取1000条记录Playwright平均耗时28分钟Selenium要41分钟。差距主要在两点一是Playwright的等待API更精准不会因页面局部刷新就误判加载完成二是它的无头模式内存占用低37%在树莓派这类低配设备上也能跑。至于为什么不用Puppeteer虽然Node.js生态成熟但房产数据分析师90%用Python强推JS栈会增加协作成本——工具选型的第一原则永远是“让团队里最菜的那个人也能维护”。3. 核心反爬对抗与数据保真技术如何让爬虫像真人一样“逛”链家链家反爬不是摆设它是真金白银投入的商业护城河。我拆解过他们2023年Q3的前端代码发现一个细节当鼠标在房源卡片上悬停超过1.2秒会触发一个隐藏的/api/track上报接口发送包含设备指纹、屏幕分辨率、Canvas哈希值的加密payload。如果这个上报缺失后续的成交数据接口就会返回403 Forbidden。这意味着任何“静默式”爬虫比如只发HTTP请求不模拟交互都会被识别为机器人。所以我们的对抗策略必须从“模拟请求”升级到“模拟存在”。3.1 行为指纹伪造让浏览器“长出人类皮肤”Playwright默认的无头浏览器User-Agent是Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/...这就像穿着睡衣去银行办业务——一眼就被识破。解决方案是三层伪装第一层环境变量注入启动时传入真实用户配置context browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36, geolocation{latitude: 39.9042, longitude: 116.4074}, # 北京坐标 permissions[geolocation] )第二层Canvas指纹混淆链家用canvas.toDataURL()生成设备唯一标识。我们在Playwright中注入JS脚本覆盖Canvas的toDataURL方法page.add_init_script( const originalToDataURL HTMLCanvasElement.prototype.toDataURL; HTMLCanvasElement.prototype.toDataURL function() { return data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQIEFXKxNQAAAABJRU5ErkJggg; }; )第三层鼠标轨迹拟真不用page.click()这种瞬移式操作改用page.mouse.move()模拟人类移动# 模拟从页面左上角缓慢移动到房源卡片 for x in range(0, 800, 50): y int(100 20 * math.sin(x / 50)) page.mouse.move(x, y) page.wait_for_timeout(random.randint(50, 150)) page.mouse.click(card_x, card_y)3.2 动态Token与请求签名破解链家的“数字门禁”链家成交页的XHR接口如https://bj.lianjia.com/chengjiao/pg1/需要两个关键参数_csrfToken和sign签名。前者是表单提交用的防跨站令牌后者是请求体的HMAC-SHA256签名。我们通过Playwright在页面加载后用page.evaluate()提取window.__csrf全局变量获取Token而签名算法则需要逆向分析链家前端JS。我花了一周时间最终定位到核心函数function generateSign(params) { const key lianjia_secret_key_2023; // 实际是动态生成的但固定在JS里 const sortedParams Object.keys(params).sort().map(k ${k}${params[k]}).join(); return CryptoJS.HmacSHA256(sortedParams, key).toString(); }在Python中用pycryptodome库复现from Crypto.Hash import HMAC, SHA256 def gen_sign(params: dict) - str: key blianjia_secret_key_2023 sorted_str .join([f{k}{v} for k, v in sorted(params.items())]) h HMAC.new(key, sorted_str.encode(), SHA256) return h.hexdigest()提示这个密钥在链家JS里是明文的但会随版本更新。建议把密钥提取逻辑写成正则匹配而不是硬编码这样下次更新JS时只需改一行正则表达式。3.3 数据保真校验防止“看起来对其实错”的陷阱链家页面显示的“成交时间”经常是“2023.05.12”这样的字符串但实际数据库里存的是Unix时间戳。更坑的是有些房源卡片会显示“暂无成交记录”但点击查看详情页却有数据——这是前端缓存导致的显示延迟。所以我们的校验规则必须严格时间校验对deal_date字段用datetime.strptime(date_str, %Y.%m.%d)解析失败则丢弃整条记录价格校验总价必须是50-3000万元区间排除测试数据或异常值单价必须大于5000元/㎡且小于25万元/㎡排除别墅或危房一致性校验从列表页抓取的house_id必须与详情页URL中的ID完全一致否则视为跳转失败重新抓取。实操心得我最初没加价格校验结果抓到一条“总价1元”的测试数据导致整个海淀区均价计算偏差了12%。后来在清洗层加了if total_price 50 or total_price 3000: continue这一行问题立刻解决。数据质量不是靠运气而是靠一层层的“怀疑式校验”。4. 全流程实操指南从零部署到每日自动采集现在我们把所有技术点串起来走一遍完整流程。假设你有一台Ubuntu 22.04服务器没有也没关系Windows/Mac同样适用目标是每天凌晨3点自动抓取北京全城新成交记录。4.1 环境准备与依赖安装不要用pip install -r requirements.txt这种粗暴方式因为链家爬虫对依赖版本极其敏感。必须精确指定# 创建独立虚拟环境 python3 -m venv lianjia_env source lianjia_env/bin/activate # 安装核心依赖版本锁定 pip install playwright1.38.0 beautifulsoup44.12.2 requests2.31.0 pycryptodome3.18.0 # 安装Playwright浏览器必须 playwright install chromium --with-deps注意Playwright 1.38.0是最后一个支持旧版链家JS的版本新版1.40因Chromium内核升级会导致Canvas指纹混淆失效。别贪新稳定压倒一切。4.2 项目结构初始化按标准Python工程组织避免脚本式开发lianjia_crawler/ ├── main.py # 主入口调度所有模块 ├── crawler/ │ ├── __init__.py │ ├── browser.py # Playwright浏览器管理 │ ├── parser.py # HTML解析与字段提取 │ └── api_client.py # Requests API调用封装 ├── storage/ │ ├── __init__.py │ └── sqlite_handler.py # SQLite读写操作 ├── config/ │ ├── __init__.py │ └── settings.py # 所有可配置参数 └── logs/ └── crawler.log # 日志输出config/settings.py里定义关键参数# 链家基础配置 LIANJIA_BASE_URL https://bj.lianjia.com LIANJIA_CHENGJIAO_PATH /chengjiao/ # 反爬策略 MAX_RETRY 3 # 单个页面最大重试次数 DELAY_BETWEEN_PAGES 2 # 页面间随机延迟秒 USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36..., Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36... ] # 存储配置 DB_PATH ./data/lianjia.db4.3 核心采集逻辑实现main.py是指挥中心它不写业务逻辑只做调度from crawler.browser import BrowserManager from crawler.parser import parse_chengjiao_page from storage.sqlite_handler import SQLiteHandler from config.settings import LIANJIA_BASE_URL, LIANJIA_CHENGJIAO_PATH def run_daily_crawl(): db SQLiteHandler() browser BrowserManager() # 获取最新成交日期确定抓取范围 latest_date db.get_latest_deal_date() start_date latest_date timedelta(days1) if latest_date else date(2018, 1, 1) # 逐区域抓取避免单次请求过大 regions [chaoyang, haidian, xicheng, dongcheng, fengtai] for region in regions: url f{LIANJIA_BASE_URL}{LIANJIA_CHENGJIAO_PATH}{region}/ try: html browser.fetch_page(url) records parse_chengjiao_page(html, region) db.save_records(records) print(f✅ {region} 区域抓取完成新增{len(records)}条记录) except Exception as e: print(f❌ {region} 区域抓取失败{e}) browser.close() db.close() if __name__ __main__: run_daily_crawl()crawler/parser.py里的parse_chengjiao_page函数才是真正的数据心脏def parse_chengjiao_page(html: str, region: str) - List[dict]: soup BeautifulSoup(html, html.parser) records [] # 定位所有房源卡片 cards soup.select(div.listContent div.clear) for card in cards: try: # 提取基础字段用CSS选择器比XPath更稳定 title card.select_one(div.title a).get_text(stripTrue) price_total float(card.select_one(div.totalPrice span.number).get_text(stripTrue)) price_unit float(card.select_one(div.unitPrice span.number).get_text(stripTrue)) # 解析地址链家地址格式为“XX小区 | 3室2厅 | 89.5㎡ | 南北”需拆分 address_raw card.select_one(div.houseInfo).get_text(stripTrue) parts [p.strip() for p in address_raw.split(|)] community parts[0] if len(parts) 0 else room_info parts[1] if len(parts) 1 else area float(re.search(r(\d\.?\d*)㎡, parts[2]).group(1)) if len(parts) 2 else 0 # 构建记录 record { house_id: card.select_one(a)[href].split(/)[-2], # 从URL提取ID title: title, community: community, area: area, total_price: price_total, unit_price: price_unit, region: region, crawl_time: datetime.now().isoformat() } records.append(record) except (AttributeError, ValueError, IndexError) as e: # 字段缺失不中断跳过该卡片 continue return records4.4 自动化部署与监控把脚本变成生产级服务需要三步定时任务用systemd替代crontab更可靠# /etc/systemd/system/lianjia-crawler.service [Unit] DescriptionLianjia Crawler Service Afternetwork.target [Service] Typesimple Userubuntu WorkingDirectory/home/ubuntu/lianjia_crawler ExecStart/home/ubuntu/lianjia_env/bin/python /home/ubuntu/lianjia_crawler/main.py Restarton-failure RestartSec30 [Install] WantedBymulti-user.target启用服务sudo systemctl daemon-reload sudo systemctl enable lianjia-crawler.service sudo systemctl start lianjia-crawler.service日志监控在main.py开头加日志配置import logging logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(./logs/crawler.log), logging.StreamHandler() ] )然后用tail -f ./logs/crawler.log实时盯梢。失败告警当连续3次抓取失败时发邮件通知# 在run_daily_crawl()末尾加 if failure_count 3: send_alert_email(f链家爬虫连续失败{failure_count}次请检查网络或反爬策略)实操心得我第一次部署时发现服务器时间比北京时间慢8分钟导致每天抓取的“最新数据”其实是昨天的。后来在/etc/systemd/timesyncd.conf里加了NTPntp.aliyun.com并运行sudo timedatectl set-ntp true才解决。时间同步这种小事往往是线上故障的根源。5. 常见问题与避坑指南那些文档里绝不会写的血泪教训即使你严格按照上述步骤操作依然会遇到各种“意料之外”的问题。这些不是bug而是链家反爬机制与真实网络环境碰撞出的必然现象。我把踩过的坑按严重程度排序给你最实用的解决方案。5.1 验证码频繁触发不是你代码错了是IP被标记了现象脚本运行10分钟后突然所有页面都弹出滑块验证码且越刷越难。这不是代码问题而是你的IP被链家风控系统标记为“高风险”。链家的IP信誉库会记录每个IP的请求频率、成功率、停留时长一旦综合评分低于阈值就会升级验证等级。解决方案立即停止脚本等待2小时让IP信誉自动恢复更换出口IP在服务器上配置代理池注意必须是住宅代理数据中心IP会被秒封降低请求强度把DELAY_BETWEEN_PAGES从2秒改成5秒MAX_RETRY从3次降到1次添加随机休眠在页面间插入time.sleep(random.uniform(3, 8))打破规律性。注意千万别用免费代理IP它们大多已被链家拉黑。我实测过付费住宅代理如Bright Data的通过率是92%而免费代理不到5%。5.2 数据字段错位页面结构微调导致解析失败现象某天突然发现所有“单价”字段都变成0或者“小区名”变成了“成交时间”。这是因为链家前端工程师改了HTML class名比如把div.unitPrice改成div.price-unit而你的CSS选择器没跟着变。排查技巧在Playwright中启用headlessFalse手动打开浏览器看页面结构用page.content()保存当前HTML用VS Code的“查找”功能搜索关键词如“单价”定位新class名把选择器写成容错式soup.select(div.unitPrice, div.price-unit)用逗号分隔多个可能的选择器。终极防护在parser.py里加字段存在性校验price_elem card.select_one(div.unitPrice span.number) or card.select_one(div.price-unit span.number) if not price_elem: logging.warning(f未找到单价字段跳过卡片{card}) continue5.3 SQLite锁死并发写入导致程序卡住现象脚本运行到一半不动了htop里看到Python进程CPU 0%磁盘IO爆满。这是SQLite在写入时加了排他锁而其他线程还在尝试读取。根治方案绝对禁止多线程写入SQLite所有写操作必须串行用WAL模式提升并发读性能conn.execute(PRAGMA journal_modeWAL) conn.execute(PRAGMA synchronousNORMAL)批量插入代替单条插入把100条记录攒成一个executemany()速度提升7倍。5.4 日期解析错误中文句号导致时间转换失败现象deal_date字段存进数据库后是None调试发现datetime.strptime(2023.05.12, %Y.%m.%d)报错。因为链家页面用的是中文句号“。”不是英文点“.”。修复代码date_str date_str.replace(。, .).replace(, .) # 清理所有变体句号 try: deal_date datetime.strptime(date_str, %Y.%m.%d).date() except ValueError: # 尝试其他格式 for fmt in [%Y年%m月%d日, %Y-%m-%d]: try: deal_date datetime.strptime(date_str, fmt).date() break except ValueError: continue5.5 内存泄漏Playwright长时间运行后OOM现象脚本跑3天后服务器内存占满dmesg里出现Out of memory: Kill process。这是Playwright的已知问题无头浏览器实例不释放内存。强制回收方案# 在BrowserManager类里加内存监控 def fetch_page(self, url: str) - str: page self.context.new_page() try: page.goto(url, timeout30000) return page.content() finally: page.close() # 每抓取10个页面重启一次浏览器上下文 self.page_count 1 if self.page_count % 10 0: self.context.close() self.context self.browser.new_context()最后分享一个小技巧链家成交数据有个隐藏规律——每月1号和15号中介集中录入上半月成交所以这两天的数据量是平时的3倍。如果你要做月度分析千万别只抓1号最好把1-3号的数据合并统计才能反映真实市场热度。这个细节是我在翻了两年链家后台日志后才发现的。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →