前言
playwright是微软设计的一款工具,可以爬取网页,还可以自动化测试自己编写的网站,而且不像bs4、request编写爬虫那么复杂,也不需要考虑反爬技术,只需要知道最基础的前端知识,就可以高效、便捷的编写爬虫代码
但是这篇文章不可能将playwright的所有功能全部讲到,但是覆盖了最基础的一些知识,如果想更系统的学习,可以参考playwright python的官方文档:https://playwright.dev/python/docs/intro
也非常推荐白月黑羽的教程:Playwright web自动化 - Python版_哔哩哔哩_bilibili
安装步骤
pip install pytest-playwright
playwright install
简单demo
使用下面命令可以开启录制
playwright codegen
右边窗口会自动根据左边浏览器做的操作进行记录,下面是自动生成的代码模板,定义了同步操作(不是异步)的playwright类,并输入到run函数中,browser这句代码开辟新的进程,打开了playwright自带的浏览器chromium,这个可以自己改为其他的浏览器(参考官网),headless为False时,执行过程会打开浏览器,否则将不会显示。
from playwright.sync_api import Playwright, sync_plawright, expectdef run(playwright: Playwright) -> None:browser = playwright.chromium.launch(headless=False)context = browser.new_context()page = context.new_page()# ---------------------context.close()browser.close()with sync_playwright() as playwright:run(playwright)
在左边浏览器键入www.baidu.com,右边代码就新增了代码,表示跳转到了这个界面
page.goto("https://www.baidu.com/")
然后点击输入框,并输入nihao,就会新增下面这些代码,其中locator是定位器,page.locator("#kw")
定位到的是输入的文本框,即定位到网页中的一个部件,后面的click、fill、press分别表示点击、填充、输入回车等操作,对部件的操作见Actions | Playwright Python
page.locator("#kw").click()
page.locator("#kw").fill("nihao")
page.locator("#kw").press("Enter")
这样一来就可以自动设计代码啦,同时还可以使用tracing模块来记录执行的过程
context = browser.new_context()
context.tracing.start(snapshots=True, sources=True, screenshots=True)
...
context.tracing.stop(path="trace.zip")
然后在终端执行就可以显示整个的过程,左边为playwright的各种操作,右边有Action,Before和After显示每一步的动作、先前状态和执行过后的状态
playwright show-trace trace.zip
这样一个简单的demo,打开baidu网站,查询nihao,就实现啦
from playwright.sync_api import Playwright, sync_playwright, expectdef run(playwright: Playwright) -> None:browser = playwright.chromium.launch(headless=False)context = browser.new_context()context.tracing.start(snapshots=True, sources=True, screenshots=True)page = context.new_page()page.goto("https://www.baidu.com/")page.locator("#kw").click()page.locator("#kw").fill("nihao")page.locator("#kw").press("Enter")context.tracing.stop(path="trace.zip")context.close()browser.close()with sync_playwright() as playwright:run(playwright)
任务描述和预备知识
我将通过一个实际应用来说明如何使用playwright,获取到leetcode上每一个题目的url网址。
进入leetcode官网,点击题库
然后跳到第二页
发现网页为:https://leetcode.cn/problemset/?page=2,其中?右边为参数,表示在第几页,没有page参数时默认第1页,我们发现每一页都有50个题目(除了第一页有51个题目,需要特殊处理)
点击F12打开开发者模式,右边选择“元素”菜单
点击下面的图表,然后在左侧点击控件,可以查看到第52题这块对应的源码,右边源码中有属性href=“/problems/n-queens-ii”,这就是我们想要获取的信息,但是这个网页不完整,点击这个网站后,可以看到网页变为了https://leetcode.cn/problems/n-queens-ii/description,所以还需要加上前缀:“https://leetcode.cn” 和后缀 ”/description“
我们的目标是获取到每一个题目的href信息,并保存在一个txt文件当中
选择器定位
前面使用到了page.locator方法,里面的参数就是我们去定位组件的标准,我们不能完全依靠codegen这一个方法自动化完成代码,如果需要输出一些信息,就不能指望它了,所以这一节将介绍如何定位元素以及元素的一些操作
我们首先在模板中填写下面的代码,暂时不处理最后一页的题目,遍历第1页到第72页的题目,然后goto到该网站,并等待5s,之所以等待5s是因为需要等网页加载完毕后,才能提取到相关的信息,否则会加载错误
for page_index in trange(1, 73):page.goto(f"https://leetcode.cn/problemset/?page={page_index}")page.wait_for_timeout(5000)
接下来就是定位了,例如某一个分块有class属性,如下所示,则locator里面可以写为page.locator(".truncate")
,这里的"."表示匹配的class元素
<div class='truncate'>nihao
</div>
然后可以调用.inner_text()可以打印这一个locator的文本信息
lc = page.locator(".truncate")
print(lc.inner_text()) # 输出nihao
然而一个html网页中可能有多个块的class包含truncate,那么 page.locator(".truncate")
就会定位到所有的块,这时候就需要使用.all()方法,将所有定位到的locator转变为list对象,然后我们遍历list,再打印文本信息
lcs = page.locator(".truncate").all()
for lc in lcs:print(lc.inner_text())
注意,如果locator定位到的元素个数大于1,则不能执行fill、click、inner_text等操作,需要使用all()将其转换为列表,下面是locator的一些方法和用法
locator(".truncate").count() # 匹配元素的个数
locator(".truncate").first # 第一个元素
locator(".truncate").last # 最后一个元素
locator(".truncate").nth(3) # 第三个元素
除了使用class定位,还可以使用id定位,一般而言html中的id是唯一的,例如下面的代码可以使用 page.locator("#animal")
来定位,"#"表示id
<div id='animal'>nihao
</div>
如果一个块中有多个class类别,例如下面的例子,使用 page.locator(".animal.plant")
定位,注意中间不能有空格
<div class='animal plant'>nihao
</div>
还可以直接使用page.locator(“div”)来定位这个div块,如果不是上述情况,而是其他的属性名,则可以用方括号来指示,例如下面的例子可以用 page.locator("[role=region]")
定位
<div role='region'>nihao
</div>
还可以是模糊匹配,包含 *=, ^=, *=等方式
[href*=www] -> href属性包含www的元素
[href^=www] -> href属性以www开头的元素
[href$=www] -> href属性以www结尾的元素
我们还可以通过多级选择器来选择,在下面的例子中,想定位nihao,可以使用 page.locator(".a > .b)"
定位,“>” 表示直接子节点,如果想定位oahin,还可以使用子孙节点的定位方式,使用空格来隔开 page.locator(".a .d")
,这样就跳过了中间节点".c"
<div class='a'><div class='b'>nihao</div><div class='c'><div class='d'>oahin</div></div>
</div>
实际定位
弄清楚如何定位后,接下来,我们要获取每一页中的所有题目的信息,找到某一个题目的信息,代码如下
可以使用下面的代码定位,但是如何确定是否准确定位到呢,一个方法就是执行一遍代码,打印出来定位到的块,查看是否符合预期,但是这样太麻烦了,浏览器中可以帮助我们查看
lcs = page.locator(".truncate .h-5")
在右上方(下方是另一个界面了)输入Ctrl + F开启搜索功能,输入.truncate .h-5,看到我们找到了50个元素,刚好对应一页中题目的个数
我们可以通过下面的代码获取到第i个元素的url地址,使用到了get_attribute方法获取某一个属性的值
lcs = page.locator(".truncate .h-5")
lcs.nth(i).get_attribute("href")
然后我们可以将url补充完整,特殊处理一下第一页51道题目的情况,将题目索引以及url保存在urls.txt文件当中,编写完成下面的脚本
from playwright.sync_api import Playwright, sync_playwright
from tqdm import trangedef run(playwright: Playwright) -> None:with open('urls.txt', 'w') as f:browser = playwright.chromium.launch(headless=True)context = browser.new_context()page = context.new_page()cnt = 1for page_index in trange(1, 73):page.goto(f"https://leetcode.cn/problemset/?page={page_index}")page.wait_for_timeout(5000)lcs = page.locator(".truncate .h-5")for problem_index in range(50): # 第一页的题目索引要加一problem_page = f'https://leetcode.cn{lcs.nth(problem_index + int(page_index == 1)).get_attribute("href")}/description'f.write(str(cnt) + ' ' + problem_page + '\n')cnt += 1f.flush()context.close()browser.close()with sync_playwright() as playwright:# 获取系统参数run(playwright)
最终获取了信息啦!
如果对您有帮助,请不要吝啬您的赞呀,你们的鼓励是我最大的动力!
补充样例
下面的例子是打开有道翻译,输入英文,返回翻译后的信息代码,如果您感兴趣,可以看看哦,使用到的方法都差不多~
from playwright.sync_api import Playwright, sync_playwright, expect
import redef run(playwright: Playwright) -> None:browser = playwright.chromium.launch(headless=True)context = browser.new_context()page = context.new_page()page.goto("https://fanyi.youdao.com/#/") page.locator(".close").click()page.locator("div").filter(has_text=re.compile(r"^翻译$")).click()page.locator(".ic_language_arrowdown").first.click()page.get_by_text("英语").first.click()page.wait_for_timeout(1000)while True:word = input("=" * 100 + "\n>>> ")page.locator("#js_fanyi_input").click()page.locator("#js_fanyi_input").fill(word)page.wait_for_timeout(1000)if page.locator(".pron").count(): # 单个单词,打印音标以及所有意思for text in page.locator(".pron").all_inner_texts():split = text.split()if not split:continueelse:print(f"{split[0]}: /{split[1]}/")print(page.locator(".paraphrase-container").first.inner_text())else: # 句子,直接显示翻译print(page.locator(".tgt.un-step-trans").inner_text())# ---------------------context.close()browser.close()with sync_playwright() as playwright:run(playwright)
执行效果