第 2 课
抓 JS 页面、找数据接口,写一个能暂停续爬的 Spider
用 scrapling install 准备浏览器,对比 curl 与 extract fetch 抓 JS 页面,用 StealthyFetcher 的 wait_selector 等延迟内容,用 StealthySession 的 record_requests 和 network.search 找到无限滚动背后的接口,再写一个遵守 robots.txt 的 Spider 抓完 10 页 100 条名言、导出 JSON/CSV,并用 crawldir 加 Ctrl+C 暂停续爬、用 on_scraped_item 边抓边存。
课程目录第 2 / 3 课
学习位置仅保存在当前浏览器,有效期 180 天。
图文教材
本课对照的官方页面:浏览器抓取 StealthyFetcher、extract 命令、爬虫入门、爬虫进阶、迁移到 0.5。命令与输出在 Linux x86_64 云主机上实测(Scrapling 0.5,patchright 1.64.4),输出为节选。
上一课的 Fetcher 只下载 HTML,不执行 JavaScript。这一课换成浏览器抓取器 StealthyFetcher,抓靠 JS 渲染、延迟加载的页面;再打开网络记录,直接找到页面背后的数据接口;最后写一个 Spider 爬虫,抓完 10 页 100 条名言,导出 JSON 和 CSV,并试一下中途暂停、下次接着爬。
第 1 步:准备浏览器,确认 curl 拿不到 JS 页面
接着用上一课的虚拟环境。浏览器抓取器需要先下载浏览器:
source .venv/bin/activate
scrapling install
上一课说过,测试机这一步在安装系统库时因软件源报错退出,但浏览器已下载完成,后面的命令都正常。
练习站的 /js/ 页面,名言是页面加载后由 JavaScript 写进去的。先用 curl 数一下 HTML 里有几条:
curl -s https://quotes.toscrape.com/js/ | grep -c 'class="quote"'
对照结果:
0
第 2 步:用 extract fetch 抓 JS 页面
extract fetch 用 Scrapling 的隐身浏览器打开页面,等 JavaScript 执行完再保存。--network-idle 表示等网络空闲(500 毫秒内没有新请求)再取内容:
scrapling extract fetch "https://quotes.toscrape.com/js/" js.md \
--network-idle
grep -c '^“' js.md
对照结果(数以弯引号 “ 开头的行,即名言条数;测试机约 3 秒):
10
js.md 里的名言格式和上一课一样,例如 “The world as we have created it ...”by Albert Einstein(作者前没有空格,是转换时的原样输出)。
0.5 版改了命令名:旧教程里的 extract stealthy-fetch 现在叫 extract fetch,旧的 DynamicFetcher 已经删除,统一用 StealthyFetcher。照着 0.4 的文章抄命令会报错,以官方迁移说明为准。
第 3 步:在 Python 里等元素出现再抓
/js-delayed/ 页面会故意延迟几秒才写入名言,只等页面加载完还不够,要等目标元素出现。新建 js_compare.py:
from scrapling.fetchers import Fetcher, StealthyFetcher
url = 'https://quotes.toscrape.com/js-delayed/'
plain = Fetcher.get(url)
print('Fetcher:', len(plain.css('.quote')), 'quotes')
page = StealthyFetcher.fetch(url, wait_selector='.quote',
wait_selector_state='visible')
print('StealthyFetcher:', len(page.css('.quote')), 'quotes')
print(page.css('.quote .author::text').getall()[:3])
python js_compare.py
对照结果(已去掉 INFO 日志行,测试机约 12 秒):
Fetcher: 0 quotes
StealthyFetcher: 10 quotes
['Albert Einstein', 'J.K. Rowling', 'Albert Einstein']
wait_selector 填 CSS 选择器,wait_selector_state='visible' 表示等到元素可见。StealthyFetcher.fetch() 返回的页面对象和 Fetcher.get() 一样,上一课的 css()、xpath()、find_similar() 都能直接用。
第 4 步:记录网络请求,找到页面背后的接口
很多页面的数据来自后台接口。与其解析 HTML,不如直接拿接口返回的 JSON。练习站的 /scroll 页面是无限滚动,每滚到底部就请求一次 /api/quotes?page=N。
0.5 版的做法是:打开一个 StealthySession 并设 record_requests=True,用 page_action 在页面上滚动,最后用 session.network.search() 按网址正则查找记录下来的请求。新建 scroll_api.py:
from patchright.sync_api import Page
from scrapling.fetchers import StealthySession
def scroll(page: Page):
for _ in range(3):
page.mouse.wheel(0, 5000)
page.wait_for_timeout(1200)
with StealthySession(record_requests=True) as session:
page = session.fetch('https://quotes.toscrape.com/scroll',
network_idle=True, page_action=scroll)
print('页面上的名言:', len(page.css('.quote')))
calls = session.network.search(url_pattern=r'/api/quotes', limit=None)
for r in calls:
data = r.json()
print(r.status, r.url, 'page', data['page'],
'quotes', len(data['quotes']), 'has_next', data['has_next'])
python scroll_api.py
对照结果(已去掉 INFO 日志行,网址前缀有删节):
页面上的名言: 40
200 .../api/quotes?page=1 page 1 quotes 10 has_next True
200 .../api/quotes?page=2 page 2 quotes 10 has_next True
200 .../api/quotes?page=3 page 3 quotes 10 has_next True
200 .../api/quotes?page=4 page 4 quotes 10 has_next True
滚动 3 次,页面一共请求了 4 页接口、显示 40 条。记下的响应在会话关闭后也能读,r.json() 直接得到接口数据。知道接口地址之后,后面就可以用上一课的 Fetcher.get() 直接请求 /api/quotes?page=N,不必再开浏览器。
几个说明:
page_action接收一个函数,参数是浏览器页面对象(Playwright 接口),可以滚动、点击、填表。search()默认最多返回 100 条,limit=None返回全部。- 旧版的
capture_xhr参数在 0.5 版已删除,按上面的写法替代。
第 5 步:写一个爬虫,抓完全部分页
单页抓取之外,Scrapling 0.5 自带类似 Scrapy 的爬虫框架。新建 quotes_spider.py:
import json
from scrapling.spiders import Spider, Response
class QuotesSpider(Spider):
name = "quotes"
start_urls = ["https://quotes.toscrape.com/"]
allowed_domains = {"quotes.toscrape.com"}
robots_txt_obey = True
concurrent_requests = 2
download_delay = 1.0
async def parse(self, response: Response):
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get(""),
"author": quote.css("small.author::text").get(""),
"tags": quote.css("a.tag::text").getall(),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)
async def on_scraped_item(self, item: dict) -> dict | None:
# 每抓到一条就追加写盘,暂停后再续爬也不会丢前面的数据
with open("quotes.jsonl", "a", encoding="utf-8") as f:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
return item
if __name__ == "__main__":
result = QuotesSpider(crawldir="crawl_data/quotes").start()
print("paused:", result.paused, "completed:", result.completed)
print("items:", result.stats.items_scraped,
"requests:", result.stats.requests_count,
f"elapsed: {result.stats.elapsed_seconds:.1f}s")
if result.completed:
result.items.to_json("quotes.json", indent=True)
result.items.to_csv("quotes.csv", fields=["author", "text"])
代码说明:
parse()里yield一个字典就是一条数据,yield response.follow()就是继续抓下一页。allowed_domains防止爬到别的网站;robots_txt_obey = True先读 robots.txt 并遵守规则(默认关闭);concurrent_requests和download_delay控制并发和间隔,别把对方网站压垮。on_scraped_item()是官方提供的钩子,每条数据入库前都会调用。这里用它把每条数据马上追加写进quotes.jsonl,原因见第 6 步。crawldir开启断点续爬。
运行:
python quotes_spider.py
对照结果(节选):
INFO: Fetched (404) <GET https://quotes.toscrape.com/robots.txt>
...
"robots_disallowed_count": 0,
INFO: Saved 100 items to quotes.json
INFO: Saved 100 items to quotes.csv
paused: False completed: True
items: 100 requests: 10 elapsed: 12.2s
练习站没有 robots.txt(404),所以不限制抓取。查看导出结果:
wc -l quotes.jsonl quotes.csv
head -3 quotes.csv
对照结果(第 2 行名言有删节):
100 quotes.jsonl
101 quotes.csv
201 total
author,text
Albert Einstein,“The world as we have created it is a process ...”
J.K. Rowling,"“It is our choices, Harry, that show what we truly ..."
quotes.csv 多出的 1 行是表头。to_csv() 的 fields 参数决定导出哪些列、按什么顺序。
第 6 步:中途暂停,下次接着爬
开了 crawldir 之后,运行中按一次 Ctrl+C,爬虫会等正在进行的请求完成,把待抓队列和已抓网址存成检查点再退出。用同样的 crawldir 再运行一次,就从断点继续。连按两次 Ctrl+C 是强制退出,不等请求完成。
先删掉上一步的结果,重新跑,大约 4 秒后按一次 Ctrl+C:
rm -rf crawl_data quotes.json quotes.csv quotes.jsonl
python quotes_spider.py
# 约 4 秒后按一次 Ctrl+C
wc -l quotes.jsonl
对照结果(节选):
INFO: Checkpoint saved: 1 requests, 5 seen URLs
INFO: Spider paused, checkpoint saved
paused: True completed: False
items: 40 requests: 4 elapsed: 5.1s
40 quotes.jsonl
再运行一次同样的命令:
python quotes_spider.py
wc -l quotes.jsonl
对照结果(节选):
INFO: Checkpoint loaded: 1 requests, 5 seen URLs
INFO: Resuming from checkpoint, skipping start_requests()
DEBUG: Checkpoint file cleaned up
INFO: Saved 60 items to quotes.json
INFO: Saved 60 items to quotes.csv
paused: False completed: True
items: 60 requests: 6 elapsed: 7.5s
100 quotes.jsonl
注意 Saved 60 items:续爬后的 result.items 只包含这一次运行抓到的 60 条,前面 40 条不在里面。如果只靠最后的 to_json() 导出,暂停前的数据就丢了。这就是第 5 步用 on_scraped_item() 边抓边写 quotes.jsonl 的原因:两次运行加起来正好 100 行,测试机核对 100 条名言没有重复。
小结
- JS 渲染或延迟加载的页面用
StealthyFetcher或scrapling extract fetch,配合network_idle、wait_selector等内容出现。 - 0.5 版删除了
DynamicFetcher和capture_xhr;抓接口用StealthySession(record_requests=True)加session.network.search()。 - Spider 爬虫用
parse()产出数据和下一页请求,allowed_domains、robots_txt_obey、download_delay让抓取更规矩。 crawldir加 Ctrl+C 可以暂停续爬,但续爬后result.items只有本次数据,要用on_scraped_item()之类的办法边抓边存。
下一课把 Scrapling 作为 MCP 服务器交给 AI 应用,让智能体自己调用这些抓取能力。
