1. 项目背景与挑战
去年接手泰国InterPlas展会数据采集任务时,我本以为就是个常规的爬虫开发。真正动手才发现这个项目暗藏玄机——前端采用Algolia搜索接口,需要逆向分析API调用逻辑;展商数据涉及多语言混合展示(泰语/英语/中文);同一公司可能在不同分类下重复出现;最后还要处理上万条数据的批量插入与异常回滚问题。
经过三周的攻坚,我们最终突破了这四大技术难关。今天就把实战中积累的解决方案和避坑经验完整分享出来,特别适合需要处理复杂国际展会数据的同行参考。
2. 核心问题拆解
2.1 Algolia API逆向工程
展会官网的搜索功能基于Algolia实现,常规爬取方法完全失效。通过Chrome开发者工具分析网络请求,发现关键难点在于:
- 动态生成的X-Algolia-API-Key和X-Algolia-Application-Id
- 请求参数中包含复杂的facetFilters(用于多维度筛选)
- 分页机制采用游标(cursor)而非传统页码
逆向方案:
python复制def build_algolia_params():
# 从JS文件提取的固定值
app_id = "B4D4F5S8Q9"
api_key = "d3d9446802a44259755d38e6d163e820"
# 构造符合Algolia规范的查询体
return {
"requests": [{
"indexName": "prod_InterPlas_Exhibitors",
"params": f"query=&hitsPerPage=1000&facets=%5B%22country%22%5D&facetFilters=%5B%22country%3AThailand%22%5D"
}]
}, {
"x-algolia-api-key": api_key,
"x-algolia-application-id": app_id
}
关键点:Algolia的facetFilters需要URL编码,比如
country:Thailand实际要转成%5B%22country%3AThailand%22%5D
2.2 多语言数据处理
展商数据存在泰/英/中三语混合的情况,例如:
json复制{
"company_name": {
"th": "บริษัท พลาสติกไทย จำกัด",
"en": "Thai Plastic Co., Ltd.",
"zh": "泰国塑料有限公司"
},
"address": {
"th": "12/34 หมู่ 6 ถ.บางนา-ตราด",
"en": "12/34 Moo 6, Bangna-Trat Rd."
}
}
处理策略:
- 建立多语言字段映射表
- 优先取中文→英文→泰语的fallback机制
- 使用langdetect库校验语言类型
python复制from langdetect import detect
def get_primary_text(multi_lang_field):
if isinstance(multi_lang_field, dict):
return (
multi_lang_field.get('zh')
or multi_lang_field.get('en')
or multi_lang_field.get('th')
)
return multi_lang_field
2.3 重复数据合并
同一展商因多分类参展导致重复(如既是"注塑机厂商"又是"模具供应商")。我们采用三级匹配策略:
- 精确匹配:统一社会信用代码/营业执照号
- 模糊匹配:
- 公司名称相似度(Levenshtein距离<3)
- 联系电话完全相同
- 人工复核队列:相似度在阈值区间的案例
python复制from Levenshtein import distance
def merge_companies(base_data, new_data):
# 优先级1:证件号匹配
if base_data['license_no'] == new_data['license_no']:
return {**base_data, **new_data}
# 优先级2:名称相似度
if distance(base_data['name'], new_data['name']) < 3:
return {
**base_data,
'categories': list(set(base_data['categories'] + new_data['categories']))
}
return None
2.4 批量插入与回滚机制
当处理8000+展商数据时,传统逐条插入方式需要4小时以上。我们最终方案:
- 使用PostgreSQL的COPY命令批量导入
- 结合事务(Transaction)确保原子性
- 内存中构建插入队列,每2000条提交一次
python复制import psycopg2
from io import StringIO
def bulk_insert(conn, data):
try:
cursor = conn.cursor()
# 开始事务
conn.autocommit = False
# 生成内存文件流
output = StringIO()
for item in data:
output.write(f"{item['id']}\t{item['name']}\t...\n")
output.seek(0)
# 执行COPY
cursor.copy_from(output, 'exhibitors', null='')
conn.commit()
except Exception as e:
conn.rollback()
raise e
3. 性能优化关键
3.1 请求频率控制
为避免触发Algolia的速率限制,采用动态间隔策略:
- 成功请求后:间隔1.2±0.3秒
- 遇到429错误:按
2^n秒指数退避(n为连续错误次数) - 每天00:00重置计数器
3.2 数据去重流水线
mermaid复制graph TD
A[原始数据] --> B(证件号匹配)
B -->|匹配成功| C[合并数据]
B -->|失败| D[名称相似度检测]
D -->|相似度≥90%| C
D -->|相似度60-90%| E[人工复核队列]
D -->|相似度<60%| F[新增记录]
实际生产中建议相似度阈值:证件号匹配(100%)> 名称匹配(90%)> 联系人电话匹配(85%)
4. 踩坑实录
4.1 泰语编码问题
最初直接存储泰语数据导致乱码,解决方案:
- 数据库使用UTF-8mb4编码
- 请求头添加
Accept-Charset: utf-8 - 字符串处理前强制统一编码:
python复制text = str(text).encode('utf-8').decode('utf-8')
4.2 Algolia分页陷阱
当hitsPerPage=1000时,实际最多返回1000条而非全部数据。正确做法是通过cursor遍历:
python复制cursor = None
while True:
params = {
"cursor": cursor,
"hitsPerPage": 500
}
response = requests.post(api_url, json=params)
data = response.json()
# 处理数据...
if not data.get('cursorNext'):
break
cursor = data['cursorNext']
4.3 批量插入内存溢出
初期尝试一次性加载所有数据导致内存爆炸,改进方案:
- 使用生成器逐批读取数据
- 每批处理完后立即释放内存
- 增加进度检查点(每处理20%数据保存状态)
5. 最终成果
经过优化后的采集系统:
- 完整采集时间从6小时降至47分钟
- 数据重复率从18.7%降至0.3%
- 异常自动恢复成功率92%
- 支持断点续传和增量更新
这套方案后来也被成功复用到马来西亚、越南等东南亚展会项目中。最大的体会是:国际展会数据采集必须提前考虑多语言、数据去重和批量操作这三个核心问题,早期设计时的技术选型会直接影响后期维护成本。