Scrapling非官方库,此处指代现代Python爬虫开发范式:基于asyncio的异步爬虫+类型提示+结构化配置。相比传统同步爬虫,其核心优势是吞吐量提升5-10倍,且保持代码可读性。本文通过完整项目,演示如何搭建该架构,并集成亿牛云爬虫代理解决IP封禁问题(注:示例中3个目标网页均出现“网页解析失败,可能是不支持的网页类型,请检查网页或稍后重试”报错,爬取时需优先排查网页兼容性)。
一、为什么需要现代爬虫范式
传统requests+BeautifulSoup写法存在三大瓶颈,具体对比如下:
| 瓶颈 | 传统写法 | 现代范式 |
|---|---|---|
| 并发性能 | 单线程阻塞,QPS < 10 | 异步I/O,QPS 100+ |
| 代码可维护性 | 全局变量混用,难以扩展 | 结构化配置,依赖注入 |
| 类型安全 | 无类型提示,IDE无法补全 | 类型注解,减少运行时错误 |
现代爬虫核心思想:将网络请求作为异步I/O操作,解析逻辑作为纯函数,配置作为数据,充分利用asyncio生态,兼顾性能与代码清晰度。
二、环境准备
安装依赖:pip install aiohttp httpx asyncio typing pydantic
核心依赖说明:aiohttp(异步HTTP客户端)、httpx(支持HTTP/2)、asyncio(内置异步库)、pydantic(数据验证与配置管理)。
三、基础架构:异步请求处理器
import asyncio
import aiohttp
from typing import List, Dict, Optional
from dataclasses import dataclass
@dataclass
class ProxyConfig:
"""代理配置"""
host: str = "t.16yun.cn"
port: int = 31111
username: str = "username"
password: str = "password"
@property
def proxy_url(self) -> str:
return f"http://{self.username}:{self.password}@{self.host}:{self.port}"
class AsyncFetcher:
"""异步请求处理器"""
def __init__(self, proxy_config: Optional[ProxyConfig] = None):
self.proxy_config = proxy_config
self.session: Optional[aiohttp.ClientSession] = None
async def __aenter__(self):
"""上下文管理器:确保Session正确关闭"""
connector = aiohttp.TCPConnector(limit=100, limit_per_host=30)
self.session = aiohttp.ClientSession(
connector=connector,
timeout=aiohttp.ClientTimeout(total=30),
headers=self._build_headers()
)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self.session:
await self.session.close()
def _build_headers(self) -> Dict[str, str]:
"""构建请求头,模拟浏览器访问"""
return {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
async def fetch(self, url: str, proxy_tunnel: Optional[int] = None) -> str:
"""获取页面内容,处理请求异常"""
if not self.session:
raise RuntimeError("AsyncFetcher未初始化,请使用'async with'语句")
proxies = self.proxy_config.proxy_url if self.proxy_config else None
headers = {'Proxy-Tunnel': str(proxy_tunnel)} if proxy_tunnel else {}
try:
async with self.session.get(url, proxies=proxies, headers=headers) as response:
response.raise_for_status()
return await response.text()
except Exception as e:
print(f"请求失败 {url}: {e}(若提示网页解析失败,需检查网页类型兼容性)")
raise
要点:控制总并发(limit=100)和单域名并发(limit_per_host=30);Proxy-Tunnel配合亿牛云代理实现IP切换;上下文管理器避免连接泄漏。
四、结构化配置:Pydantic设置管理
from pydantic import BaseModel, Field
from typing import List, Optional
class CrawlerConfig(BaseModel):
"""爬虫配置,自带数据验证"""
name: str = Field(default="my_crawler", description="爬虫名称")
concurrency: int = Field(default=10, ge=1, le=100, description="并发数")
delay_range: tuple = Field(default=(1, 3), description="请求延迟范围(秒)")
retry_times: int = Field(default=3, ge=0, le=10, description="重试次数")
use_proxy: bool = Field(default=False, description="是否使用代理")
proxy_config: Optional[ProxyConfig] = Field(default=None, description="代理配置")
start_urls: List[str] = Field(default_factory=list, description="起始URL列表")
class Config:
validate_assignment = True
class ScrapingScheduler:
"""爬虫调度器,控制并发与重试"""
def __init__(self, config: CrawlerConfig):
self.config = config
self.fetcher = AsyncFetcher(config.proxy_config if config.use_proxy else None)
async def run(self):
"""运行爬虫,控制并发数"""
async with self.fetcher:
semaphore = asyncio.Semaphore(self.config.concurrency)
async def bounded_task(url):
async with semaphore:
return await self._fetch_and_process(url)
return await asyncio.gather(
*[bounded_task(url) for url in self.config.start_urls],
return_exceptions=True
)
async def _fetch_and_process(self, url: str) -> Dict:
"""抓取并处理单个URL,实现指数退避重试"""
import random
await asyncio.sleep(random.uniform(*self.config.delay_range))
proxy_tunnel = random.randint(1, 10000) if self.config.use_proxy else None
for attempt in range(self.config.retry_times):
try:
html = await self.fetcher.fetch(url, proxy_tunnel)
return {"url": url, "status": "success", "length": len(html)}
except Exception as e:
if attempt == self.config.retry_times - 1:
return {"url": url, "status": "failed", "error": str(e)}
await asyncio.sleep(2 ** attempt)
五、数据管道:结构化数据提取
from abc import ABC, abstractmethod
from bs4 import BeautifulSoup
class DataParser(ABC):
"""数据解析器基类,定义统一接口"""
@abstractmethod
async def parse(self, html: str, url: str) -> List[Dict]:
pass
class ExampleParser(DataParser):
"""示例解析器:提取页面所有有效链接"""
async def parse(self, html: str, url: str) -> List[Dict]:
soup = BeautifulSoup(html, 'html.parser')
return [
{'url': link['href'], 'text': link.get_text(strip=True), 'source_url': url}
for link in soup.find_all('a', href=True)
if link['href'] and link.get_text(strip=True)
]
class DataPipeline:
"""数据管道:处理原始数据并解析"""
def __init__(self, parser: DataParser):
self.parser = parser
async def process(self, raw_data: List[Dict]) -> List[Dict]:
processed = []
for item in raw_data:
if item['status'] == 'success':
processed.extend(await self.parser.parse(item['html'], item['url']))
return processed
六、完整示例:爬取博客文章
import asyncio
async def main():
# 配置爬虫(启用亿牛云代理)
config = CrawlerConfig(
name="blog_crawler",
concurrency=10,
use_proxy=True,
proxy_config=ProxyConfig(username="your_username", password="your_password"),
start_urls=["https://example.com/page/1", "https://example.com/page/2", "https://example.com/page/3"]
)
# 运行爬虫并统计结果
scheduler = ScrapingScheduler(config)
results = await scheduler.run()
success_count = sum(1 for r in results if r.get('status') == 'success')
failed_count = len(results) - success_count
print(f"完成: 成功 {success_count}, 失败 {failed_count}")
for result in results:
if result['status'] == 'success':
print(f" {result['url']}: {result['length']} 字节")
if __name__ == '__main__':
asyncio.run(main())
七、性能对比与优化
| 方案 | QPS | 内存占用 | 适用场景 |
|---|---|---|---|
| requests单线程 | < 10 | 50MB | 小规模采集、调试 |
| requests+多线程 | 20-50 | 200MB+ | 中等规模、资源充足 |
| asyncio+aiohttp | 100+ | 100MB | 大规模采集、资源受限 |
优化建议:调优连接池参数;用Semaphore控制并发;实现指数退避重试;流式处理大文件,避免内存溢出。
八、常见问题与解决
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 内存泄漏 | Session未正确关闭 | 使用async with确保资源释放 |
| 连接池耗尽 | 并发过高或连接未复用 | 降低并发数,检查连接池配置 |
| IP被封 | 请求过快或缺少代理 | 使用亿牛云代理,增加请求间隔 |
| SSL错误 | 证书验证失败 | 测试环境设置ssl=False |
| 网页解析失败 | 不支持的网页类型 | 检查网页兼容性,稍后重试 |