1. 项目背景与挑战
最近接手了一个尼日利亚橡胶塑料及印刷包装展的参展商数据采集项目,目标网站采用了Next.js框架构建。刚拿到需求时觉得就是个常规爬虫任务,真正上手才发现现代前端框架给数据采集带来的挑战远超预期。这个项目让我深刻体会到,传统爬虫技术在现代Web应用面前已经显得力不从心。
网站采用了典型的前端路由+服务端渲染架构,页面内容动态加载,常规的HTML解析完全失效。更棘手的是,展商数据以复杂嵌套的JSON结构存储,其中还混杂着HTML标签,分页逻辑也不同于传统参数递增模式。经过两周的攻坚,最终我们突破了四大技术难关,今天就把这些实战经验分享给大家。
2. 技术难点全景分析
2.1 Next.js数据架构解析
Next.js应用最显著的特点是将页面初始状态存储在__NEXT_DATA__脚本标签中。这个JSON对象就像个黑匣子,包含了当前路由、组件props和所有动态加载的数据。我们的首要任务就是破解这个数据结构。
通过Chrome开发者工具分析,发现目标网站的展商数据被嵌套在props.pageProps.dehydratedState.queries路径下。这里有个坑:不同Next.js版本的数据结构可能有差异,需要针对具体网站进行适配。
python复制import json
from bs4 import BeautifulSoup
def parse_next_data(html):
soup = BeautifulSoup(html, 'html.parser')
script = soup.find('script', id='__NEXT_DATA__')
if not script:
raise ValueError('__NEXT_DATA__ not found')
try:
data = json.loads(script.string)
exhibitors = data['props']['pageProps']['dehydratedState']['queries'][0]['state']['data']
return exhibitors
except (KeyError, json.JSONDecodeError) as e:
print(f"解析错误: {str(e)}")
return None
注意:Next.js的数据结构可能随版本更新而变化,建议先用浏览器开发者工具手动验证数据路径,再编写解析代码。
2.2 JSON内嵌HTML的清洗难题
从__NEXT_DATA__提取出的展商数据中,公司介绍、联系方式等字段经常包含HTML标签。比如:
json复制{
"company": "ABC Plastics",
"description": "<div><p>Leading manufacturer of <strong>plastic</strong> products</p></div>",
"contact": "<a href=\"mailto:info@abc.com\">info@abc.com</a>"
}
我们开发了多层次的清洗方案:
- 先用
BeautifulSoup提取纯文本 - 对特殊字段(如邮箱、电话)使用正则表达式提取
- 保留关键格式信息(如换行符)
python复制import re
from bs4 import BeautifulSoup
def clean_html_content(html_str):
if not html_str or not isinstance(html_str, str):
return html_str
# 提取邮箱
email_pattern = r'[\w\.-]+@[\w\.-]+'
emails = re.findall(email_pattern, html_str)
# 提取纯文本
soup = BeautifulSoup(html_str, 'html.parser')
text = soup.get_text(separator='\n', strip=True)
# 合并结果
result = {
'text': text,
'emails': emails
}
return result
3. 展位信息数组化处理
3.1 数据结构分析
展位信息在原始数据中呈现为自由文本格式,例如:"Stand: A1, A2, B3"或"Hall 3 - Stand 4A"。这种非结构化数据难以直接使用,需要转换为规范的数组格式。
我们观察到展位信息有几种常见模式:
- 单个展位:A1
- 连续展位:A1-A5
- 多个展位:A1, A2, B3
- 带展厅号:Hall 3 Stand A1
3.2 解析算法实现
开发了基于正则表达式的多级解析器:
python复制import re
def parse_stand_info(stand_text):
if not stand_text:
return []
# 统一处理大小写和空格
stand_text = stand_text.upper().replace(' ', '')
# 匹配基础模式
patterns = [
r'HALL(\d+)STAND([A-Z]\d+)', # Hall3StandA1
r'STAND([A-Z]\d+)', # StandA1
r'([A-Z]\d+(?:-[A-Z]\d+)?)', # A1 或 A1-A5
r'([A-Z]\d+(?:,[A-Z]\d+)+)' # A1,A2,B3
]
stands = []
for pattern in patterns:
matches = re.findall(pattern, stand_text)
if matches:
for match in matches:
if isinstance(match, tuple):
match = match[-1] # 取最后一个分组
# 处理连续展位
if '-' in match:
start, end = match.split('-')
stands.extend(expand_stand_range(start, end))
# 处理多个展位
elif ',' in match:
stands.extend(match.split(','))
else:
stands.append(match)
break
return list(set(stands)) # 去重
def expand_stand_range(start, end):
""" 展开连续展位范围如A1-A5 """
prefix = start[0]
start_num = int(start[1:])
end_num = int(end[1:])
return [f"{prefix}{i}" for i in range(start_num, end_num + 1)]
实战技巧:展位信息解析后建议存储为数组类型,方便后续的查询和分析。同时保留原始文本以便核对。
4. 分页参数固定化处理
4.1 分页机制分析
传统分页通常使用page=1这样的递增参数,但现代前端框架往往采用固定参数分页。目标网站的分页请求如下:
code复制POST /api/exhibitors
{
"cursor": "abc123",
"limit": 20
}
关键发现:
cursor是加密字符串,无法简单递增- 下一页的
cursor包含在当前响应中 - 最后一页的
cursor为null
4.2 分页爬取实现
python复制import requests
def crawl_exhibitors(base_url, initial_cursor=None):
all_exhibitors = []
cursor = initial_cursor
limit = 50 # 每页数量
while True:
payload = {
"cursor": cursor,
"limit": limit
}
try:
response = requests.post(
f"{base_url}/api/exhibitors",
json=payload,
headers={"Content-Type": "application/json"}
)
response.raise_for_status()
data = response.json()
all_exhibitors.extend(data['exhibitors'])
if not data.get('next_cursor'):
break
cursor = data['next_cursor']
except Exception as e:
print(f"请求失败: {str(e)}")
break
return all_exhibitors
4.3 分页终止条件
现代分页API通常有以下几种终止条件:
- 返回空列表
next_cursor为null- 返回的记录数小于
limit - 达到最大页数限制(如有)
建议在代码中同时检查多种条件,提高鲁棒性:
python复制if (not data.get('exhibitors') or
not data.get('next_cursor') or
len(data['exhibitors']) < limit):
break
5. 系统架构与优化
5.1 整体采集流程
mermaid复制graph TD
A[开始] --> B[获取初始页面]
B --> C[解析__NEXT_DATA__]
C --> D[提取初始cursor]
D --> E[请求API分页数据]
E --> F[清洗和转换数据]
F --> G{是否有下一页}
G -- 是 --> E
G -- 否 --> H[存储数据]
H --> I[结束]
5.2 性能优化措施
-
并发控制:使用
aiohttp实现异步请求python复制import aiohttp import asyncio async def fetch_page(session, url, cursor): payload = {"cursor": cursor, "limit": 50} async with session.post(url, json=payload) as response: return await response.json() -
缓存机制:对已爬取的
cursor进行缓存,避免重复请求 -
错误重试:对失败请求实现指数退避重试
python复制from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) def safe_request(url, payload): response = requests.post(url, json=payload) response.raise_for_status() return response
6. 常见问题与解决方案
6.1 数据解析失败
症状:__NEXT_DATA__结构变化导致解析失败
解决方案:
- 定期检查数据结构是否变化
- 实现多版本兼容的解析逻辑
- 添加详细的错误日志
6.2 反爬虫机制
症状:请求被拒绝或返回验证码
应对策略:
- 设置合理的请求头(如
User-Agent) - 控制请求频率(建议500ms-1s/次)
- 使用住宅代理轮换IP
6.3 数据不一致
症状:API返回数据与页面显示不一致
排查步骤:
- 检查是否有客户端渲染的数据未包含在API中
- 验证
cursor分页是否遗漏记录 - 对比不同时间点的数据快照
7. 项目成果与经验总结
最终我们成功采集了全部326家参展商的完整信息,包括:
- 公司基本信息
- 产品类别
- 展位位置
- 联系方式
- 公司介绍
关键经验:
- 现代前端框架网站的数据通常隐藏在
__NEXT_DATA__或API响应中 - 复杂JSON结构需要逐层解析和验证
- 分页逻辑可能完全不同于传统模式
- 数据清洗是保证质量的关键步骤
这个项目让我深刻认识到,爬虫工程师必须紧跟Web技术发展,持续更新技术栈。下次再遇到类似项目,我会首先:
- 全面分析网站的技术架构
- 优先寻找数据API接口
- 设计灵活的数据解析方案
- 实现健壮的错误处理机制
对于需要采集现代前端框架网站数据的同行,建议重点掌握:
- Chrome开发者工具的高级用法
- JSONPath/XPath数据提取
- 异步请求处理
- 反反爬虫策略