1. 为什么爬虫开发者必须掌握JSON和分页?
我刚入行爬虫时,曾经对着网页源代码里那些密密麻麻的方括号和花括号发懵——这就是传说中的JSON数据?更让我困惑的是,明明网页上显示着完整的数据列表,为什么开发者工具里只能看到前20条?直到踩过几次坑才明白,这就是分页机制在作祟。
JSON和分页是现代Web开发中数据交互的两大基石。根据W3Techs的统计,超过70%的API接口采用JSON作为数据交换格式,而分页机制则出现在90%以上的数据列表接口中。作为爬虫开发者,如果不能熟练处理这两种数据结构,就像厨师不会用菜刀一样尴尬。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JSON:爬虫世界的通用语言
2.1 JSON的本质与结构解析
JSON(JavaScript Object Notation)本质上是一种轻量级的数据交换格式。与XML相比,它的最大优势在于结构简洁、解析高效。一个典型的JSON数据看起来是这样的:
json复制{
"status": 200,
"data": [
{
"id": 101,
"title": "Python爬虫入门",
"author": "张伟",
"tags": ["爬虫", "Python", "数据分析"]
},
{
"id": 102,
"title": "JSON详解",
"author": "李娜",
"tags": ["Web开发", "数据传输"]
}
],
"page": {
"current": 1,
"total": 5
}
}
这种结构清晰地体现了JSON的三大核心要素:
- 键值对(Key-Value):用冒号分隔的成对数据
- 对象(Object):用花括号包裹的键值对集合
- 数组(Array):用方括号包裹的值列表
实际开发中常见的问题是JSON格式错误。推荐使用JSONLint等在线工具验证数据格式,避免因缺少逗号或引号导致解析失败。
2.2 Python中的JSON处理实战
Python内置的json模块让JSON处理变得异常简单。以下是三个最常用的操作场景:
场景1:字符串与Python对象的转换
python复制import json
# JSON字符串转Python对象
json_str = '{"name": "Alice", "age": 25}'
python_dict = json.loads(json_str)
print(python_dict['name']) # 输出: Alice
# Python对象转JSON字符串
data = {'city': 'Beijing', 'population': 2171}
json_data = json.dumps(data, ensure_ascii=False) # 中文不转义
print(json_data) # 输出: {"city": "北京", "population": 2171}
场景2:文件读写操作
python复制# 写入JSON文件
with open('data.json', 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2) # indent参数美化格式
# 读取JSON文件
with open('data.json', 'r', encoding='utf-8') as f:
loaded_data = json.load(f)
场景3:处理复杂嵌套结构
当JSON数据包含多层嵌套时,建议使用递归或工具函数处理:
python复制def extract_values(obj, key):
"""递归提取嵌套JSON中的特定键值"""
arr = []
if isinstance(obj, dict):
for k, v in obj.items():
if k == key:
arr.append(v)
elif isinstance(v, (dict, list)):
arr.extend(extract_values(v, key))
elif isinstance(obj, list):
for item in obj:
arr.extend(extract_values(item, key))
return arr
# 示例:提取所有'title'值
titles = extract_values(python_dict, 'title')
3. 分页机制:数据获取的通行证
3.1 分页的底层逻辑与实现方式
分页(Pagination)是Web开发中处理大数据集的通用方案。它的核心目的是:
- 减轻服务器负载
- 提升前端渲染性能
- 优化用户体验
常见的分页参数传递方式有三种:
-
URL参数分页(最常见)
code复制https://api.example.com/items?page=2&size=20 -
路径参数分页
code复制https://api.example.com/items/page/2/size/20 -
请求体分页(POST请求)
json复制{ "page": 2, "pageSize": 20 }
分页响应通常包含以下元数据:
json复制{
"data": [...], // 当前页数据
"pagination": {
"total": 100, // 总记录数
"page": 2, // 当前页码
"pageSize": 20, // 每页大小
"totalPages": 5 // 总页数
}
}
3.2 爬虫如何处理分页数据
基础分页爬取模板
python复制import requests
import time
base_url = "https://api.example.com/items"
page = 1
page_size = 50
results = []
while True:
params = {
"page": page,
"size": page_size
}
try:
response = requests.get(base_url, params=params)
data = response.json()
if not data['items']: # 空列表表示已到末页
break
results.extend(data['items'])
page += 1
# 避免触发反爬机制
time.sleep(1)
except Exception as e:
print(f"第{page}页获取失败: {str(e)}")
break
print(f"共获取{len(results)}条数据")
应对特殊分页策略
-
Token分页(如Twitter API)
python复制next_token = None while True: params = {'max_results': 100} if next_token: params['pagination_token'] = next_token resp = requests.get(url, params=params) data = resp.json() results.extend(data['data']) next_token = data.get('meta', {}).get('next_token') if not next_token: break -
滚动加载(Scroll)
适用于Elasticsearch等搜索引擎:python复制scroll_id = None first_batch = requests.post(url, json={ "query": {...}, "size": 100, "scroll": "1m" # 保持游标1分钟 }).json() results = first_batch['hits']['hits'] scroll_id = first_batch['_scroll_id'] while True: scroll_data = requests.post(scroll_url, json={ "scroll": "1m", "scroll_id": scroll_id }).json() if not scroll_data['hits']['hits']: break results.extend(scroll_data['hits']['hits'])
4. 实战:抓取分页API的完整案例
让我们通过一个真实案例,综合运用JSON和分页知识。假设我们要抓取某电商平台的商品评论数据。
4.1 接口分析与逆向工程
通过浏览器开发者工具(F12),我们发现评论数据来自以下API:
code复制GET https://api.ecommerce.com/product/{id}/reviews
Query Params:
- page: 页码
- pageSize: 每页数量 (默认20)
- sort: 排序方式 (newest/helpful)
响应结构示例:
json复制{
"code": 200,
"data": {
"reviews": [
{
"id": "R123",
"user": "user123",
"rating": 5,
"text": "质量非常好...",
"date": "2023-07-15"
}
],
"pageInfo": {
"page": 1,
"pageSize": 20,
"total": 348,
"hasNext": true
}
}
}
4.2 爬虫实现与异常处理
python复制import requests
from urllib.parse import urlencode
import json
import time
def scrape_reviews(product_id, max_pages=10):
base_url = f"https://api.ecommerce.com/product/{product_id}/reviews"
headers = {
"User-Agent": "Mozilla/5.0",
"Referer": f"https://www.ecommerce.com/product/{product_id}"
}
all_reviews = []
current_page = 1
while current_page <= max_pages:
params = {
"page": current_page,
"pageSize": 20,
"sort": "newest"
}
try:
response = requests.get(
base_url,
params=params,
headers=headers,
timeout=10
)
# 检查HTTP状态码
if response.status_code != 200:
print(f"请求失败,状态码:{response.status_code}")
break
data = response.json()
# 检查API返回码
if data.get('code') != 200:
print(f"API错误:{data.get('message', '未知错误')}")
break
reviews = data['data']['reviews']
if not reviews:
print("没有更多评论")
break
all_reviews.extend(reviews)
print(f"已获取第{current_page}页,累计{len(all_reviews)}条评论")
# 检查是否还有下一页
if not data['data']['pageInfo']['hasNext']:
break
current_page += 1
time.sleep(1.5) # 礼貌性延迟
except requests.exceptions.RequestException as e:
print(f"网络请求异常:{str(e)}")
break
except json.JSONDecodeError:
print("响应不是有效的JSON格式")
break
except KeyError as e:
print(f"响应缺少必要字段:{str(e)}")
break
return all_reviews
# 使用示例
reviews = scrape_reviews("P10086", max_pages=5)
with open('reviews.json', 'w', encoding='utf-8') as f:
json.dump(reviews, f, ensure_ascii=False, indent=2)
4.3 反爬应对策略
-
请求头伪装
务必设置合理的User-Agent和Referer,有些API会校验这些头部。 -
请求频率控制
添加随机延迟,避免固定间隔请求:python复制from random import uniform time.sleep(uniform(0.5, 2.0)) # 随机延迟0.5~2秒 -
代理IP池
当遇到IP封锁时,需要使用代理:python复制proxies = { 'http': 'http://user:pass@proxy_ip:port', 'https': 'http://user:pass@proxy_ip:port' } response = requests.get(url, proxies=proxies) -
Cookie维持
对于需要登录的接口:python复制session = requests.Session() session.cookies.update({'key': 'value'}) response = session.get(url)
5. 进阶技巧与性能优化
5.1 JSON处理的高阶操作
处理日期时间格式
JSON标准不支持日期类型,常见处理方式:
python复制from datetime import datetime
# 自定义JSON编码器
class DateTimeEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
return super().default(obj)
# 使用示例
data = {'event': '发布会', 'time': datetime.now()}
json_str = json.dumps(data, cls=DateTimeEncoder)
大数据量分块处理
当JSON文件过大时,使用ijson库流式处理:
python复制import ijson
with open('large_file.json', 'rb') as f:
for item in ijson.items(f, 'item'):
process_item(item) # 逐项处理
5.2 分页爬取的并发优化
使用多线程加速分页抓取(注意线程安全):
python复制from concurrent.futures import ThreadPoolExecutor
import threading
lock = threading.Lock()
results = []
def fetch_page(page):
url = f"https://api.example.com/items?page={page}"
try:
data = requests.get(url).json()
with lock:
results.extend(data['items'])
except Exception as e:
print(f"Page {page} failed: {str(e)}")
with ThreadPoolExecutor(max_workers=5) as executor:
executor.map(fetch_page, range(1, 11)) # 并发抓取1~10页
5.3 数据存储方案选择
根据数据量选择存储方式:
| 数据规模 | 推荐方案 | 优点 | 缺点 |
|---|---|---|---|
| <1MB | JSON文件 | 简单直观 | 无查询能力 |
| 1MB-1GB | SQLite | 轻量级,支持SQL | 单文件限制 |
| >1GB | MongoDB | 灵活扩展 | 需要单独服务 |
SQLite存储示例
python复制import sqlite3
def save_to_sqlite(data, db_file):
conn = sqlite3.connect(db_file)
c = conn.cursor()
# 创建表
c.execute('''CREATE TABLE IF NOT EXISTS reviews
(id TEXT, user TEXT, rating INTEGER, text TEXT, date TEXT)''')
# 批量插入
to_insert = [(r['id'], r['user'], r['rating'], r['text'], r['date'])
for r in data]
c.executemany("INSERT INTO reviews VALUES (?,?,?,?,?)", to_insert)
conn.commit()
conn.close()
6. 常见问题排查指南
6.1 JSON解析错误排查
问题1:JSONDecodeError异常
python复制try:
data = json.loads(invalid_json_str)
except json.JSONDecodeError as e:
print(f"解析失败:{e.msg}")
print(f"错误位置:第{e.lineno}行,第{e.colno}列")
问题2:Unicode编码问题
python复制# 错误做法
json_str = '{"name": "张三"}'
data = json.loads(json_str) # 可能报错
# 正确做法
data = json.loads(json_str.encode('utf-8').decode('unicode_escape'))
6.2 分页接口的特殊情况处理
情况1:总页数不准确
python复制# 不要依赖接口返回的totalPages,改用以下逻辑
while True:
data = get_page(page)
if not data['items']:
break
# ...处理数据...
page += 1
情况2:动态页码限制
某些API会限制最大页码访问(如只允许访问前100页),解决方案:
- 按时间范围查询(如有createdAt字段)
- 使用更细粒度的筛选条件
情况3:分页参数加密
遇到像page_token=AX45%2F...这样的参数时:
- 确认token是否在之前的响应中返回
- 观察token是否有时效性
- 尝试固定使用某个有效token看是否始终可用
6.3 性能瓶颈诊断
当爬取速度变慢时,检查以下方面:
-
网络延迟
使用requests的timeout参数检测响应时间:python复制try: response = requests.get(url, timeout=(3.05, 10)) except requests.exceptions.Timeout: print("请求超时") -
JSON解析耗时
对于大JSON文件,测试不同解析方式的性能:python复制import timeit def test_json_loads(): with open('large.json') as f: json.load(f) print(timeit.timeit(test_json_loads, number=10)) -
内存占用
使用生成器替代列表存储中间结果:python复制def get_all_reviews(): page = 1 while True: data = get_page(page) if not data['reviews']: break for review in data['reviews']: yield review page += 1
7. 安全合规与最佳实践
7.1 合法爬取注意事项
-
检查robots.txt
在爬取前访问https://目标网站/robots.txt,确认目标路径是否允许爬取。 -
遵守API速率限制
如果API返回429状态码(Too Many Requests),应自动降速:python复制if response.status_code == 429: retry_after = int(response.headers.get('Retry-After', 60)) time.sleep(retry_after) continue -
用户数据保护
对爬取到的个人信息(如用户名、邮箱)应进行脱敏处理:python复制import re def anonymize_text(text): text = re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL]', text) text = re.sub(r'\b\d{3}[-.]?\d{4}\b', '[PHONE]', text) return text
7.2 代码健壮性提升
-
请求重试机制
使用tenacity库实现智能重试: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 fetch_data(url): response = requests.get(url) response.raise_for_status() return response.json() -
数据验证
使用Pydantic验证JSON数据结构:python复制from pydantic import BaseModel class ReviewModel(BaseModel): id: str user: str rating: int = Field(..., ge=1, le=5) text: str date: str # 使用示例 try: validated = ReviewModel(**review_data) except ValidationError as e: print(f"数据验证失败: {e}") -
日志记录
配置详细日志以便问题追踪:python复制import logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('crawler.log'), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) logger.info(f"开始处理第{page}页")
8. 扩展学习路径
8.1 推荐工具链
-
开发调试工具
- Postman:API测试与文档生成
- JSON Viewer Pro:Chrome插件,美化JSON展示
- jq:命令行JSON处理工具
-
Python生态工具
- jsonpath-ng:JSON路径查询
- orjson:更快的JSON解析库
- pydantic:数据验证与设置管理
-
爬虫框架
- Scrapy:全功能爬虫框架
- httpx:支持HTTP/2的请求库
- playwright:自动化浏览器工具
8.2 进阶学习资源
-
JSON相关
- 《JSON at Work》- Tom Marrs
- JSON Schema规范(json-schema.org)
-
分页优化
- Facebook的Graph API分页设计
- Twitter API v2的分页实践
-
爬虫工程化
- 分布式任务队列(Celery/RQ)
- 反爬对抗策略研究
- 数据清洗与ETL流程
8.3 实战项目建议
-
初级项目
- 抓取天气预报API并存储
- 爬取豆瓣电影Top250
-
中级项目
- 实现电商价格监控系统
- 构建新闻聚合爬虫
-
高级挑战
- 设计分布式爬虫架构
- 开发自动化爬虫调度平台
我曾在处理一个政府开放数据平台时,遇到过分页参数加密的情况。通过分析前端JavaScript代码,发现他们只是用Base64编码了页码和时间戳的组合。这个经历让我明白,很多看似复杂的限制,背后可能是相对简单的实现。关键是要保持耐心,用开发者工具的Network面板仔细观察每个请求的细节。
