
本文针对无法通过xhr/js定位json接口的万级分页网站(如哈萨克斯坦动物登记系统),提供基于异步并发、请求批处理与错误韧性设计的高性能爬取方案,显著缩短20,000页×15条数据的采集耗时。
本文针对无法通过xhr/js定位json接口的万级分页网站(如哈萨克斯坦动物登记系统),提供基于异步并发、请求批处理与错误韧性设计的高性能爬取方案,显著缩短20,000页×15条数据的采集耗时。
在实际Web数据采集中,常遇到一类“伪静态”网站:页面HTML由服务端渲染,但关键数据(如表格)不通过独立API返回,也不在初始HTML中明文存在;开发者工具的Network → XHR/Fetch标签下查无有效JSON请求,JS源码中亦无明显的fetch()或axios调用。你正在抓取的 https://www.php.cn/link/283ea714ba84bbb51b605755fe0af8ee 正属此类——它依赖服务端分页生成完整HTML,而非前端动态加载JSON。此时,强行逆向API不可行,必须转向高吞吐HTML解析策略。
✅ 核心优化方向:从串行阻塞到异步并发
你当前的async def scrape_page(...)已迈出关键一步,但若仅用asyncio.gather()逐页提交20,000个协程,仍会因连接池耗尽、DNS阻塞或服务端限流导致性能瓶颈。真正高效的方案需三层协同:
1. 控制并发粒度:使用 aiohttp + 连接池限流
requests 不支持原生异步,应替换为 aiohttp 并配置合理连接池:
import asyncio
import aiohttp
from bs4 import BeautifulSoup
import csv
import logging
BASE_URL = "https://tanba.kezekte.kz/ru/reestr-tanba-public/animal/list?p="
# 全局连接池复用,避免重复创建连接开销
connector = aiohttp.TCPConnector(
limit=50, # 同时最多50个连接
limit_per_host=10, # 单域名最多10连接(防被封)
ttl_dns_cache=300,
keepalive_timeout=30
)
async def fetch_page(session, page):
url = BASE_URL + str(page)
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as resp:
if resp.status == 200:
return await resp.text()
else:
logging.warning(f"HTTP {resp.status} on page {page}")
return None
except Exception as e:
logging.error(f"Request failed for page {page}: {e}")
return None
async def scrape_page(session, page):
html = await fetch_page(session, page)
if not html:
return [], None
soup = BeautifulSoup(html, "html.parser")
table = soup.find("table", {"id": lambda x: x and x.startswith("guid-")})
if not table:
return [], None
titles = [th.get_text(strip=True) for th in table.find_all("th")]
rows = table.find_all("tr")[1:]
data = [
[td.get_text(strip=True) for td in row.find_all("td")]
for row in rows
]
return data, titles2. 批量调度:分块并发 + 进度反馈
避免一次性提交全部20,000任务。按每100页为一个批次,完成一批再启下一批,并写入CSV流式保存:
async def scrape_batch(session, start_page, end_page, writer, lock):
tasks = [scrape_page(session, p) for p in range(start_page, end_page + 1)]
results = await asyncio.gather(*tasks, return_exceptions=True)
for i, (data, titles) in enumerate(results):
page_num = start_page + i
if isinstance(data, list) and data and titles:
async with lock:
# 写入标题(仅首行)
if page_num == 1:
writer.writerow(titles)
# 写入数据行
for row in data:
writer.writerow(row)
logging.info(f"✅ Batch {start_page}-{end_page} completed.")
async def main():
total_pages = 20_000
batch_size = 100
lock = asyncio.Lock()
async with aiohttp.ClientSession(connector=connector) as session:
with open("tanba_animals.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
# 分批执行
for start in range(1, total_pages + 1, batch_size):
end = min(start + batch_size - 1, total_pages)
await scrape_batch(session, start, end, writer, lock)
await asyncio.sleep(0.5) # 批次间轻量休眠,降低服务端压力
logging.info("? All pages scraped successfully.")
# 启动
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())3. 关键增强项(生产必备)
- User-Agent轮换:添加随机UA头,避免被识别为爬虫;
-
重试机制:对失败请求自动重试2次(
aiohttp.RetryClient); - 代理池集成:当遭遇IP封锁时,可无缝接入付费/自建HTTP代理;
- 结果校验:每批次校验记录数是否恒为15(异常缺失时告警);
- 断点续爬:记录最后成功页码至文件,崩溃后从中断处恢复。
⚠️ 法律与伦理提醒:该站点为哈萨克斯坦政府公开数据平台(
.kz域),请务必检查其/robots.txt(当前返回404,但需人工确认)及页面页脚版权说明;建议添加headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"}并将请求间隔控制在 ≥1秒/页(本方案通过连接池+批次休眠已满足)。
综上,放弃寻找不存在的“隐藏API”,转而以工程化思维构建健壮、可监控、可伸缩的异步爬取流水线,才是应对万级静态分页网站的最优解。Scrapy虽更成熟,但对单目标、强定制化场景,纯aiohttp+BeautifulSoup组合更轻量、调试更直观,且性能差距在合理配置下可忽略。

















