1. 搜索页采集的核心价值与挑战
搜索页采集是爬虫工程师的必修课,也是数据获取的重要入口。相比固定URL的页面抓取,搜索采集能动态获取最新内容,特别适合以下场景:
- 竞品监控(跟踪对手新品/价格变动)
- 舆情监测(捕捉特定关键词的讨论)
- 学术研究(收集领域内最新论文)
- 商品比价(聚合多平台商品信息)
但搜索采集面临三个典型难题:
- 关键词管理:如何高效组织海量搜索词?如何避免重复搜索?
- 结果去重:同一内容被不同关键词命中时,如何避免重复存储?
- 反爬对抗:搜索接口通常有严格频率限制,如何稳定长期运行?
2. 五步系统化解法
2.1 关键词队列设计
核心思路是将待搜索词放入队列管理,推荐使用Redis的List或Sorted Set结构:
python复制import redis
r = redis.Redis()
# 添加关键词
r.lpush('search:keywords', 'Python爬虫', '反爬策略', '数据清洗')
# 获取待处理关键词
keyword = r.rpop('search:keywords')
状态机设计:
- 待处理(pending)
- 处理中(processing)
- 已完成(completed)
- 失败(failed)
关键技巧:用Redis的HSET记录关键词状态,避免重复处理:
python复制r.hset('search:status', 'Python爬虫', 'processing')
2.2 搜索请求封装
不同网站的搜索接口差异很大,需要抽象通用请求模板:
python复制def build_search_url(keyword, page=1):
return f"https://example.com/search?q={keyword}&page={page}"
headers = {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json'
}
def make_search_request(url):
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
log_error(f"搜索失败: {url} - {str(e)}")
return None
2.3 结果去重设计
去重关键在于设计唯一标识符,常用方案:
- 内容哈希:对标题+正文取MD5
- 平台ID:优先使用平台提供的唯一ID
- 复合键:URL+发布时间戳的组合
python复制def get_content_fingerprint(item):
# 优先使用平台ID
if item.get('id'):
return f"id_{item['id']}"
# 次选URL+发布时间
if item.get('url') and item.get('publish_time'):
return f"url_{item['url']}_time_{item['publish_time']}"
# 最后使用内容哈希
return hashlib.md5(
(item['title'] + item['content']).encode()
).hexdigest()
2.4 限速与退避策略
搜索接口通常有严格QPS限制,建议:
- 基础延迟:请求间固定间隔(如2秒)
- 动态退避:遇到429状态码时指数级增加延迟
- 随机抖动:添加±0.5秒随机值避免规律性访问
python复制import random
import time
base_delay = 2.0
max_delay = 60.0
def adaptive_delay(last_response=None):
if last_response and last_response.status_code == 429:
current_delay = min(base_delay * 2 ** retry_count, max_delay)
else:
current_delay = base_delay
time.sleep(current_delay + random.uniform(-0.5, 0.5))
2.5 失败处理与死信队列
对于持续失败的关键词,应移入死信队列人工检查:
python复制dead_letter_queue = 'search:dead_letters'
def handle_failed_keyword(keyword, error):
r.hset('search:errors', keyword, str(error))
r.lpush(dead_letter_queue, keyword)
r.hset('search:status', keyword, 'failed')
3. 完整工程化实现
3.1 系统架构设计
code复制┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 关键词管理 │───>│ 搜索采集器 │───>│ 结果处理器 │
└─────────────┘ └─────────────┘ └─────────────┘
↑ ↓ ↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 死信队列 │ │ 代理/IP池 │ │ 数据存储 │
└─────────────┘ └─────────────┘ └─────────────┘
3.2 核心代码实现
python复制class SearchSpider:
def __init__(self):
self.redis = redis.Redis()
self.session = requests.Session()
self.session.headers.update({'User-Agent': 'Mozilla/5.0'})
def run(self):
while True:
keyword = self.get_next_keyword()
if not keyword:
break
self.process_keyword(keyword)
def process_keyword(self, keyword):
try:
self.mark_processing(keyword)
page = 1
while True:
results = self.search_page(keyword, page)
if not results:
break
self.process_results(keyword, results)
page += 1
self.mark_completed(keyword)
except Exception as e:
self.handle_error(keyword, e)
3.3 去重存储方案
使用Redis的Set实现内存去重,MySQL持久化存储:
python复制def save_results(keyword, results):
for item in results:
fingerprint = get_content_fingerprint(item)
# Redis去重判断
if not self.redis.sismember('search:fingerprints', fingerprint):
self.redis.sadd('search:fingerprints', fingerprint)
# MySQL存储
db.execute("""
INSERT INTO search_results
(fingerprint, title, content, url, found_by)
VALUES (%s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
found_by = CONCAT(found_by, ',', VALUES(found_by))
""", (fingerprint, item['title'],
item['content'], item['url'], keyword))
4. 反爬对抗实战策略
4.1 User-Agent轮换
准备常见UA列表随机选择:
python复制USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X)'
]
def get_random_ua():
return random.choice(USER_AGENTS)
4.2 验证码处理方案
- 简单验证码:使用Tesseract OCR识别
- 复杂验证码:接入打码平台
- 终极方案:触发验证码后暂停任务并报警
python复制def handle_captcha(response):
if 'captcha' in response.text:
save_captcha_image(response)
alert_admin()
raise CaptchaEncounteredError()
4.3 IP代理池集成
推荐使用住宅代理服务,实现IP自动切换:
python复制PROXY_POOL = [
'http://user:pass@proxy1.example.com:8080',
'http://user:pass@proxy2.example.com:8080'
]
def get_proxy():
return {'http': random.choice(PROXY_POOL)}
5. 实战经验与避坑指南
5.1 搜索结果翻页判断
常见终止条件:
- 返回结果为空
- 出现"没有更多结果"提示
- 连续3页结果重复率>80%
- 页码超过最大限制(通常50页)
python复制def should_stop_paging(results, prev_results):
if not results:
return True
if len(results) < 10: # 结果数量显著减少
return True
if prev_results and similarity(results, prev_results) > 0.8:
return True
return False
5.2 特殊字符处理
对搜索词进行URL安全编码:
python复制from urllib.parse import quote
safe_keyword = quote('C++爬虫#高级技巧')
5.3 增量采集优化
记录最后采集时间,只获取新增内容:
python复制def get_incremental_results(keyword):
last_crawl_time = db.execute(
"SELECT MAX(created_at) FROM results WHERE keyword = %s",
(keyword,)
).fetchone()[0]
params = {'q': keyword}
if last_crawl_time:
params['after'] = last_crawl_time.isoformat()
return search_api(params)
6. 性能优化进阶技巧
6.1 关键词自动扩展
使用同义词库扩展搜索范围:
python复制SYNONYMS = {
'爬虫': ['spider', 'crawler', 'scraper'],
'Python': ['Py', 'python3']
}
def expand_keywords(keyword):
words = keyword.split()
expanded = [keyword]
for i, word in enumerate(words):
if word.lower() in SYNONYMS:
for syn in SYNONYMS[word.lower()]:
new_words = words.copy()
new_words[i] = syn
expanded.append(' '.join(new_words))
return expanded
6.2 搜索结果质量评分
基于以下指标评估结果质量:
- 关键词匹配度(标题/正文中出现频率)
- 内容长度(过短可能是低质内容)
- 来源权威性(域名权重)
- 新鲜度(发布时间)
python复制def quality_score(item, keyword):
score = 0
# 标题匹配
score += item['title'].lower().count(keyword.lower()) * 10
# 内容长度
score += min(len(item['content']) / 1000, 10)
# 新鲜度(最近3天内)
if item['publish_time'] > datetime.now() - timedelta(days=3):
score += 5
return score
6.3 分布式采集架构
使用Celery实现分布式任务队列:
python复制from celery import Celery
app = Celery('search_spider', broker='redis://localhost:6379/0')
@app.task
def process_keyword_task(keyword):
spider = SearchSpider()
spider.process_keyword(keyword)
部署方案:
- 1个主节点负责关键词管理
- N个工作节点执行采集任务
- Redis作为中央存储和消息队列
- MySQL集群持久化数据