1. 项目概述:从零构建一个现代网络爬虫框架
最近在做一个数据采集相关的项目,发现市面上的爬虫工具要么太重,要么太轻,要么就是配置起来极其繁琐。于是,我萌生了自己动手写一个爬虫框架的想法,我把它命名为
xcrawl
。这个名字里的 “x” 可以理解为 “扩展”、“高效” 或者 “未知”,寓意着它应该具备强大的可扩展性和处理未知复杂场景的能力。我的核心目标不是再造一个
Scrapy
那样的庞然大物,而是打造一个轻量、灵活、对开发者友好的现代爬虫框架,尤其适合处理那些结构多变、需要动态调整采集策略的网站。
简单来说,
xcrawl
希望解决几个痛点:第一,配置要足够简单直观,最好能用声明式的方式定义爬取规则,减少样板代码;第二,要有良好的异步支持,能轻松应对高并发请求,提升采集效率;第三,内置常见的反爬应对策略,如自动切换用户代理、请求延迟、失败重试等,让开发者能更专注于业务逻辑;第四,提供清晰的数据流管道,方便对抓取到的数据进行清洗、验证和存储。如果你也经常需要写爬虫,但又觉得现有工具用起来不够顺手,或者想深入理解一个爬虫框架是如何从零搭建的,那么这篇分享或许能给你一些启发。
2. 核心设计理念与架构拆解
2.1 为什么选择异步架构作为核心?
在决定
xcrawl
的技术栈时,我首先考虑的就是并发模型。传统的多线程爬虫在 I/O 密集型任务(网络请求)中,线程切换的开销和资源占用是个大问题。而 Python 的
asyncio
库提供了基于协程的异步 I/O 支持,能够在单个线程内处理成千上万个网络连接,这对于爬虫这种大量时间花在等待网络响应上的应用来说,是天然的绝配。
因此,
xcrawl
的核心将完全构建在
asyncio
之上。这意味着框架的引擎、下载器、调度器等核心组件都是异步的。这样做的好处显而易见:极高的资源利用率和吞吐量。一个简单的对比是,一个配置合理的
xcrawl
项目,在一台普通开发机上,同时维持数百个活跃的请求连接是轻而易举的,而如果用同步方式,可能几十个线程就足以让程序变得笨重不堪。
当然,异步编程也带来了复杂性,比如需要处处使用
async/await
,要小心避免阻塞事件循环。但在框架层面,我们可以把这些复杂性封装起来,对外提供相对简单的接口。例如,用户只需要用
async def
定义自己的页面解析函数,框架会自动在事件循环中调度执行,用户无需手动管理协程或事件循环。
2.2 模块化与插件化设计思路
一个健壮的框架不应该是一个铁板一块的黑盒,而应该像乐高积木一样,允许用户根据需要替换或增强其中的部件。
xcrawl
采用了高度模块化的设计,主要分为以下几个核心模块:
- 引擎 (Engine) :这是框架的大脑,负责协调调度器、下载器、爬虫中间件、下载器中间件等所有组件的工作流程。它控制着整个爬取任务的启动、暂停、恢复和停止。
- 调度器 (Scheduler) :负责管理待抓取的请求队列。它决定下一个要发出的请求是什么。一个简单的实现是内存队列,但我们可以设计接口,允许接入基于 Redis 的分布式队列,为将来扩展成分布式爬虫留出可能。
-
下载器 (Downloader)
:基于
aiohttp或httpx等异步 HTTP 客户端库封装,负责发送 HTTP 请求并接收响应。它会集成连接池、超时控制、自动重试等基础功能。 - 爬虫 (Spider) :这是用户编写业务逻辑的地方。用户在这里定义起始 URL,并编写解析响应的回调函数,从页面中提取数据和新的链接。
- 数据管道 (Item Pipeline) :处理爬虫提取出来的数据项。典型的操作包括数据清洗(去重、格式化)、验证(检查字段完整性)和持久化(保存到数据库、文件或消息队列)。
- 中间件 (Middleware) :这是插件化的关键。分为爬虫中间件和下载器中间件,它们像钩子一样嵌入到请求-响应生命周期中。用户可以通过中间件实现全局的请求头修改、代理设置、自定义异常处理、响应预处理等功能。
这种设计使得
xcrawl
的每个部分都可以独立演进和替换。比如,如果你觉得内置的下载器不够快,完全可以自己实现一个基于
uvloop
和
httpx
的高性能版本替换掉默认的。
2.3 配置与规则的定义方式
为了让用户快速上手,
xcrawl
倾向于使用 Python 类和数据类来声明式地定义爬虫规则。一个最简单的爬虫可能长这样:
from xcrawl import Spider, Request, Item
from dataclasses import dataclass
@dataclass
class ArticleItem(Item):
title: str
author: str
publish_time: str
content: str
class BlogSpider(Spider):
name = "blog_spider"
start_urls = ["https://example.com/blog"]
async def parse(self, response):
# 使用 CSS 选择器或 XPath 解析列表页
article_links = response.css('h2.post-title a::attr(href)').getall()
for link in article_links:
yield Request(url=response.urljoin(link), callback=self.parse_article)
# 翻页
next_page = response.css('a.next-page::attr(href)').get()
if next_page:
yield Request(url=response.urljoin(next_page), callback=self.parse)
async def parse_article(self, response):
item = ArticleItem(
title=response.css('h1.entry-title::text').get().strip(),
author=response.css('span.author a::text').get(),
publish_time=response.css('time.published::attr(datetime)').get(),
content=''.join(response.css('div.entry-content > p::text').getall())
)
yield item
通过继承
Spider
类并定义清晰的回调方法,爬虫的逻辑一目了然。
Item
类利用数据类,不仅定义了数据结构,还能结合框架实现自动的类型验证和序列化。这种设计比在配置文件中写一大堆字符串规则要直观和强大得多,也更容易利用 IDE 的代码提示和静态检查功能。
3. 核心组件深度实现解析
3.1 异步引擎的事件循环调度
引擎是框架最复杂的部分,它需要在一个主事件循环中,优雅地协调所有异步任务。其核心循环逻辑可以简化为以下步骤:
-
初始化
:引擎启动,初始化所有组件(调度器、下载器、中间件、管道),并启动爬虫的
start_requests方法生成初始请求。 -
请求循环
:
a. 从调度器获取下一个待处理的请求。
b. 将请求依次通过所有
下载器中间件
的
process_request方法。中间件可以在这里修改请求(如添加代理、更换UA)、或者直接返回一个响应(用于缓存场景)或异常。 c. 将处理后的请求交给下载器执行,这是一个异步网络调用。 d. 下载器返回响应或异常后,再依次通过下载器中间件的process_response或process_exception方法。 e. 将最终的响应或异常,连同原始请求,传递给对应的爬虫回调函数(parse等)进行处理。 -
结果处理
:爬虫回调函数执行后,会
yield出新的Request对象或Item对象。-
如果是
Request,则将其放回调度器,等待下一轮抓取。 -
如果是
Item,则将其送入 数据管道 进行后续处理。
-
如果是
- 循环与终止 :重复步骤2,直到调度器为空,并且所有进行中的请求和回调都已完成。引擎随后关闭所有组件,结束运行。
这里的关键是,所有步骤都是非阻塞的。当下载器在等待网络响应时,事件循环可以去处理其他已经返回的响应,或者执行爬虫的解析函数,从而实现极高的并发效率。引擎需要维护一个计数器来跟踪当前活跃的请求数,防止并发数过高把目标服务器或自身压垮。
注意 :在实现引擎时,要特别注意异常处理。任何一个环节的未捕获异常都可能导致整个事件循环崩溃。必须确保每个异步任务都有完善的
try...except,并将错误信息妥善记录,同时不影响其他任务的执行。
3.2 下载器的稳健性增强策略
下载器不能只是一个简单的
aiohttp
封装,它必须足够健壮以应对恶劣的网络环境和网站的反爬措施。
xcrawl
的下载器内置了以下策略:
- 连接池与会话复用 :为每个域名创建独立的连接池和客户端会话,复用 TCP 连接,大幅减少建立 SSL 握手和连接的开销。
-
智能重试机制
:对于网络超时、连接错误、服务器返回 5xx 状态码等情况,自动进行重试。重试策略可以配置,例如指数退避延迟:第一次重试等待 1 秒,第二次 2 秒,第三次 4 秒,以此类推。
# 简化的指数退避实现 async def fetch_with_retry(self, request, retries=3): for attempt in range(retries): try: return await self._download(request) except (TimeoutError, ClientError) as e: if attempt == retries - 1: raise delay = (2 ** attempt) + random.random() # 加入随机抖动 await asyncio.sleep(delay) -
自动请求头管理
:内置一个常见的用户代理字符串池,并在每次请求时随机选取,避免使用单一 UA 被识别。同时,可以配置自动添加
Referer等常见头信息。 -
并发与延迟控制
:全局控制每秒发出的请求数(QPS)以及针对特定域名的并发连接数。这是遵守
robots.txt和避免被封 IP 的基本礼仪。实现上可以使用异步信号量 (asyncio.Semaphore) 来控制并发,使用异步睡眠来控制频率。 -
响应解码与字符集探测
:自动根据 HTTP 头或 HTML 元标签识别页面编码,并正确解码为 Unicode 字符串。对于乱码页面,可以尝试
chardet库进行探测。
3.3 数据管道的灵活性与效率
数据管道是数据流出爬虫前的最后一道关卡。
xcrawl
的管道系统设计为可插拔的组件序列。每个管道组件只需实现
process_item
异步方法。一个典型的管道处理流程如下:
- 去重管道 :基于 Item 的某个或某几个字段(如文章ID、URL)计算指纹,利用内存集合或 Redis 进行判重,丢弃重复数据。
- 验证管道 :利用 Item 数据类的类型注解,自动检查字段类型是否正确,必填字段是否缺失。也可以编写自定义验证规则。
-
清洗管道
:对字段进行格式化处理,例如去除字符串首尾空白、将日期字符串转换为
datetime对象、过滤掉无意义的占位符等。 -
存储管道
:将处理好的 Item 持久化。框架可以提供多种内置存储后端:
- JSON Lines 文件 :轻量,适合调试和中小规模数据。
-
MySQL/PostgreSQL
:通过异步数据库驱动(如
aiomysql,asyncpg)进行批量插入,提升效率。 - MongoDB :利用其无模式特性,灵活存储半结构化数据。
- 消息队列(如 RabbitMQ, Kafka) :将数据作为事件发出,供下游实时处理系统消费。
管道的强大之处在于其组合性。用户可以根据需要在配置中启用和排序管道。例如,一个简单的配置可能是
['DeduplicationPipeline', 'ValidationPipeline', 'JsonLinesPipeline']
。所有管道组件都异步执行,不会阻塞爬虫的主循环。
4. 实战:构建一个完整的新闻网站爬虫
4.1 目标分析与爬虫定义
假设我们需要抓取一个新闻网站(例如一个科技媒体)的最新文章。目标数据包括:文章标题、正文、发布时间、作者、分类标签以及文章内的图片链接。
首先,我们定义数据模型和爬虫类:
from xcrawl import Spider, Request, Item
from dataclasses import dataclass
from typing import Optional, List
@dataclass
class NewsItem(Item):
url: str # 文章链接,作为唯一标识
title: str
content: str
publish_time: str # 先保留为字符串,后续可转换
author: Optional[str] = None
tags: List[str] = None
image_urls: List[str] = None
class TechNewsSpider(Spider):
name = "tech_news"
# 起始页面可以是网站首页或新闻列表页
start_urls = ["https://www.example-tech-news.com/latest"]
# 自定义设置,例如针对该域名的并发限制
custom_settings = {
'CONCURRENT_REQUESTS_PER_DOMAIN': 2, # 对该域名并发为2
'DOWNLOAD_DELAY': 1.0, # 每次请求间隔1秒
}
4.2 列表页解析与详情页调度
列表页通常包含文章摘要和链接。我们的
parse
方法需要提取这些链接并生成新的请求指向详情页。
async def parse(self, response):
"""
解析新闻列表页
"""
# 假设列表项由 <article class="news-item"> 包裹
news_items = response.css('article.news-item')
for item in news_items:
# 提取详情页相对链接
detail_url = item.css('h2 a::attr(href)').get()
if detail_url:
# 构建绝对URL并生成请求,指定回调函数为 parse_detail
absolute_url = response.urljoin(detail_url)
yield Request(url=absolute_url, callback=self.parse_detail)
# 翻页逻辑
next_page_url = response.css('a.pagination-next::attr(href)').get()
if next_page_url:
yield Request(url=response.urljoin(next_page_url), callback=self.parse)
这里使用了
response.css
,这是框架内置的基于
parsel
库的快捷方式,它返回一个选择器列表,支持链式调用。
response.urljoin()
方法能智能地拼接相对 URL 和基础 URL,避免链接错误。
4.3 详情页数据提取与 Item 生成
在详情页回调函数中,我们提取具体字段并生成
NewsItem
。
async def parse_detail(self, response):
"""
解析新闻详情页
"""
# 使用更精确的选择器定位元素
item = NewsItem(
url=response.url,
title=response.css('h1.article-title::text').get('').strip(),
# 获取所有段落文本,并用换行符连接
content='\n'.join(response.css('div.article-content p::text').getall()).strip(),
publish_time=response.css('time.pub-date::attr(datetime)').get() or
response.css('span.publish-date::text').get('').strip(),
author=response.css('span.author-name::text').get(),
tags=response.css('div.tags a.tag::text').getall(),
# 提取文章内容中的图片
image_urls=[response.urljoin(img) for img in response.css('div.article-content img::attr(src)').getall()]
)
# 可以在这里进行一些简单的现场清洗
if item.tags is None:
item.tags = []
yield item
注意,字段提取时都提供了默认值(
get('')
或
getall()
),这是为了防止因为页面结构微调导致的选择器返回
None
而引发错误。对于时间字段,我们优先获取机器可读的
datetime
属性。
4.4 配置与运行爬虫
最后,我们需要创建一个主文件来配置和启动爬虫。这里我们启用去重管道和 JSON 存储管道。
# main.py
import asyncio
from xcrawl import Engine, CrawlerProcess
from xcrawl.pipelines import DeduplicationPipeline, JsonLinesItemPipeline
from my_spiders import TechNewsSpider # 假设爬虫写在 my_spiders 模块
async def main():
# 定义项目设置
settings = {
'USER_AGENT': 'xcrawl/1.0 (+https://my-crawler-info)',
'CONCURRENT_REQUESTS': 16, # 全局总并发数
'DOWNLOAD_TIMEOUT': 30,
'RETRY_TIMES': 2,
'ITEM_PIPELINES': {
DeduplicationPipeline: 100, # 数字代表优先级,越小越先执行
JsonLinesItemPipeline: 200,
},
'JSONLINES_OUTPUT_FILE': './output/news.jsonl',
}
# 创建爬虫进程
process = CrawlerProcess(settings)
# 添加爬虫
await process.crawl(TechNewsSpider)
# 启动(这会阻塞直到所有爬虫任务完成)
await process.start()
if __name__ == '__main__':
asyncio.run(main())
运行
python main.py
,爬虫就会开始工作,将抓取到的数据去重后,一行一个 JSON 对象保存到
news.jsonl
文件中。
5. 高级特性与反爬应对实战
5.1 动态渲染页面的处理
现代网站大量使用 JavaScript 动态加载内容,简单的 HTTP 请求无法获取完整页面。
xcrawl
可以通过集成无头浏览器来解决。这里我们选择
playwright
作为渲染引擎,因为它异步友好,且支持多种浏览器。
我们需要创建一个特殊的下载器中间件或直接一个替代的下载器:
from xcrawl.downloadermiddlewares import DownloaderMiddleware
from playwright.async_api import async_playwright
class PlaywrightDownloaderMiddleware(DownloaderMiddleware):
"""
使用 Playwright 渲染页面的下载器中间件。
注意:此中间件会显著降低爬取速度,仅对需要JS的请求启用。
"""
def __init__(self, crawler):
super().__init__(crawler)
self.playwright = None
self.browser = None
self.context = None
async def open_spider(self):
# 爬虫启动时,初始化浏览器
self.playwright = await async_playwright().start()
# 使用 Chromium,可配置为 headless=True (无头模式)
self.browser = await self.playwright.chromium.launch(headless=True)
self.context = await self.browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 ...' # 设置浏览器UA
)
async def close_spider(self):
# 爬虫关闭时,清理资源
if self.context:
await self.context.close()
if self.browser:
await self.browser.close()
if self.playwright:
await self.playwright.stop()
async def process_request(self, request, spider):
# 检查请求是否需要渲染。可以通过request.meta中的一个标志位来判断
if request.meta.get('render', False):
page = await self.context.new_page()
try:
# 导航到页面,等待网络空闲或特定元素出现
await page.goto(request.url, wait_until='networkidle')
# 可以在这里执行滚动、点击等操作来触发加载
# await page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
# await page.wait_for_timeout(2000)
# 获取渲染后的HTML内容
content = await page.content()
# 构建一个包含HTML的Response对象,返回给引擎,引擎将跳过默认下载器
from xcrawl.http import HtmlResponse
return HtmlResponse(
url=page.url,
body=content.encode('utf-8'),
request=request
)
finally:
await page.close()
# 如果不需要渲染,返回None,让请求继续走默认下载流程
return None
在爬虫中,你可以为特定的请求标记需要渲染:
yield Request(url=dynamic_page_url, callback=self.parse, meta={'render': True})
5.2 代理IP池的集成与管理
对于需要大规模抓取或目标网站有严格IP限制的情况,代理IP池是必需品。
xcrawl
可以通过下载器中间件无缝集成代理。
class ProxyMiddleware(DownloaderMiddleware):
"""
代理中间件,从IP池获取代理并设置给请求。
"""
def __init__(self, crawler):
super().__init__(crawler)
self.proxy_pool_url = crawler.settings.get('PROXY_POOL_URL', 'http://your-proxy-pool-service/get')
async def fetch_proxy(self):
"""从代理池获取一个代理地址。"""
async with aiohttp.ClientSession() as session:
try:
async with session.get(self.proxy_pool_url) as resp:
if resp.status == 200:
data = await resp.json()
return data.get('proxy') # 假设返回格式为 {"proxy": "http://ip:port"}
except Exception:
pass
return None
async def process_request(self, request, spider):
# 如果请求已经设置了代理,或者明确要求不使用代理,则跳过
if 'proxy' in request.meta or request.meta.get('dont_proxy', False):
return
proxy = await self.fetch_proxy()
if proxy:
request.meta['proxy'] = proxy
spider.logger.debug(f'Using proxy: {proxy} for {request.url}')
else:
spider.logger.warning('No proxy available from pool.')
同时,你还需要一个处理代理失效的机制。这可以在下载器的异常处理或另一个中间件中实现,当请求因代理失败时,从请求的
meta
中移除该代理,并可能将代理标记为失效回馈给代理池。
5.3 基于规则的自动限速与礼貌爬取
遵守
robots.txt
并对目标网站友好是长期稳定爬取的基础。
xcrawl
可以集成
reppy
或
robotexclusionrulesparser
这样的库来解析
robots.txt
。
更精细的限速可以通过自定义的调度器或下载器中间件实现。例如,实现一个基于域的延迟中间件:
from collections import defaultdict
import asyncio
import time
class AutoThrottleMiddleware(DownloaderMiddleware):
"""
自动限速中间件,根据服务器响应动态调整请求延迟。
"""
def __init__(self, crawler):
super().__init__(crawler)
self.domain_delays = defaultdict(list) # 记录每个域名的最近响应时间
self.max_history = 5 # 记录最近5次响应
async def process_request(self, request, spider):
domain = request.url.split('/')[2] # 简单提取域名
last_delays = self.domain_delays.get(domain, [])
if len(last_delays) >= self.max_history:
# 计算平均响应时间,并以此为基础设置延迟
avg_delay = sum(last_delays) / len(last_delays)
# 礼貌性延迟可以设为平均响应时间的0.5到1.5倍
throttle_delay = avg_delay * 1.0
spider.logger.debug(f'Throttling request to {domain} for {throttle_delay:.2f}s')
await asyncio.sleep(throttle_delay)
async def process_response(self, request, response, spider):
# 记录请求的响应时间(这里简化处理,实际应从请求开始时间计算)
# 我们可以通过request.meta记录开始时间
start_time = request.meta.get('download_start_time')
if start_time:
delay = time.time() - start_time
domain = request.url.split('/')[2]
history = self.domain_delays[domain]
history.append(delay)
if len(history) > self.max_history:
history.pop(0) # 保持固定长度
return response
这个中间件会在请求发出前,根据该域名过去的平均响应时间动态等待一段时间,模拟人类浏览的间隔,既礼貌又能自适应不同服务器的响应速度。
6. 部署、监控与性能调优
6.1 将爬虫部署为长期运行的服务
开发调试完成后,你可能需要爬虫7x24小时运行。这时,需要将其部署到服务器上。推荐使用
systemd
或
supervisor
来管理进程。
一个简单的
supervisor
配置 (
xcrawl_news.conf
) 可能如下:
[program:xcrawl_news]
command=/path/to/your/venv/bin/python /path/to/your/main.py
directory=/path/to/your/project
user=your_username
autostart=true
autorestart=true
startsecs=10
stopwaitsecs=600
stdout_logfile=/var/log/xcrawl/news_stdout.log
stderr_logfile=/var/log/xcrawl/news_stderr.log
environment=PYTHONPATH="/path/to/your/project",PATH="/path/to/your/venv/bin:%(ENV_PATH)s"
使用
supervisorctl
可以方便地启动、停止、重启和查看日志。此外,将爬虫容器化(Docker)也是现代部署的佳选,能更好地隔离环境。
6.2 实现简单的状态监控与告警
爬虫在无人值守运行时,我们需要知道它的状态。可以在爬虫内部集成简单的状态上报,或者通过外部监控来实现。
内部状态上报
:可以在爬虫的
close_spider
方法或自定义扩展中,将最终统计信息(抓取数量、错误数等)发送到监控平台(如 Prometheus PushGateway)或写入数据库。
外部进程监控
:使用
supervisor
或
systemd
本身就有基本的进程存活监控。可以搭配
crontab
定时运行一个检查脚本,检查爬虫日志最后更新时间、输出文件是否增长等,如果异常则发送邮件或钉钉告警。
日志是关键
:确保爬虫的日志配置完善,不同级别(DEBUG, INFO, WARNING, ERROR)的日志输出到不同文件或进行分割。使用 Python 的
logging
模块,并合理设置日志格式,包含时间、日志级别、爬虫名、消息等。
6.3 性能瓶颈分析与调优要点
当爬虫速度达不到预期时,可以从以下几个维度排查:
-
网络 I/O
:这是最常见的瓶颈。使用
CONCURRENT_REQUESTS和CONCURRENT_REQUESTS_PER_DOMAIN设置并发上限。过高的并发可能导致本地端口耗尽或目标服务器拒绝服务。通过DOWNLOAD_DELAY和AutoThrottle中间件进行限速。 -
解析效率
:HTML 解析(
parsel/lxml)通常是 CPU 操作。如果页面非常复杂,解析函数写得低效,会成为瓶颈。优化选择器,避免使用复杂的 XPath 或嵌套过深的 CSS 选择器。对于大量重复的页面结构,可以考虑将解析逻辑编译成预定义的模式。 -
内存使用
:检查是否有内存泄漏。确保请求和响应对象在不再需要时能被垃圾回收。特别是在处理大量数据时,避免在内存中堆积所有 Item,应尽快通过管道持久化。使用异步生成器 (
async for,yield) 可以有效控制内存。 - 数据库/存储 I/O :如果管道是写入数据库,数据库可能成为瓶颈。使用连接池、批量插入(而不是逐条插入)、异步数据库驱动来优化。对于文件写入,确保是异步非阻塞的。
-
调试工具
:使用
cProfile或py-spy等性能分析工具,找到代码中的热点函数。xcrawl框架本身也可以加入统计信息,如每秒请求数、平均响应时间等,帮助定位问题。
一个实用的技巧是,在开发初期,将并发数设低,延迟设高,稳定运行一段时间。然后逐步提高并发,观察错误率和服务器响应时间的变化,找到一个性能与稳定性兼顾的平衡点。
7. 常见问题排查与经验心得
7.1 高频错误代码与解决方案速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
TimeoutError
或连接超时
|
1. 目标服务器不稳定或封锁。
2. 网络问题。 3. 代理失效。 4. 并发过高,本地资源不足。 |
1. 增加
DOWNLOAD_TIMEOUT
。
2. 启用重试机制。 3. 检查代理IP是否可用。 4. 降低
CONCURRENT_REQUESTS
,检查服务器负载。
|
返回
403 Forbidden
|
1. 请求头(如 User-Agent)被识别为爬虫。
2. IP 被封锁。 3. 需要 Cookies 或特定 Header。 |
1. 轮换 User-Agent,添加常见浏览器 Header(Accept, Accept-Language等)。
2. 使用代理IP池。 3. 模拟登录获取 Cookies,或在中间件中维护会话。 |
| 解析不到数据(选择器返回空) |
1. 页面结构已更新,选择器失效。
2. 页面是动态加载的,初始HTML中无数据。 3. 编码问题导致乱码。 |
1. 重新审查页面HTML结构,更新选择器。
2. 使用
Playwright
或
Selenium
渲染页面。
3. 检查响应编码,使用
response.text
而非
response.body
。
|
| 爬虫卡住,不再产生新请求 |
1. 调度器队列空,且无新请求生成。
2. 回调函数中有阻塞操作卡住了事件循环。 3. 异步任务出现未处理的异常。 |
1. 检查
parse
方法是否遗漏了
yield Request
。
2. 排查代码中是否有
time.sleep()
(应用
asyncio.sleep
替代)或同步的耗时IO操作。
3. 查看错误日志,确保所有
async def
函数内部都妥善处理了异常。
|
| 内存占用持续增长 |
1. 数据管道处理慢,Item 在内存中堆积。
2. 有全局变量或缓存未及时清理。 3. 产生了循环引用。 |
1. 优化管道,特别是数据库写入,考虑批量操作。
2. 避免在爬虫类属性中存储大量数据。使用
weakref
或定期清理。
3. 使用内存分析工具(如
objgraph
,
tracemalloc
)定位泄漏点。
|
7.2 来自实战的避坑指南与技巧
-
选择器的稳健性
:不要依赖过于脆弱的选择器,比如绝对路径
div[1]/table[2]/tr[3]/td[1]。尽量使用具有语义化的 class 或 id,或者相对路径。多使用get()和getall()的默认值参数。对于重要的数据字段,可以准备多个备选选择器,依次尝试。 -
处理登录与会话
:对于需要登录的网站,建议单独写一个“登录爬虫”,只负责获取并保存 Cookies。主爬虫在启动时,可以读取这些 Cookies 并加载到请求会话中。使用
aiohttp.ClientSession可以自动管理 Cookies。 -
分布式爬虫的思考
:当单机性能达到瓶颈,需要考虑分布式。
xcrawl的架构设计允许将调度器(如使用 Redis)和去重器(如使用 Redis Set)移到外部,实现多台爬虫节点协同工作。此时,需要确保 Item 的 ID 生成规则全局唯一。 -
尊重网站与法律法规
:始终检查目标网站的
robots.txt,遵守其中定义的爬取延迟和禁止目录。控制爬取速度,避免对目标网站造成压力。清晰标注你的爬虫 User-Agent,并提供一个联系方式。最重要的是,确保你的爬取行为和数据用途符合相关法律法规和服务条款。 -
数据清洗宜早不宜迟
:尽量在管道的最前端进行数据清洗和验证。一个脏数据或格式错误的数据项如果流到后面的管道(比如数据库写入),可能导致整个管道失败。定义清晰的
Item数据类并利用类型注解,能在早期发现很多问题。
构建和维护一个爬虫框架就像养一盆植物,它需要持续的关注和调整。网站会改版,反爬策略会升级,网络环境会波动。
xcrawl
的设计初衷就是提供足够的灵活性和扩展性,让你能快速适应这些变化,把精力更多地花在数据本身,而不是与工具搏斗上。
3164

被折叠的 条评论
为什么被折叠?



