🔥本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!!

全文目录:
-
-
- [🌟 开篇语](#🌟 开篇语)
- [📚 上期回顾](#📚 上期回顾)
- [🎯 本篇目标](#🎯 本篇目标)
- [💡 为什么"接口优先"?](#💡 为什么"接口优先"?)
- [🔍 实战演示:从 Network 还原接口](#🔍 实战演示:从 Network 还原接口)
-
- 案例站点:某科技资讯网
-
- [步骤 1:打开 Network 面板](#步骤 1:打开 Network 面板)
- [步骤 2:识别数据接口](#步骤 2:识别数据接口)
- [步骤 3:分析请求参数](#步骤 3:分析请求参数)
- [步骤 4:用 Python 还原请求](#步骤 4:用 Python 还原请求)
- [🛠️ 完整 Demo 代码](#🛠️ 完整 Demo 代码)
-
- 项目结构
- [模块 1:配置文件](#模块 1:配置文件)
- [模块 2:API 客户端](#模块 2:API 客户端)
- [模块 3:数据转换器](#模块 3:数据转换器)
- [模块 4:API 爬虫引擎](#模块 4:API 爬虫引擎)
- [模块 5:数据库管理](#模块 5:数据库管理)
- [模块 6:主程序](#模块 6:主程序)
- [📊 运行效果](#📊 运行效果)
- [🚀 快速适配新站点](#🚀 快速适配新站点)
- [📝 小结](#📝 小结)
- [🎯 下期预告](#🎯 下期预告)
- [🌟 文末](#🌟 文末)
-
- [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
-
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 👉 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏《Python爬虫实战》
订阅后更新会优先推送,按目录学习更高效~
📚 上期回顾
上一篇《通用新闻采集器:从零打造可复用的静态站模板》我们打造了通用静态新闻采集器,用配置化方式快速适配多个站点。但你有没有发现一个问题:很多现代网站根本不是静态的------打开浏览器能看到数据,查看源代码却是空的。
这时候很多人会直接上 Playwright,启动浏览器、等待渲染、提取数据......但这真的是最优方案吗?
99% 的情况下,答案是 NO。
今天我们就来学习降维打击的终极技巧 :用浏览器开发者工具找到真正的数据接口,然后用轻量级的 requests 直接抓取 JSON------速度快 10 倍,稳定性更高!🚀
🎯 本篇目标
看完这篇,你能做到:
- 熟练使用 Network 面板找接口(核心技能)
- 分析接口参数规律(分页、筛选、加密)
- 还原接口请求逻辑(Headers、参数构造)
- 封装通用 API 采集器(可复用模板)
验收标准:从 3 个动态网站找到数据接口,用 requests 采集各 200+ 条数据。
💡 为什么"接口优先"?
性能对比实测
| 方案 | 采集 200 条耗时 | 内存占用 | 稳定性 |
|---|---|---|---|
| Playwright | ~60秒 | 300-500MB | ⭐⭐⭐ |
| API 直采 | ~5秒 | 10-20MB | ⭐⭐⭐⭐⭐ |
效率提升 12 倍! ⚡
真实案例
某电商商品搜索页:
浏览器看到的:
html
<div id="app">
<!-- 由 JavaScript 渲染 -->
</div>
<script src="app.js"></script>
背后的真相:
javascript
// app.js 中的请求
fetch('https://api.example.com/search', {
method: 'POST',
body: JSON.stringify({
keyword: '手机',
page: 1,
pageSize: 20
})
})
直接抓接口:
python
import requests
resp = requests.post('https://api.example.com/search', json={
'keyword': '手机',
'page': 1,
'pageSize': 20
})
data = resp.json() # 直接拿到 JSON 数据!
🔍 实战演示:从 Network 还原接口
案例站点:某科技资讯网
我们以一个真实场景演示完整流程(以 IT 之家为例)。
步骤 1:打开 Network 面板
- 打开目标网站(如 https://www.ithome.com)
- 按
F12打开开发者工具 - 切换到 Network 标签
- 勾选 Preserve log(保留日志)
- 点击筛选器选择 Fetch/XHR(只看 Ajax 请求)
- 刷新页面
F5
步骤 2:识别数据接口
观察请求列表,找名字像 API 的:
json
✅ 这是数据接口:
newslist
api/news/getlist
search.json
data/list
❌ 这不是数据接口:
app.js
logo.png
analytics.js
点击可疑请求,查看 Preview 或 Response:
json
{
"code": 0,
"message": "success",
"data": {
"list": [
{
"newsid": "123456",
"title": "苹果发布新品",
"postdate": "2026-01-23 10:00:00"
}
],
"total": 500,
"pageSize": 20
}
}
看到这种结构,就是数据接口!
步骤 3:分析请求参数
切换到 Headers 标签,记录:
json
Request URL: https://api.ithome.com/api/news/newslistpageget
Request Method: POST
Request Headers:
User-Agent: Mozilla/5.0...
Content-Type: application/json
Referer: https://www.ithome.com
Request Payload:
{
"categoryid": "0",
"type": "0",
"page": 1,
"pageSize": 20
}
步骤 4:用 Python 还原请求
python
import requests
url = 'https://api.ithome.com/api/news/newslistpageget'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Content-Type': 'application/json',
'Referer': 'https://www.ithome.com'
}
payload = {
'categoryid': '0',
'type': '0',
'page': 1,
'pageSize': 20
}
resp = requests.post(url, json=payload, headers=headers, timeout=10)
print(resp.status_code)
print(resp.json())
运行成功,拿到数据! 🎉
🛠️ 完整 Demo 代码
项目结构
json
api_spider_demo/
├── config/
│ ├── sites/
│ │ ├── ithome.yaml # IT之家配置
│ │ ├── zhihu.yaml # 知乎配置
│ │ └── template.yaml # 配置模板
│ └── spider.yaml
│
├── src/
│ ├── core/
│ │ ├── api_client.py # API 客户端
│ │ ├── database.py # 数据库管理
│ │ └── spider.py # 爬虫引擎
│ │
│ └── adapters/
│ ├── base.py # 适配器基类
│ └── ithome.py # IT之家适配器
│
├── data/
│ └── news.db
├── logs/
├── requirements.txt
├── main.py
└── README.md
模块 1:配置文件
yaml
# config/sites/ithome.yaml
site:
name: "IT之家"
domain: "https://www.ithome.com"
api:
list:
url: "https://api.ithome.com/api/news/newslistpageget"
method: "POST" # GET / POST
headers:
User-Agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
Content-Type: "application/json"
Referer: "https://www.ithome.com"
params: # GET 参数
{}
payload: # POST 参数
categoryid: "0"
type: "0"
page: "{page}" # 使用占位符
pageSize: 20
response:
data_path: "data.list" # 数据在响应中的路径
total_path: "data.total"
detail:
url: "https://api.ithome.com/api/news/detail/{newsid}"
method: "GET"
params:
newsid: "{newsid}"
fields:
mapping:
# API 字段 → 数据库字段
newsid: "article_id"
title: "title"
postdate: "publish_time"
summary: "summary"
url: "detail_url"
transforms:
# 字段转换规则
publish_time:
type: "datetime"
format: "%Y-%m-%d %H:%M:%S"
pagination:
type: "page_number" # page_number / offset / cursor
start_page: 1
max_pages: 10
page_size: 20
incremental:
enabled: true
field: "publish_time"
direction: "desc" # desc / asc
模块 2:API 客户端
python
# src/core/api_client.py
import requests
import json
from typing import Dict, Any, Optional
class APIClient:
"""通用 API 客户端"""
def __init__(self, config: Dict):
self.config = config
self.session = requests.Session()
self._setup_session()
def _setup_session(self):
"""配置会话"""
# 设置默认 headers
headers = self.config['api']['list'].get('headers', {})
self.session.headers.update(headers)
def fetch_list(self, page: int) -> Optional[Dict]:
"""
获取列表数据
Args:
page: 页码
Returns:
dict: {items: [...], total: 100}
"""
api_config = self.config['api']['list']
url = api_config['url']
method = api_config['method'].upper()
# 构造参数
params = self._build_params(api_config.get('params', {}), page)
payload = self._build_params(api_config.get('payload', {}), page)
print(f"📡 请求接口:{method} {url}")
print(f" 页码:{page}")
try:
if method == 'GET':
resp = self.session.get(url, params=params, timeout=15)
else: # POST
resp = self.session.post(url, json=payload, params=params, timeout=15)
resp.raise_for_status()
data = resp.json()
# 提取数据
items = self._extract_data(data, api_config['response']['data_path'])
total = self._extract_data(data, api_config['response'].get('total_path'))
print(f" ✅ 获得 {len(items)} 条数据")
return {
'items': items,
'total': total
}
except Exception as e:
print(f" ❌ 请求失败:{e}")
return None
def fetch_detail(self, article_id: str) -> Optional[Dict]:
"""获取详情数据"""
if 'detail' not in self.config['api']:
return None
api_config = self.config['api']['detail']
url = api_config['url'].format(newsid=article_id)
method = api_config['method'].upper()
try:
if method == 'GET':
resp = self.session.get(url, timeout=15)
else:
resp = self.session.post(url, timeout=15)
resp.raise_for_status()
return resp.json()
except Exception as e:
print(f" ❌ 详情请求失败:{e}")
return None
def _build_params(self, template: Dict, page: int) -> Dict:
"""构造参数(替换占位符)"""
params = {}
for key, value in template.items():
if isinstance(value, str) and '{page}' in value:
params[key] = page
elif isinstance(value, str) and value.startswith('{') and value.endswith('}'):
# 保留占位符,后续处理
params[key] = value
else:
params[key] = value
return params
def _extract_data(self, response: Dict, path: str) -> Any:
"""
从响应中提取数据
Args:
response: 完整响应
path: 数据路径(如 'data.list')
"""
if not path:
return response
keys = path.split('.')
data = response
for key in keys:
if isinstance(data, dict):
data = data.get(key)
else:
return None
return data
模块 3:数据转换器
python
# src/core/transformer.py
from datetime import datetime
from hashlib import md5
from typing import Dict, Any
class DataTransformer:
"""数据转换器"""
def __init__(self, config: Dict):
self.config = config
self.field_mapping = config['fields']['mapping']
self.transforms = config['fields'].get('transforms', {})
def transform_list_item(self, raw_item: Dict) -> Dict:
"""
转换列表项
Args:
raw_item: API 返回的原始数据
Returns:
dict: 标准化后的数据
"""
item = {}
# 字段映射
for api_field, db_field in self.field_mapping.items():
value = raw_item.get(api_field)
if value is not None:
# 应用转换规则
if db_field in self.transforms:
value = self._apply_transform(value, self.transforms[db_field])
item[db_field] = value
# 生成去重键
source = self.config['site']['name']
unique_id = raw_item.get('newsid') or raw_item.get('id') or raw_item.get('url')
item['dedup_key'] = md5(f"{source}_{unique_id}".encode()).hexdigest()
# 添加来源
item['source'] = source
return item
def _apply_transform(self, value: Any, rules: Dict) -> Any:
"""应用转换规则"""
transform_type = rules.get('type')
if transform_type == 'datetime':
# 时间转换
date_format = rules.get('format', '%Y-%m-%d %H:%M:%S')
try:
dt = datetime.strptime(value, date_format)
return {
'datetime': dt.strftime('%Y-%m-%d %H:%M:%S'),
'timestamp': int(dt.timestamp())
}
except:
return value
elif transform_type == 'int':
try:
return int(value)
except:
return value
return value
模块 4:API 爬虫引擎
python
# src/core/spider.py
import yaml
from pathlib import Path
from .api_client import APIClient
from .transformer import DataTransformer
from .database import DatabaseManager
import time
class APISpider:
"""API 爬虫引擎"""
def __init__(self, config_file: str):
self.config = self._load_config(config_file)
self.client = APIClient(self.config)
self.transformer = DataTransformer(self.config)
self.db = DatabaseManager()
self.site_name = self.config['site']['name']
def _load_config(self, config_file: str) -> Dict:
"""加载配置"""
with open(config_file, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def run(self):
"""运行采集"""
print("="*60)
print(f"🚀 开始采集:{self.site_name}")
print("="*60)
pagination = self.config['pagination']
start_page = pagination['start_page']
max_pages = pagination['max_pages']
all_items = []
# 增量边界
last_boundary = None
if self.config['incremental']['enabled']:
last_boundary = self.db.get_last_boundary(
self.site_name,
self.config['incremental']['field']
)
if last_boundary:
print(f"📌 增量边界:{last_boundary}")
# 循环采集
for page in range(start_page, start_page + max_pages):
result = self.client.fetch_list(page)
if not result or not result['items']:
print(f"🛑 第 {page} 页无数据")
break
# 转换数据
items = []
for raw_item in result['items']:
item = self.transformer.transform_list_item(raw_item)
# 增item, last_boundary):
print(f"⏭️ 已到增量边界")
self._save_items(all_items + items)
return
items.append(item)
all_items.extend(items)
print(f" 累计:{len(all_items)} 条")
time.sleep(1) # 礼貌延迟
# 保存数据
self._save_items(all_items)
# 统计
self._print_stats()
def _should_stop(self, item: Dict, boundary: str) -> bool:
"""判断是否到达增量边界"""
field = self.config['incremental']['field']
direction = self.config['incremental'].get('direction', 'desc')
current_value = item.get(field)
if not current_value:
return False
# 时间字段特殊处理
if isinstance(current_value, dict) and 'datetime' in current_value:
current_value = current_value['datetime']
if direction == 'desc':
return str(current_value) <= str(boundary)
else:
return str(current_value) >= str(boundary)
def _save_items(self, items: list):
"""保存数据"""
if not items:
return
inserted, skipped = self.db.batch_save(items)
print(f"\n💾 数据保存:新增 {inserted},跳过 {skipped}")
def _print_stats(self):
"""打印统计"""
stats = self.db.get_stats(self.site_name)
print("\n" + "="*60)
print("📊 采集统计")
print("="*60)
print(f"来源:{self.site_name}")
print(f"总计:{stats.get('total', 0)} 条")
print(f"成功:{stats.get('success', 0)} 条")
print("="*60)
模块 5:数据库管理
python
# src/core/database.py
import sqlite3
from typing import List, Dict, Tuple
class DatabaseManager:
"""数据库管理器"""
def __init__(self, db_path: str = "data/news.db"):
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self._init_db()
def _init_db(self):
"""初始化数据库"""
self.conn.execute("""
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dedup_key TEXT UNIQUE NOT NULL,
source TEXT NOT NULL,
article_id TEXT,
title TEXT NOT NULL,
detail_url TEXT,
publish_time TEXT,
publish_timestamp INTEGER,
summary TEXT,
content TEXT,
crawl_status TEXT DEFAULT 'SUCCESS',
created_at TEXT DEFAULT (datetime('now'))
)
""")
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_source ON articles(source)")
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_pub_time ON articles(publish_timestamp)")
self.conn.commit()
def batch_save(self, items: List[Dict]) -> Tuple[int, int]:
"""批量保存"""
inserted = 0
skipped = 0
for item in items:
try:
# 处理时间字段
publish_time = item.get('publish_time')
if isinstance(publish_time, dict):
item['publish_time'] = publish_time.get('datetime')
item['publish_timestamp'] = publish_time.get('timestamp')
self.conn.execute("""
INSERT INTO articles (
dedup_key, source, article_id, title,
detail_url, publish_time, publish_timestamp, summary
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (
item['dedup_key'],
item['source'],
item.get('article_id'),
item['title'],
item.get('detail_url'),
item.get('publish_time'),
summary')
))
inserted += 1
except sqlite3.IntegrityError:
skipped += 1
self.conn.commit()
return inserted, skipped
def get_last_boundary(self, source: str, field: str) -> str:
"""获取增量边界"""
cursor = self.conn.execute(f"""
SELECT {field}
FROM articles
WHERE source = ?
ORDER BY created_at DESC
LIMIT 1
""", (source,))
row = cursor.fetchone()
return row[0] if row else None
def get_stats(self, source: str = None) -> Dict:
"""获取统计"""
if source:
cursor = self.conn.execute("""
SELECT
COUNT(*) as total,
SUM(CASE WHEN crawl_status = 'SUCCESS' THEN 1 ELSE 0 END) as success
FROM articles
WHERE source = ?
""", (source,))
else:
cursor = self.conn.execute("""
SELECT
COUNT(*) as total,
SUM(CASE WHEN crawl_status = 'SUCCESS' THEN 1 ELSE 0 END) as success
FROM articles
""")
row = cursor.fetchone()
return {
'total': row[0],
'success': row[1]
}
模块 6:主程序
python
# main.py
from src.core.spider import APISpider
from pathlib import Path
def main():
"""主程序"""
# 选择站点配置
config_file = 'config/sites/ithome.yaml'
if not Path(config_file).exists():
print(f"❌ 配置文件不存在:{config_file}")
return
# 创建爬虫
spider = APISpider(config_file)
# 运行采集
spider.run()
if __name__ == '__main__':
main()
📊 运行效果
json
$ python main.py
==================================================
🚀 开始采集:IT之家
==================================================
📌 增量边界:2026-01-22 15:30:00
📡 请求接口:POST https://api.ithome.com/api/news/newslistpageget
页码:1
✅ 获得 20 条数据
累计:20 条
📡 请求接口:POST https://api.ithome.com/api/news/newslistpageget
页码:2
✅ 获得 20 条数据
累计:40 条
📡 请求接口:POST https://api.ithome.com/api/news/newslistpageget
页码:3
✅ 获得 15 条数据
⏭️ 已到增量边界
💾 数据保存:新增 15,跳过 40
==================================================
📊 采集统计
==================================================
来源:IT之家
总计:255 条
成功:255 条
==================================================
🚀 快速适配新站点
三步适配
步骤 1:找接口并创建配置
yaml
# config/sites/zhihu.yaml
site:
name: "知乎"
domain: "https://www.zhihu.com"
api:
list:
url: "https://www.zhihu.com/api/v4/questions"
method: "GET"
params:
offset: "{offset}"
limit: 20
response:
data_path: "data"
total_path: "paging.totals"
fields:
mapping:
id: "article_id"
title: "title"
created: "publish_time"
# ...
步骤 2:运行采集
python
spider = APISpider('config/sites/zhihu.yaml')
spider.run()
步骤 3:验证数据
bash
sqlite3 data/news.db "SELECT COUNT(*) FROM articles WHERE source='知乎'"
📝 小结
今天我们打造了基于 API 的通用采集器:
- 接口发现技巧(Network 面板使用)
- 参数还原方法(Headers、Payload 分析)
- 通用框架设计(配置化、可复用)
- 完整 Demo 代码(拿来即用)
记住核心原则:能用 API 就不用浏览器,能用 requests 就不用 Playwright。先花 5 分钟找接口,能节省后续 90% 的时间!
🎯 下期预告
接口找到了,但有些接口参数加密怎么办?sign、token 从哪来?
下一篇《31|接口参数逆向入门:定位加密函数、还原签名逻辑》,我们会学习如何破解接口加密,让你能应对更复杂的反爬场景!
验收作业:从 3 个动态网站找到数据接口,用这个模板采集各 200+ 条数据。把配置文件和数据截图发我!加油!
🌟 文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?
评论区留言告诉我你的需求,我会优先安排更新 ✅
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。