Python Crawler4AI 下载 PDF:修复 wait_for 错误与 JS 点击
2025-04-19 17:27:59
用 Crawler4AI 下载 PDF?搞定 JavaScript 点击和 wait_for
报错
碰到了个有点意思的问题:想用 Python 配合 Crawler4AI 这个库,从某个网站 (Humdata PDF Repository) 批量下载一堆 PDF 文件。麻烦在于,这些下载链接似乎需要执行点 JavaScript 才能工作,而直接用 Crawler4AI 文档里的代码,改了改 JavaScript 部分,却报了个错。
原代码大概长这样:
from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig
from crawl4ai import AsyncWebCrawler
import os, asyncio
from pathlib import Path
async def download_multiple_files(url: str, download_path: str):
# 配置浏览器允许下载,并指定路径
config = BrowserConfig(accept_downloads=True, downloads_path=download_path)
async with AsyncWebCrawler(config=config) as crawler:
# 配置爬虫运行参数
run_config = CrawlerRunConfig(
# 注入 JS 代码,找到所有标题为 'Download' 的链接并点击
js_code="""
const downloadLinks = document.querySelectorAll(
"a[title='Download']",
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
for (const link of downloadLinks) {
link.click();
}
""",
wait_for=10 # 等待 10 秒,期望下载开始
)
result = await crawler.arun(url=url, config=run_config)
# 检查下载结果
if result.downloaded_files:
print("Downloaded files:")
for file in result.downloaded_files:
print(f"- {file}")
else:
print("No files downloaded.")
# 设置下载路径
download_path = os.path.join(Path.cwd(), "Downloads")
os.makedirs(download_path, exist_ok=True) # 确保目录存在
# 运行主函数
asyncio.run(download_multiple_files("https://data.humdata.org/dataset/repository-for-pdf-files", download_path))
结果运行时,抛出了这样的错误:
× Unexpected error in _crawl_web at line 1551 in _crawl_web (.venv\Lib\site- │
│ packages\crawl4ai\async_crawler_strategy.py): │
│ Error: Wait condition failed: 'int' object has no attribute 'strip' │
│ │
│ Code context: │
│ 1546 try: │
│ 1547 await self.smart_wait( │
│ 1548 page, config.wait_for, timeout=config.page_timeout │
│ 1549 )
│
│ 1550 except Exception as e:
│
│ 1551 → raise RuntimeError(f"Wait condition failed: {str(e)}")
│
│ 1552
│
│ 1553 # Update image dimensions if needed
│
│ 1554 if not self.browser_config.text_mode:
│
│ 1555 update_image_dimensions_js = load_js_script("update_image_dimensions")
│
│ 1556 try:
│
看报错信息 Wait condition failed: 'int' object has no attribute 'strip'
,问题出在 crawl4ai
内部处理 wait_for
参数的 smart_wait
函数里。它似乎尝试对一个整数(我们设置的 10
)执行字符串的 strip()
方法,自然就报错了。
用户也确认了,根据目标网站的 robots.txt
文件,爬取这个页面是被允许的,只需要遵守 Crawl-Delay: 10
的建议。
User-agent: *
Disallow: /dataset/rate/
Disallow: /revision/
Disallow: /dataset/*/history
Disallow: /api/
Disallow: /user/*/api-tokens
Crawl-Delay: 10
目标是下载数据集里的所有 PDF。咱们来看看怎么解决这个 wait_for
的问题,并且让 PDF 下载顺利进行。
问题分析
- 核心错误 :
TypeError: 'int' object has no attribute 'strip'
。这个错误明确指向CrawlerRunConfig
中的wait_for=10
参数。Crawler4AI
内部的smart_wait
函数在处理这个整数类型的等待条件时,可能预期的是一个字符串(比如 CSS 选择器或 JavaScript 表达式),或者是处理整数等待时存在 bug,误用了字符串方法。 - JavaScript 点击策略 :虽然
robots.txt
允许爬取,但通过 JS 模拟点击来触发下载,有时不是最稳妥的方法。浏览器处理多个并发下载、页面状态变化、网络延迟等因素都可能导致行为不如预期。即使wait_for
参数没问题,这种方法也可能漏掉文件或遇到其他奇怪问题。 - 等待机制的意义 :
wait_for=10
的本意是给浏览器一点时间,让 JavaScript 点击生效,然后下载可以开始。但这个机制内部似乎出了岔子。 - 更好的下载方式? :模拟点击并非唯一途径。如果我们能直接拿到 PDF 文件的 URL,是不是可以绕过浏览器点击,用更可靠的 HTTP 请求库来下载?这通常更稳定、资源消耗更少。
解决方案
针对上面分析的原因,我们提供两种思路来解决问题。
方案一:调整等待策略,绕开 wait_for
整数问题
既然错误发生在 smart_wait
处理整数 wait_for
时,咱们可以试试不直接用这个功能,而是采用别的等待方式。
原理和作用
这个方案的核心思路是:避免 向 smart_wait
函数直接传递整数 10
。我们去掉 CrawlerRunConfig
里的 wait_for
参数,让 crawler.arun
执行 JS 点击动作。然后,我们在 arun
调用 之后,手动加一个异步等待 (asyncio.sleep
),给浏览器下载留出足够的时间。
这种方式绕过了 smart_wait
内部对整数参数的处理逻辑,从而避开了那个 TypeError
。同时,它仍然尝试给下载操作提供时间窗口。
操作步骤和代码示例
- 修改
download_multiple_files
函数。 - 从
CrawlerRunConfig
中移除wait_for=10
。 - 在
result = await crawler.arun(...)
这一行 之后,添加await asyncio.sleep(...)
。等待时间可以根据网络情况和文件数量调整,比如设置 15 或 20 秒试试看。
from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig
from crawl4ai import AsyncWebCrawler
import os, asyncio
from pathlib import Path
async def download_multiple_files_v1(url: str, download_path: str):
print("尝试方案一:移除 wait_for,使用 asyncio.sleep")
config = BrowserConfig(accept_downloads=True, downloads_path=download_path)
async with AsyncWebCrawler(config=config) as crawler:
run_config = CrawlerRunConfig(
js_code="""
const downloadLinks = document.querySelectorAll("a[title='Download']");
console.log(`找到 ${downloadLinks.length} 个下载链接。`); // 加点日志输出
let clickedCount = 0;
for (const link of downloadLinks) {
try {
// 稍微模拟下真实点击行为,或许增加点击成功率
link.focus(); // 先聚焦
await new Promise(resolve => setTimeout(resolve, 50)); // 短暂等待
link.click();
clickedCount++;
console.log(`点击了链接: ${link.href}`);
// 每次点击后稍微停顿一下,避免过于频繁触发反爬或浏览器崩溃
await new Promise(resolve => setTimeout(resolve, 200)); // 等待 200ms
} catch (e) {
console.error(`点击链接 ${link.href} 时出错: ${e}`);
}
}
console.log(`总共尝试点击了 ${clickedCount} 个链接。`);
""",
# 不再使用 wait_for 参数
# wait_for=10
)
print("正在执行页面操作和 JS 点击...")
# 执行 arun,但不在这里等待下载完成
result_intermediate = await crawler.arun(url=url, config=run_config)
# 在 arun 完成后,手动等待一段时间让下载开始并进行
# 时间需要根据实际情况调整,这里设长一点,比如 30 秒
# 注意:robots.txt 建议 Crawl-Delay 是 10 秒,JS 点击本身可能有间隔,
# 但下载是并发开始的,这里的 sleep 是为了让后台下载有时间跑完。
wait_download_time = 30
print(f"JS 执行完毕,等待 {wait_download_time} 秒让下载进行...")
await asyncio.sleep(wait_download_time)
print("等待结束,检查下载文件...")
# Crawler4AI 在退出 `async with` 时会收集下载信息
# result.downloaded_files 实际上是在退出上下文管理器时更新的
# 所以我们需要重新获取最终结果,或者设计让下载状态更明确
# 退出 `async with` 后,再次检查 crawler 对象的下载列表
# 注意: result_intermediate 可能没有最新的下载信息,因为下载是异步发生的。
# Crawler4AI 的下载文件列表通常在浏览器关闭时(即 `async with` 块结束时)确定。
# 直接检查下载目录可能更直接。
downloaded_files_in_dir = list(Path(download_path).glob('*.pdf')) # 假设都是PDF
if downloaded_files_in_dir:
print("在下载目录中找到的文件:")
for file_path in downloaded_files_in_dir:
print(f"- {file_path.name}")
else:
print("下载目录中没有找到 PDF 文件。")
# 也可以尝试访问 result.downloaded_files,但它的更新时机需注意
# if hasattr(crawler, 'downloaded_files_history'): # 检查是否有记录
# print("Crawler 记录的下载历史:")
# for f in crawler.downloaded_files_history: print(f"- {f}")
# 设置下载路径
download_path = os.path.join(Path.cwd(), "Downloads_v1")
os.makedirs(download_path, exist_ok=True)
# 运行主函数
asyncio.run(download_multiple_files_v1("https://data.humdata.org/dataset/repository-for-pdf-files", download_path))
说明与提醒
- 可靠性问题 :这种方法依然依赖浏览器的下载机制,行为可能不稳定。JS 点击是否都成功?点击后下载是否都正常启动?
asyncio.sleep
的时间是否足够长?这些都是不确定因素。 - 资源消耗 :启动和控制一个完整的浏览器实例(即使是无头的)来做下载,比直接 HTTP 请求要消耗更多系统资源。
Crawl-Delay
:虽然robots.txt
的Crawl-Delay: 10
主要是针对连续的页面请求,但我们的 JS 代码里快速连续点击多个链接,也可能给服务器带来短时压力。代码中已加入小的延时 (setTimeout
) 模拟更真实的行为,并减缓点击速度。
方案二:提取 PDF 链接,使用 HTTP 库直接下载(推荐)
这个方案跳过了模拟点击的复杂性,是更常用也更稳健的网络爬虫下载方式。
原理和作用
- 利用 Crawler4AI 获取链接 :我们依然使用 Crawler4AI,因为它擅长处理需要 JavaScript 渲染的页面。但这次不用它的
js_code
去点击,而是用它的extraction_schema
功能,精确提取出所有 PDF 下载链接的href
属性(也就是真实的 URL)。 - 独立下载 :拿到 URL 列表后,我们使用专门的异步 HTTP 客户端库(比如
aiohttp
或httpx
)和文件操作库(aiofiles
)来发起直接的 HTTP GET 请求下载这些文件。这样控制更精细,错误处理也更方便。
操作步骤和代码示例
- 安装依赖库 :如果还没装,需要安装
aiohttp
和aiofiles
。pip install aiohttp aiofiles
- 修改 Python 代码 :
- 移除
BrowserConfig
中的accept_downloads=True
和downloads_path
(因为我们不用浏览器下载了)。 - 修改
CrawlerRunConfig
:去掉js_code
,添加extraction_schema
来提取链接。目标链接是<a>
标签,并且title
属性为 "Download"。我们要提取它的href
属性。 - 编写新的异步下载逻辑:使用
aiohttp
创建ClientSession
,遍历提取到的 URL,发起请求,并将响应内容写入本地文件。使用aiofiles
进行异步文件写入。 - 尊重
robots.txt
的Crawl-Delay: 10
:在每次下载请求之间加入asyncio.sleep(10)
。
- 移除
from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig, ExtractionSchema
from crawl4ai import AsyncWebCrawler
import os, asyncio, aiohttp, aiofiles
from pathlib import Path
from urllib.parse import urlparse, unquote
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
async def download_files_directly(url: str, download_path: str):
logging.info("尝试方案二:提取链接后直接下载")
# 配置浏览器,这次不需要下载功能
config = BrowserConfig(headless=True) # 可以保持无头模式
async with AsyncWebCrawler(config=config) as crawler:
# 配置提取规则
run_config = CrawlerRunConfig(
extraction_schema=ExtractionSchema(
# 使用 CSS 选择器定位元素
# 提取所有 title='Download' 的 a 标签的 href 属性
elements=[
{"selector": "a[title='Download']", "attribute": "href", "name": "pdf_links"}
]
),
# 等待页面加载完成即可,不需要额外的等待时间给点击
wait_for=5 # 等待 5 秒确保 JS 可能的渲染完成
)
logging.info(f"开始访问 URL: {url} 并提取链接...")
result = await crawler.arun(url=url, config=run_config)
if result.extracted_data and 'pdf_links' in result.extracted_data:
pdf_urls = result.extracted_data['pdf_links']
logging.info(f"成功提取到 {len(pdf_urls)} 个 PDF 链接。")
# 使用 aiohttp 下载文件
async with aiohttp.ClientSession() as session:
tasks = []
for i, pdf_url in enumerate(pdf_urls):
# 简单的从 URL 末尾提取文件名,并做 URL 解码
parsed_url = urlparse(pdf_url)
filename = os.path.basename(parsed_url.path)
filename = unquote(filename) # 处理 URL 编码的字符,如 %20 -> space
# 如果文件名无效或太简单,可以考虑生成一个,或者从页面其他地方提取
if not filename:
filename = f"downloaded_file_{i+1}.pdf"
file_save_path = Path(download_path) / filename
logging.info(f"准备下载: {pdf_url} 到 {file_save_path}")
# 创建下载任务
task = asyncio.create_task(download_single_file(session, pdf_url, file_save_path))
tasks.append(task)
# 遵守 Crawl-Delay
if i < len(pdf_urls) - 1: # 如果不是最后一个链接
logging.info(f"遵循 Crawl-Delay,等待 10 秒...")
await asyncio.sleep(10) # 在发起下一个请求前等待
# 等待所有下载任务完成 (注意:上面的 sleep 发生在任务创建之间,所以下载是顺序发起的)
await asyncio.gather(*tasks)
logging.info("所有下载任务已尝试完成。")
else:
logging.warning("未能提取到 PDF 链接。检查页面结构或提取规则。")
if result.raw_html:
logging.debug(f"页面原始 HTML (前 500 字符): {result.raw_html[:500]}")
async def download_single_file(session: aiohttp.ClientSession, url: str, save_path: Path):
"""使用 aiohttp 下载单个文件"""
try:
# 设置合理的 User-Agent
headers = {'User-Agent': 'MyFriendlyCrawler/1.0 (+http://mywebsite.com/bot_info)'}
async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=60)) as response: # 60秒超时
response.raise_for_status() # 检查 HTTP 错误 (如 404, 500)
# 使用 aiofiles 异步写入文件
async with aiofiles.open(save_path, 'wb') as f:
while True:
chunk = await response.content.read(8192) # 每次读 8KB
if not chunk:
break
await f.write(chunk)
logging.info(f"成功下载: {url} -> {save_path}")
return True
except aiohttp.ClientError as e:
logging.error(f"下载 {url} 时发生 aiohttp 错误: {e}")
except asyncio.TimeoutError:
logging.error(f"下载 {url} 超时。")
except Exception as e:
logging.error(f"下载 {url} 时发生未知错误: {e}")
# 如果下载失败,尝试删除可能创建的不完整文件
if save_path.exists():
try:
os.remove(save_path)
except OSError:
pass # 删除失败就算了
return False
# 设置下载路径
download_path_v2 = os.path.join(Path.cwd(), "Downloads_v2")
os.makedirs(download_path_v2, exist_ok=True)
# 运行主函数
asyncio.run(download_files_directly("https://data.humdata.org/dataset/repository-for-pdf-files", download_path_v2))
进阶使用技巧
-
并发下载控制 :如果目标网站允许,且你的网络带宽足够,可以考虑使用
asyncio.Semaphore
来限制并发下载的数量,而不是严格的串行下载。这样能加快整体下载速度,同时避免对服务器造成过大负担。# 在 download_files_directly 中 semaphore = asyncio.Semaphore(5) # 比如,最多同时下载 5 个文件 async def download_single_file_with_semaphore(...): async with semaphore: # 获取信号量 # ... 原来的下载逻辑 ... await asyncio.sleep(1) # 即使并发,也可以稍微错峰 # ... 创建任务时 ... task = asyncio.create_task(download_single_file_with_semaphore(...)) tasks.append(task) # 注意:如果使用并发,Crawl-Delay 的应用方式需要重新考虑。 # 可能不再需要在创建任务间 sleep,而是整体速率控制。 # 或者保持串行获取URL,并发下载URL内容。
-
更完善的错误处理 :添加重试逻辑。如果下载失败(比如网络波动或服务器临时错误),可以等待一小段时间后重试几次。
-
动态文件名 :如果 URL 中没有包含合适的文件名,可能需要从页面的其他文本内容(比如链接旁边的文字)来提取更有意义的文件名。这需要调整
extraction_schema
。 -
进度条 :对于大文件下载,可以使用
tqdm
库(及其异步版本)来显示下载进度。
安全建议
- 遵守
robots.txt
:方案二代码中已加入了对Crawl-Delay
的遵守。务必尊重网站的爬虫规则。 - 设置 User-Agent :在 HTTP 请求头中设置一个清晰、有代表性的 User-Agent,表明你的爬虫身份和目的。这是一种礼貌,也有助于网站管理员识别流量来源。代码示例中已加入。
- 不过度请求 :即使并发下载,也要合理设置并发数,避免给目标服务器带来太大压力。
总结来说,虽然方案一尝试修复了原代码的直接错误,但方案二(提取链接 + 直接 HTTP 下载)通常是处理这类下载任务更推荐、更健壮的方法。它避免了模拟用户操作的不确定性,也更符合网络爬虫的最佳实践。