1. 多源数据爬取的痛点与解决方案
当我们需要从多个网站采集同一类数据时,最令人头疼的问题就是不同站点对相同概念的字段命名和格式完全不同。比如同样表示商品价格,A网站用"price",B网站用"current_price",C网站用"¥价格";日期格式更是五花八门,有"2023-08-15"、"15/08/2023"、"Aug 15, 2023"等多种变体。
这种情况在爬虫开发中被称为"字段异构问题",它会导致:
- 数据存储结构混乱
- 后续分析处理困难
- 可视化展示不一致
- 跨源数据比对几乎不可能
解决这个问题的核心思路是建立"字段映射表+数据清洗管道":
- 为每个目标字段定义标准名称(如统一用"price")
- 收集各站点原始字段的所有可能写法
- 编写转换函数处理不同格式的数据
- 最终输出统一结构的数据
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实战案例:三站点商品数据聚合
假设我们要从以下三个电商站点爬取商品信息:
- 站点A:www.example-a.com
- 站点B:m.example-b.com
- 站点C:item.example-c.com
2.1 目标字段分析
首先明确我们需要采集的核心字段:
- 商品名称
- 商品价格
- 商品评分
- 评论数量
- 上架时间
2.2 各站点字段对比
通过开发者工具分析各站点的网页结构,发现字段对应关系如下:
| 标准字段 | 站点A字段 | 站点B字段 | 站点C字段 |
|---|---|---|---|
| 商品名称 | .title | #productName | h1.name |
| 商品价格 | .price | .current-price | .rmb |
| 商品评分 | .star | [itemprop="rating"] | div.score |
| 评论数量 | .review-num | #comments-count | span.comment |
| 上架时间 | meta[date] | .time | div.date |
2.3 数据格式差异
更复杂的是相同字段在不同站点的呈现格式:
-
价格字段:
- 站点A:"¥199.00"
- 站点B:"199元"
- 站点C:"199.00"
-
评分字段:
- 站点A:5星制(显示"★★★★☆")
- 站点B:10分制(显示"8.5/10")
- 站点C:百分制(显示"85%")
-
日期字段:
- 站点A:"2023-08-15"
- 站点B:"15/08/23"
- 站点C:"2周前"
3. 构建字段归一化处理器
3.1 基础字段映射类
python复制class FieldMapper:
def __init__(self):
self.field_map = {
'product_name': {
'site_a': '.title',
'site_b': '#productName',
'site_c': 'h1.name'
},
'price': {
'site_a': '.price',
'site_b': '.current-price',
'site_c': '.rmb'
}
# 其他字段映射...
}
def get_selector(self, site, field):
return self.field_map[field][site]
3.2 价格归一化处理器
python复制import re
def price_normalizer(raw_price):
# 去除货币符号和单位
cleaned = re.sub(r'[^\d.]', '', raw_price)
# 处理没有小数点的情况
if '.' not in cleaned:
cleaned += '.00'
# 统一为两位小数
return format(float(cleaned), '.2f')
3.3 评分转换器
python复制def rating_converter(raw_rating, source_type):
if source_type == 'site_a': # 五星制
stars = len(raw_rating.split('★')[0])
return stars * 2 # 转换为10分制
elif source_type == 'site_b': # 10分制
return float(raw_rating.split('/')[0])
elif source_type == 'site_c': # 百分制
return float(raw_rating.strip('%')) / 10
else:
return 0.0
3.4 日期解析器
python复制from datetime import datetime, timedelta
import dateparser
def date_parser(raw_date, source_type):
if source_type == 'site_a':
return datetime.strptime(raw_date, '%Y-%m-%d')
elif source_type == 'site_b':
return datetime.strptime(raw_date, '%d/%m/%y')
elif source_type == 'site_c':
if '天前' in raw_date:
days = int(raw_date.split('天前')[0])
return datetime.now() - timedelta(days=days)
elif '周前' in raw_date:
weeks = int(raw_date.split('周前')[0])
return datetime.now() - timedelta(weeks=weeks)
else:
return dateparser.parse(raw_date)
4. 完整爬虫实现
4.1 爬虫主框架
python复制import requests
from bs4 import BeautifulSoup
class MultiSourceSpider:
def __init__(self):
self.mapper = FieldMapper()
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...'
})
def crawl_site(self, url, site_type):
try:
resp = self.session.get(url, timeout=10)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, 'html.parser')
result = {}
# 商品名称
name_selector = self.mapper.get_selector(site_type, 'product_name')
raw_name = soup.select_one(name_selector).text.strip()
result['product_name'] = raw_name
# 商品价格
price_selector = self.mapper.get_selector(site_type, 'price')
raw_price = soup.select_one(price_selector).text.strip()
result['price'] = price_normalizer(raw_price)
# 其他字段处理...
return result
except Exception as e:
print(f"Error crawling {url}: {str(e)}")
return None
4.2 数据聚合处理器
python复制class DataAggregator:
def __init__(self):
self.products = []
def add_product(self, product_data, source):
normalized = {
'source': source,
'name': product_data['product_name'],
'price': product_data['price'],
'rating': rating_converter(
product_data['rating'],
source
),
'date': date_parser(
product_data['date'],
source
).strftime('%Y-%m-%d')
}
self.products.append(normalized)
def export_json(self, filename):
import json
with open(filename, 'w', encoding='utf-8') as f:
json.dump(self.products, f, ensure_ascii=False, indent=2)
5. 实战中的经验技巧
5.1 动态字段映射配置
建议将字段映射关系存储在JSON配置文件中,这样新增站点时无需修改代码:
json复制// field_config.json
{
"product_name": {
"site_a": ".title",
"site_b": "#productName",
"site_c": "h1.name"
},
"price": {
"site_a": ".price",
"site_b": ".current-price",
"site_c": ".rmb"
}
}
加载方式:
python复制import json
with open('field_config.json') as f:
field_config = json.load(f)
5.2 智能字段探测
对于不确定的字段,可以编写自动探测逻辑:
python复制def detect_field(soup, possible_selectors):
for selector in possible_selectors:
element = soup.select_one(selector)
if element:
return element.text.strip()
return None
5.3 处理反爬机制
多站点爬取时,需要注意:
- 每个站点使用独立的请求间隔
- 为不同站点配置不同的请求头
- 遇到验证码时自动切换代理
python复制from time import sleep
import random
SITE_SETTINGS = {
'site_a': {
'delay': (1, 3),
'headers': {...}
},
'site_b': {
'delay': (2, 5),
'headers': {...}
}
}
def crawl_with_delay(url, site_type):
delay_range = SITE_SETTINGS[site_type]['delay']
delay = random.uniform(*delay_range)
sleep(delay)
# 发送请求...
5.4 数据质量监控
建议实现数据校验机制:
python复制def validate_product(product):
rules = {
'price': lambda x: float(x) > 0,
'rating': lambda x: 0 <= float(x) <= 10,
'date': lambda x: datetime.strptime(x, '%Y-%m-%d') <= datetime.now()
}
errors = []
for field, validator in rules.items():
if not validator(product.get(field, '')):
errors.append(field)
return len(errors) == 0, errors
6. 项目扩展思路
6.1 支持更多数据源
- 新增站点只需在配置文件中添加字段映射
- 为特殊站点编写定制解析器
- 实现自动站点发现功能
6.2 实时数据更新
- 使用APScheduler设置定时任务
- 对已采集商品进行价格变动监控
- 实现价格异常波动告警
python复制from apscheduler.schedulers.background import BackgroundScheduler
def start_monitor():
scheduler = BackgroundScheduler()
scheduler.add_job(
crawl_task,
'interval',
hours=1
)
scheduler.start()
6.3 数据可视化
- 使用Matplotlib生成价格对比图表
- 制作各站点评分分布直方图
- 展示价格随时间变化趋势
python复制import matplotlib.pyplot as plt
def plot_price_comparison(products):
sites = list(set(p['source'] for p in products))
avg_prices = [
sum(float(p['price']) for p in products if p['source'] == site) /
len([p for p in products if p['source'] == site])
for site in sites
]
plt.bar(sites, avg_prices)
plt.title('Average Price Comparison')
plt.ylabel('Price')
plt.savefig('price_comparison.png')
在实际项目中,我通常会先花1-2天时间仔细分析所有目标站点的数据结构,制作详细的字段对照表。这个前期工作虽然耗时,但能大幅减少后期的调试时间。另外建议为每个站点编写独立的测试用例,确保当站点改版时能第一时间发现问题。
