1. 项目背景与核心挑战
去年在做一个日本东京塑料展的爬虫项目时,遇到了几个特别棘手的技术问题。这个展会官网的结构相当复杂,数据分散在多个子页面,而且反爬措施严密。经过两周的攻坚,我们最终解决了相对路径拼接、TEL前缀清洗、多链接过滤和毫秒级延迟控制这四大技术难题。今天就把这些实战经验分享给大家,特别是做国际展会数据采集的朋友们可能会遇到类似问题。
这个项目的主要目标是采集参展商名录、产品信息和联系方式。目标网站采用了动态加载、参数加密等多种反爬手段,其中最让人头疼的就是URL处理和数据清洗的问题。下面我就按解决顺序,逐一拆解每个技术难点的突破过程。
2. 相对路径拼接的标准化方案
2.1 问题现象分析
目标网站的链接系统非常混乱,同一个页面的链接可能同时存在以下几种形式:
- 绝对路径:
https://example.com/exhibitors/123 - 根相对路径:
/exhibitors/123 - 相对路径:
../exhibitors/123 - 协议相对路径:
//example.com/exhibitors/123
2.2 解决方案实现
我们最终采用Python的urllib.parse库进行标准化处理:
python复制from urllib.parse import urljoin, urlparse
def normalize_url(base_url, link):
# 处理空链接和JavaScript伪链接
if not link or link.startswith(('javascript:', 'mailto:')):
return None
# 统一处理协议相对路径
if link.startswith('//'):
link = 'https:' + link
# 使用urljoin进行智能拼接
normalized = urljoin(base_url, link)
# 验证URL有效性
parsed = urlparse(normalized)
if not all([parsed.scheme, parsed.netloc]):
return None
return normalized
2.3 关键注意事项
- 一定要先处理特殊协议(如javascript:)再拼接,否则会产生无效URL
- 对于分页参数,需要保留原始查询字符串的顺序敏感性
- 遇到../等相对路径时,要确保base_url以/结尾
- 国际网站特别注意编码问题,建议统一转为UTF-8处理
3. 日本电话号码的清洗规范
3.1 日本电话格式特点
日本电话号码通常有以下几种表示方式:
- 国内格式:03-1234-5678
- 国际格式:+81-3-1234-5678
- 带TEL前缀:TEL: 03-1234-5678
- 分机号:03-1234-5678(代)
3.2 正则表达式方案
我们开发了多级清洗策略:
python复制import re
def clean_phone(phone_str):
# 第一步:去除TEL等前缀
phone_str = re.sub(r'^(TEL|電話|Tel)[:\s]*', '', phone_str.strip())
# 第二步:标准化分隔符
phone_str = re.sub(r'[-―ー‐]', '-', phone_str)
# 第三步:处理国际区号
if phone_str.startswith('0'):
phone_str = '+81' + phone_str[1:]
elif phone_str.startswith('81'):
phone_str = '+' + phone_str
# 第四步:移除分机信息
phone_str = re.sub(r'\(.*?\)', '', phone_str)
return phone_str.strip('-')
3.3 特殊案例处理
- 免费电话:0120开头的号码需要保留原始格式
- 手机号码:090/080/070开头的11位号码
- 企业总机:常带有代表字样,需要特殊标记
- 多号码情况:用分号分隔多个号码时保持原始顺序
4. 多链接过滤的智能去重
4.1 去重维度设计
我们建立了五层过滤机制:
- URL标准化去重(解决参数顺序问题)
- 正文相似度去重(防止内容重复)
- 时间戳过滤(排除过期页面)
- 关键元素比对(如公司ID识别)
- 人工规则白名单(特殊页面保留)
4.2 布隆过滤器实现
针对海量URL去重,采用布隆过滤器+Redis的方案:
python复制from pybloom_live import ScalableBloomFilter
import redis
class URLFilter:
def __init__(self):
self.redis = redis.StrictRedis()
self.bloom = ScalableBloomFilter(initial_capacity=1000000)
def is_duplicate(self, url):
# 第一步:标准化URL
canonical_url = self._canonicalize(url)
# 第二步:布隆过滤器快速判断
if canonical_url in self.bloom:
# 第三步:Redis精确验证
return self.redis.sismember('visited_urls', canonical_url)
return False
def add_url(self, url):
canonical_url = self._canonicalize(url)
self.bloom.add(canonical_url)
self.redis.sadd('visited_urls', canonical_url)
4.3 性能优化要点
- 布隆过滤器误判率设为0.001%
- Redis使用管道批量操作
- 对URL参数按字母排序标准化
- 定期清理3个月前的访问记录
5. 毫秒级延迟的精准控制
5.1 反爬策略分析
该网站采用了以下反爬机制:
- 请求频率检测(10秒内超过5次触发验证码)
- 鼠标移动轨迹分析
- 页面停留时间统计
- 操作间隔时间模式识别
5.2 动态延迟算法
我们开发了智能延迟系统:
python复制import random
import time
from statistics import mean
class DynamicDelay:
def __init__(self):
self.history = []
self.base_delay = 3.0 # 初始基准延迟(秒)
def get_delay(self):
# 计算最近10次请求的平均间隔
if len(self.history) >= 10:
avg_interval = mean(self.history[-10:])
# 动态调整基准值
self.base_delay = max(1.5, avg_interval * 0.8)
# 生成随机延迟 (基准值±30%)
delay = self.base_delay * (0.7 + 0.6 * random.random())
# 添加人类操作特征
if random.random() < 0.2:
delay *= 1.5 # 随机长延迟模拟人工思考
return delay
def record_request(self):
now = time.time()
if hasattr(self, 'last_request'):
interval = now - self.last_request
self.history.append(interval)
self.last_request = now
5.3 关键参数配置
- 页面类型差异化延迟:
- 列表页:2-4秒
- 详情页:3-6秒
- 搜索页:4-8秒
- 工作时间模拟:
- 东京时间9-18点增加20%延迟
- 周末减少30%请求量
- 错误自动退避:
- 遇到403错误自动休眠5分钟
- 验证码触发后延迟10分钟
6. 实战问题排查实录
6.1 典型问题汇总表
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 获取的URL缺少域名 | 相对路径未正确处理 | 使用urljoin前确保base_url完整 |
| 电话号码包含乱码 | 日文编码问题 | 强制转换为Shift_JIS再处理 |
| 重复采集相同内容 | 分页参数未标准化 | 对page=1和p=1进行统一映射 |
| 频繁触发验证码 | 延迟模式太规律 | 引入随机抖动和长间隔 |
6.2 调试技巧分享
- 使用mitmproxy拦截实际浏览器请求
- 保存原始HTML和清洗后的数据对比
- 对延迟系统进行可视化监控
- 建立自动化测试用例集
7. 项目成果与优化建议
最终这个爬虫系统实现了:
- 每日稳定采集约1200家参展商数据
- 电话号码清洗准确率达99.2%
- 有效URL捕获率提升至97.5%
- 反爬触发率降至0.3%以下
后续优化方向:
- 引入机器学习识别页面模板变化
- 增加分布式部署能力
- 开发自动验证码识别模块
- 建立更精细的请求调度系统
这个项目给我的最大启示是:处理国际网站时,一定要深入研究当地的语言习惯和技术特点。比如日本网站常用的Shift_JIS编码、特殊的电话号码格式等,都需要定制化处理。