动态页面与并发
3 道练习题·预计 55 分钟·做对一题解锁下一段
前言
各位童鞋,这是爬虫课的最后一章了。前面学的 requests 加 BeautifulSoup,已经能对付大多数网站了。可总有一些情况,它们对付不了。这一章,就来看其中的两种。
第一种:页面上的数据,要靠 JavaScript 才能显示出来,网页背后又找不到好用的接口。这时候,requests 只能拿到一个空架子。怎么办呢?那就请一个真正的浏览器来打开网页,等 JavaScript 跑完了,再取内容。这要用到 Playwright 。
第二种:要抓的页面很多,一页一页地等,实在太慢了。那就一次发出好几个请求,同时等它们回来,这叫并发。基础课第 22 章讲过 asyncio,这一章就配上 httpx ,把它用起来。
这两样都是「大杀器」,可用起来是有代价的:浏览器又慢又占资源;并发一不小心,就成了给网站施压。所以这一章花在「什么时候该用、怎么用得有分寸」上的篇幅,不会比讲用法的少。
学完这一章,这门爬虫课就结束了。最后还会说说,想继续深入的话,再往后可以学些什么。
学这一章之前,需要先学完 爬虫课第 11 章 。基础课第 22 章的 async / await 学过更好,没学过也没关系,这一章会先把用到的部分讲够。
目录
这一章会学到:
- 一、requests 拿不到的页面——数据在 JavaScript 里
- 二、让真正的浏览器来打开——Playwright
- 三、一次抓好几页——asyncio 加 httpx
- 四、再往后学什么——Scrapy 和更大的爬虫
- 五、常见坑
- 六、自己查一次文档——Semaphore 是怎么限制并发的
- 七、小结与回看
学完这一章,你能在需要的时候用浏览器自动化抓动态页面,用有节制的并发加快抓取,也知道下一步该往哪儿学。
一、requests 拿不到的页面
先看第一种情况。quotes.toscrape.com 有一个专门的练习页 /js/ 。在浏览器里看,它和首页一样,有 10 条名言;可它的内容,是 JavaScript 生成的。用 requests 取一下,看看会怎样:
import requests
from bs4 import BeautifulSoup
html = requests.get('https://quotes.toscrape.com/js/').text
soup = BeautifulSoup(html, 'html.parser')
print(len(soup.select('div.quote')))
print('var data' in html)在自己电脑上运行,输出是:
0
True第一行是 0,一条名言都没找到。可第二行是 True ,说明网页里确实有 var data 这几个字。这是怎么回事呢?在浏览器里「查看网页源代码」,搜一下就能看到:名言的数据,写在一段 JavaScript 里。网页打开以后,再由 JavaScript 把它们一条条画到页面上。而 requests 拿到的,只是网站最初发回来的那份 HTML,里面还没有画好的名言。
遇到这种页面,该怎么办呢?按这个顺序想办法:
- 先找接口:打开「网络」面板,看看数据是不是从某个接口来的。有接口就用第 7 章的办法,最省事。
- 再看源代码:数据要是像这样直接写在
<script>里,有时可以把那一段文字切出来,当 JSON 解析。不过 JavaScript 不一定是标准的 JSON,切的时候很容易出错,网站一改就失效。 - 最后才用浏览器:前两条都走不通,就让一个真正的浏览器去打开网页,等 JavaScript 跑完,再取内容。
第三条用浏览器,是这一章新学的本事。具体怎么做,下一节就来讲。
二、让真正的浏览器来打开
上一节说到,实在没办法的时候,就让一个真正的浏览器去打开网页。可浏览器是给人用的,程序怎么去操作它呢?这就要靠 Playwright 了。它能用代码控制浏览器:打开网页、等元素出现、点按钮、填表单、取内容,就像有一个看不见的人,在替你操作浏览器。
用之前,先安装。它除了 Python 包,还要下载浏览器本身:
python -m pip install playwright
python -m playwright install chromium第二条命令会下载一个 Chromium 浏览器,有一百多 MB,要等一会儿。
装好以后,就来打开上一节那个 /js/ 页面。等名言出现了,再把网页交给 BeautifulSoup:
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto('https://quotes.toscrape.com/js/')
page.wait_for_selector('div.quote')
html = page.content()
browser.close()
soup = BeautifulSoup(html, 'html.parser')
quotes = soup.select('div.quote')
print(len(quotes))
print(quotes[0].select_one('.author').get_text())在自己电脑上运行,输出是:
10
Albert Einstein这回,10 条名言都取到了。这段代码的前半段是新东西,咱们一行一行地看:
sync_playwright()启动 Playwright,p.chromium.launch()打开一个 Chromium 浏览器。默认是无头模式,也就是浏览器在后台运行,不弹出窗口。想看着它操作,就写成launch(headless=False)。page.goto(网址)打开网页。page.wait_for_selector('div.quote')会一直等,等到页面上出现了名言,也就是 JavaScript 跑完了,才往下走。page.content()取出此时的 HTML,也就是开发者工具「元素」面板里看到的那一份,里面已经有名言了。- 后面的部分,就是熟悉的 BeautifulSoup 了。
不过,要记住它的代价:启动一个浏览器要好几秒,还要占几百 MB 内存,比 requests 慢得多,也重得多。所以能用接口、能用 requests 的,就别用浏览器。
其实 Playwright 的本事,远不止抓数据,它还能点击、输入、截图。它的 官方文档 是英文的,写得很详细,需要的时候再去学。
三、一次抓好几页
动态页面的问题解决了,再来看第二种情况:页面太多,抓得太慢。
前面的爬虫,都是一页一页抓的:发出请求,等它回来,再发下一个。可是等待的时候,程序其实什么都没干,时间就这么白白过去了。
能不能不干等呢?这就是并发:同时发出好几个请求,一起等它们回来。基础课第 22 章讲的 asyncio,就是干这个的。不过 requests 不支持 asyncio,所以要换成一个用法和它很像的库:httpx 。先装上它:
python -m pip install httpx并发到底能快多少?来比较一下:抓 quotes.toscrape.com 的前 5 页,一种是一页一页地抓,另一种是同时最多抓 2 页。两种都在每个请求之后停 0.5 秒:
import asyncio
import time
import httpx
URLS = [f'https://quotes.toscrape.com/page/{n}/' for n in range(1, 6)]
def one_by_one():
codes = []
with httpx.Client(timeout=10) as client:
for url in URLS:
codes.append(client.get(url).status_code)
time.sleep(0.5)
return codes
async def together():
limit = asyncio.Semaphore(2)
async def get(client, url):
async with limit:
response = await client.get(url)
await asyncio.sleep(0.5)
return response.status_code
async with httpx.AsyncClient(timeout=10) as client:
return await asyncio.gather(*(get(client, url) for url in URLS))
start = time.monotonic()
print(one_by_one(), f'{time.monotonic() - start:.1f} 秒')
start = time.monotonic()
print(asyncio.run(together()), f'{time.monotonic() - start:.1f} 秒')在自己电脑上运行,输出类似这样(时间每次不一样):
[200, 200, 200, 200, 200] 4.4 秒
[200, 200, 200, 200, 200] 3.0 秒同样是每次停 0.5 秒,并发的版本快了不少。一页一页抓的 one_by_one() ,是熟悉的写法,就不多说了。咱们把并发的部分拆开来看:
httpx.AsyncClient()是 httpx 的异步版客户端。await client.get(url)发出请求以后,在等它回来的这段时间里,别的任务可以接着跑。asyncio.gather(...)把 5 个任务一起交出去,等它们全部完成,再按原来的顺序返回结果。asyncio.Semaphore(2)是关键。async with limit:里面的代码,同一时间最多只有 2 个任务在跑,第 3 个任务,要等前面的出来了才能进去。
看到这里,可能有童鞋会问:「为什么不干脆 5 个一起抓呢?」因为并发数,就是你同时压在网站上的请求数。抓 5 页,倒没什么;可要是抓 5000 页时不加限制,一下子发出几千个请求,那和攻击网站就没什么两样了。所以用并发的时候,要记住这几条:
- 并发一定要配上
Semaphore,限制数量。对小网站,两三个就够了。 - 每个请求之后,照样要停一停。
- 动手之前先算一算:同时 2 个,每个停 0.5 秒,相当于一秒最多 4 个请求,网站受得了吗?
并发的写法有点新,下面先做一道练习,把它练熟。
fake_fetch(n) 模拟抓第 n 页:等 0.2 秒,返回 n * 10 。请在 main() 里用 asyncio.gather() 同时抓第 1~5 页,打印结果列表;再打印总用时是否少于 0.5 秒(一页一页抓要 1 秒)。
运行后的输出应该是:
[10, 20, 30, 40, 50]
True结果要在 main() 里面打印。
推荐一个,全网最全最友好的中文 AI 编程指南: coding.stormzhang.ai