1. 项目背景与核心价值
最近在帮朋友分析某城市二手房市场时,发现手动收集数据效率太低。作为技术从业者,自然想到用爬虫自动化解决这个问题。Q房网作为国内头部房产平台,其二手房数据具有典型性和参考价值。这个项目就是通过Python技术栈实现对Q房网二手房数据的自动化采集。
从技术角度看,这个爬虫需要解决几个关键问题:如何绕过网站反爬机制、如何高效解析动态加载内容、如何设计合理的存储结构。从业务角度看,采集到的数据可以用于价格趋势分析、房源特征挖掘、区域价值评估等场景。相比人工收集,自动化爬虫能将数据采集效率提升数十倍。
2. 技术方案设计
2.1 整体架构设计
爬虫系统采用经典的三层架构:
- 数据采集层:负责页面请求和响应获取
- 数据处理层:负责页面解析和数据清洗
- 数据存储层:负责结构化数据持久化
具体技术选型如下:
- 请求库:Requests + aiohttp(异步提升效率)
- 解析库:BeautifulSoup + PyQuery(互补使用)
- 反反爬方案:随机UA + 代理IP池 + 请求频率控制
- 存储方案:MySQL + CSV双备份
2.2 关键问题解决方案
对于动态加载内容,通过分析XHR请求发现,房源列表采用分页接口:
/api/house/list?page=2&size=20使用Postman测试确认接口返回规范的JSON数据,这比解析DOM更可靠。
对于反爬机制,实测发现Q房网主要采用以下防护措施:
- Cookie验证(特别是__jsluid_h参数)
- 请求频率检测(单IP超过10次/分钟会触发验证)
- 行为特征检测(如鼠标移动轨迹)
我们的应对策略:
- 使用selenium模拟真人操作获取初始Cookie
- 搭建代理IP池(实测需要至少50个可用IP)
- 随机化请求间隔(3-8秒浮动)
3. 核心代码实现
3.1 请求模块封装
import random import asyncio from aiohttp import TCPConnector class RequestHandler: def __init__(self): self.proxy_pool = self._init_proxies() self.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/605.1.15' ] async def fetch(self, url, session): proxy = random.choice(self.proxy_pool) headers = { 'User-Agent': random.choice(self.user_agents), 'Referer': 'https://www.qfang.com' } try: async with session.get(url, proxy=proxy, headers=headers) as response: await asyncio.sleep(random.uniform(3, 8)) return await response.text() except Exception as e: print(f"Request failed: {str(e)}") return None3.2 数据解析模块
房源详情页需要提取20+个字段,包括:
- 基础信息:标题、总价、单价、面积
- 位置信息:小区、区域、地铁、坐标
- 房屋特征:朝向、楼层、装修、年代
- 交易属性:挂牌时间、带看次数
使用XPath和CSS选择器混合提取:
def parse_detail(html): doc = PyQuery(html) item = {} # 基础信息 item['title'] = doc('.house-title h1').text() item['total_price'] = doc('.price-total').text().replace('万', '') item['unit_price'] = doc('.price-unit').text().replace('元/㎡', '') # 使用更稳健的XPath提取复杂字段 item['build_year'] = html.xpath('//li[contains(text(),"建筑年代")]/span/text()')[0] item['orientation'] = html.xpath('//li[contains(text(),"房屋朝向")]/span/text()')[0] # 处理特殊字段 subway_info = doc('.subway-info').text() item['subway_distance'] = re.search(r'距离(\d+)号线', subway_info).group(1) return item4. 存储方案优化
4.1 数据库设计
创建包含所有字段的houses表:
CREATE TABLE `houses` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(100) DEFAULT NULL, `total_price` decimal(10,2) DEFAULT NULL, `unit_price` decimal(10,2) DEFAULT NULL, `area` decimal(6,2) DEFAULT NULL, `community` varchar(50) DEFAULT NULL, `district` varchar(20) DEFAULT NULL, `subway` varchar(30) DEFAULT NULL, `build_year` year(4) DEFAULT NULL, `orientation` varchar(10) DEFAULT NULL, `floor` varchar(20) DEFAULT NULL, `layout` varchar(15) DEFAULT NULL, `listing_date` date DEFAULT NULL, `view_count` int(11) DEFAULT NULL, `url` varchar(255) DEFAULT NULL, `crawl_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_district` (`district`), KEY `idx_price` (`unit_price`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb44.2 批量插入优化
使用executemany配合事务提升写入性能:
def batch_insert(conn, items): sql = """INSERT INTO houses(title,total_price,...,url) VALUES(%s,%s,...,%s)""" try: with conn.cursor() as cursor: cursor.executemany(sql, [ (item['title'], item['total_price'], ..., item['url']) for item in items ]) conn.commit() except Exception as e: conn.rollback() print(f"Insert failed: {str(e)}")5. 反爬对抗实战经验
5.1 Cookie维护策略
通过实测发现Q房网的Cookie有效期约2小时,需要定时刷新。采用以下方案:
- 使用selenium获取初始Cookie
- 通过requests.Session保持会话
- 每90分钟重新获取一次Cookie
关键代码:
def get_cookie(): driver = webdriver.Chrome() driver.get('https://www.qfang.com') time.sleep(5) # 等待登录完成 cookies = driver.get_cookies() driver.quit() return {c['name']: c['value'] for c in cookies} def refresh_cookie(): while True: new_cookie = get_cookie() session.cookies.update(new_cookie) time.sleep(5400) # 90分钟5.2 请求特征模拟
浏览器指纹检测是难点,需要模拟真实用户行为:
- 添加合理的请求延迟(3-8秒随机)
- 模拟页面跳转路径(先列表页再详情页)
- 随机化鼠标滚动事件
- 添加合理的Referer链
6. 数据质量保障
6.1 异常数据处理
常见数据问题及处理方案:
- 价格异常值:过滤单价<5000或>200000的记录
- 面积异常:过滤<10㎡或>500㎡的记录
- 缺失字段:对关键字段(如总价)设置非空约束
- 格式不一致:统一面积单位(㎡)、价格单位(万)
清洗代码示例:
def clean_data(item): # 统一面积格式 if '㎡' in item['area']: item['area'] = item['area'].replace('㎡', '') # 过滤异常值 try: if float(item['unit_price']) < 5000: return None except: return None return item6.2 去重方案设计
采用URL+挂牌时间作为唯一标识:
ALTER TABLE houses ADD UNIQUE INDEX `idx_unique` (`url`, `listing_date`);7. 部署与调度
7.1 分布式架构
使用Redis作为任务队列:
# 列表页任务队列 qfang:start_urls = [ 'https://www.qfang.com/shenzhen/sale?page=1', ... ] # 详情页任务队列 qfang:detail_urls = []7.2 定时调度
使用APScheduler实现定时采集:
from apscheduler.schedulers.blocking import BlockingScheduler scheduler = BlockingScheduler() @scheduler.scheduled_job('cron', hour=2) # 每天凌晨2点执行 def daily_task(): spider = QfangSpider() spider.crawl() scheduler.start()8. 数据分析应用示例
采集到的数据可用于多种分析场景:
8.1 价格分布分析
import pandas as pd df = pd.read_sql("SELECT district, unit_price FROM houses", conn) # 按行政区划分析 district_stats = df.groupby('district')['unit_price'].agg(['mean', 'count']) print(district_stats.sort_values('mean', ascending=False))8.2 房源特征关联分析
# 装修与价格关系 sns.boxplot(x='decoration', y='unit_price', data=df) plt.title('不同装修等级的价格分布') plt.show()9. 法律合规要点
在开发此类爬虫时需特别注意:
- 严格遵守robots.txt协议(实测Q房网允许部分路径爬取)
- 控制请求频率,不影响网站正常运营
- 不爬取用户隐私信息(如联系方式)
- 数据仅用于个人研究,不进行商业牟利
- 在headers中声明爬虫身份
10. 性能优化记录
经过多次迭代,关键性能指标对比:
| 版本 | 请求间隔(s) | 成功率 | 日均数据量 | CPU占用 |
|---|---|---|---|---|
| v1.0 | 1 | 38% | 2,000 | 85% |
| v2.0 | 3-8随机 | 72% | 5,000 | 45% |
| v3.0 | 代理IP轮换 | 95% | 15,000 | 60% |
最终采用多级缓存策略:
- 本地缓存已爬取URL(有效期7天)
- Redis缓存列表页HTML(有效期1小时)
- 内存缓存详情页请求(LRU算法)
11. 踩坑实录
11.1 动态参数陷阱
某次更新后发现爬虫突然失效,排查发现网站新增了动态参数:
/api/house/list?page=2&_t=1234567890解决方案是提取响应中的时间戳参数回传。
11.2 字体反爬破解
价格信息显示异常,检查发现使用了自定义字体:
@font-face { font-family: 'qfang-price'; src: url('/fonts/price.woff'); }通过FontTools解析woff字体文件,建立字符映射关系表。
12. 项目演进方向
- 增加房源图片下载功能
- 集成自动价格预警系统
- 开发可视化分析Dashboard
- 支持更多房产平台数据源
- 实现自动化特征工程管道
这个项目让我深刻体会到,一个健壮的爬虫系统需要考虑的远不止数据采集本身。从反爬对抗到数据清洗,从性能优化到法律合规,每个环节都需要精心设计。特别是在高强度的反爬环境下,如何平衡效率和稳定性是个持续优化的过程。