玩转浏览器自动化(5)Playwright 等待

在前面的章节中,我们已经学习了如何使用定位器与元素进行交互。但是在实际操作中,我们必须要等待页面的加载、数据的刷新、表单的提交等等。这就需要用到等待的技巧来确保我们在合适的时机执行操作,从而制作出稳定的浏览器自动化应用。

在本章中,我们将向大家介绍如何使用 Playwright 提供的工具来控制代码执行的时机,以及不同的技术和方法,让您了解如何等待页面准备就绪、输入元素可见或可以发出请求以及其他许多事情。.

自动等待

在前面的示例代码中,我们都是直接对元素进行操作的,没有任何等待元素出现的相关代码,类似这样:

var element = page.Locator(selector);

//硬等待 1 秒
await Task.Delay(1000);

await element.ClickAsync();

这是因为,在使用Playwright 时,可以享受到自动等待的强大特性。这意味着我们无需手动编写等待元素出现的代码,Playwright会自动处理这一过程。

Playwright 可以在执行任何请求操作之前对元素进行一系列可操作性检查,以确保您的 Web 自动化运行无缝。这个功能可以消除大量的硬编码等待,节省时间并提高效率。

可操作性检查包括以下方面:

  • 元素是否存在于 DOM 中
  • 元素是否可见,大小为 0 或带有 display:none 样式的元素被认为是不可见的
  • 元素是否稳定,如正在进行动画渲染的元素被认为是不稳定的
  • 元素是否可交互,如没有被其他元素遮挡的元素被认为是可交互的
  • 元素是否启用,如具有 disabled 属性的输入元素被认为是禁用的
  • 元素是否可编辑,启用且未设置 readonly 属性的元素被认为是可编辑的

当然,自动等待也不是无限期的等待。每个方法都包含一个可选的 Timeout 属性,用于设置可操作性检查的最大超时时间,单位为毫秒,默认为 30 秒,也可以设置为 0 来禁用超时。默认值可以通过 browserContext.SetDefaultTimeout 或 page.SetDefaultTimeout 方法进行修改。如果所需的检查未在给定的超时时间内通过,则操作将失败并引发异常。

此外,还有一个 Force 属性,支持禁用非必要的可操作性检查,默认为 false。例如,如果将 Page.ClickAsync(selector, options) 方法的 Force 属性设置为 true,则即使元素不可交互,也会强制进行点击。

除了自动等待以外,Playwright 还支持显式等待。建议仅在自动等待无法满足需求时才使用显式等待,例如在执行 B 元素操作之前等待 A 元素。

显式等待

Playwright 提供了 Locator.WaitForAsync(options) 方法来帮助我们等待元素,如果在设定的超时时间内,仍然没有通过定位器等待到目标元素,将会抛出异常。

在该方法的参数中,State 属性用于检查元素是否满足可操作性的某种状态,可选值包括已附加、已移除、可见和不可见。例如,我们可以使用显式等待来等待 stackoverflow 网站首页的警告条可见,然后再点击“Log In”按钮。

玩转浏览器自动化(5)Playwright 等待

下面是示例代码:

await page.GotoAsync("https://stackoverflow.com/");

var elelment = page.Locator("id=noscript-warning");

await elelment.WaitForAsync();

Console.WriteLine(await elelment.InnerTextAsync(new LocatorInnerTextOptions { Timeout = 10  }));

我们在等待警告条可见后立即获取元素的文本,并设置了很短的超时时间(1/100 秒)。如果没有等待成功,该方法会立刻抛出异常,否则,就会打印出警告条的文本。

虽然 Locator.WaitForAsync 在大多数情况下可以节省我们的时间,但是我们还需要考虑其他显示等待情况。例如,我们可能希望先执行操作,再等待操作引起的某种情况出现。

执行操作并等待

在使用 Playwright 时,我们可以利用 Page 对象中提供的一些方法来执行操作并等待特定情况的出现。这些方法都是以 RunAndWaitFor 开头的,下面我们来一一介绍。

RunAndWaitForConsoleMessageAsync

RunAndWaitForDownloadAsync 可以帮助开发人员执行操作并等待控制台消息出现。该方法接收两个参数,第一个参数是一个 Action,用于执行操作,第二个参数包含一个 Predicate Func,用于检查控制台消息是否出现。如果出现,那么该方法就会立即返回。需要注意的是,Predicate 是可选的,如果不传入 Predicate,那么只要控制台消息出现,该方法就会立即返回。

下面我们来看一个具体的例子。我们通过访问百度网站首页来演示该方法的使用。在这个例子中,我们使用了 Predicate Func 来判断控制台消息的文本内容是否包含特定的字符串。如果包含,那么该方法就会立即返回。

玩转浏览器自动化(5)Playwright 等待

await page.RunAndWaitForConsoleMessageAsync(async () =>
{
    await page.GotoAsync("https://www.baidu.com/");
}, new PageRunAndWaitForConsoleMessageOptions { Predicate = (message) =>
{
    return message.Text.Contains("你的潜力,是改变世界的动力!");
}
});

RunAndWaitForDownloadAsync

RunAndWaitForDownloadAsync 可以帮助开发人员执行操作并等待下载完成。该方法接收两个参数,一个是用于执行操作的 Action,另一个是用于检查下载对象是否满足条件的 Predicate Func。

玩转浏览器自动化(5)Playwright 等待

 await page.GotoAsync("https://visualstudio.microsoft.com/zh-hans/downloads/");

await page.RunAndWaitForDownloadAsync(async () =>
{
    await page.ClickAsync("[data-bi-dlnm=\"Community 2022\"]");
}, new  PageRunAndWaitForDownloadOptions
{
    Predicate = (download) =>
    {
        return download.Url.Contains("version=VS2022");
    }
});

在上面的代码中,我们检查下载对象的 URL 是否包含“version=VS2022”。如果满足条件,那么该方法就会立即返回,表示下载已完成。

RunAndWaitForFileChooserAsync

RunAndWaitForFileChooserAsync 方法可以帮助我们在执行操作的同时等待文件选择器的出现。该方法接收两个参数,第一个参数是一个 Action,用于执行操作,第二个参数包含一个 Predicate Func,用于检查文件选择器是否满足条件。

玩转浏览器自动化(5)Playwright 等待

await page.GotoAsync("https://image.baidu.com/");

await page.RunAndWaitForFileChooserAsync(async () =>
{
    await page.ClickAsync("id=sttb");
    await page.ClickAsync("id=uploadImg");
}, new PageRunAndWaitForFileChooserOptions
{
    Predicate = (fileChooser) =>
    {
        return !fileChooser.IsMultiple;
    }
});

在上面的代码中,我们使用 await page.RunAndWaitForFileChooserAsync 方法,并传入一个异步操作 Action。该 Action 包含了点击“按图片搜索”按钮和上传图片的操作。同时,我们也传入了一个 PageRunAndWaitForFileChooserOptions 对象,其中 Predicate 属性是一个 Func 用于判断文件选择器是否满足条件,这里我们判断是否支持多文件上传。

RunAndWaitForNavigationAsync

RunAndWaitForNavigationAsync 方法可以帮助我们执行操作并等待页面导航完成。该方法接收两个参数,第一个参数是一个 Action,用于执行操作,第二个参数包含三种检查方式,分别是 UrlString 字符串、UrlRegex 正则表达式和 UrlFunc 方法,用于检查页面导航的 URL 是否满足条件。

await page.GotoAsync("https://stackoverflow.com/");

await page.RunAndWaitForNavigationAsync(async () =>
{
await page.ClickAsync("text='Log in'");
}, new PageRunAndWaitForNavigationOptions
{
UrlFunc = (url) =>
{
return url.Contains("users/login");
},
WaitUntil = WaitUntilState.Commit
});

在上面的代码中,我们使用 UrlFunc 方法检查页面导航的 URL 是否包含“users/login”,并设置 WaitUntil 属性为 WaitUntilState.Commit,表示等待页面完全加载完成。

RunAndWaitForPopupAsync

RunAndWaitForPopupAsync 方法可以帮助我们执行操作并等待弹出窗口出现。这个方法接收两个参数,第一个参数是一个 Action,用于执行操作;第二个参数是一个 Predicate Func,用于检查弹出窗口是否满足条件。

await page.GotoAsync("https://www.baidu.com/");

await page.RunAndWaitForPopupAsync(async () =>
{
    await page.ClickAsync("text=新闻");
}, new PageRunAndWaitForPopupOptions
{
    Predicate = (page) =>
    {
        return page.Url.Contains("news.baidu.com");
    }
});

在上面的代码中,我们先使用 await page.GotoAsync 方法访问百度首页。接着,我们通过 await page.ClickAsync 方法点击“新闻”链接,此时会弹出一个新的窗口。然后,我们使用 RunAndWaitForPopupAsync 方法等待这个窗口的出现,并且使用 Predicate Func 判断这个窗口是否符合我们的预期。在这个例子中,我们判断弹出页面导航的 URL 是否包含“news.baidu.com”。

RunAndWaitForRequestAsync/RunAndWaitForResponseAsync

在浏览器中执行操作时,浏览器会向服务器发送请求(Request),服务器接收请求后发送响应(Response)。每个页面导航都从一个页面请求开始。服务器将处理该请求并发送响应,响应通常是一个 HTML 页面,其中声明了需要请求的其他资源。服务器将再次处理这些请求并发送对应响应。

为了帮助我们等待网络请求和响应,Playwright提供了Page.RunAndWaitForRequestAsync和Page.RunAndWaitForResponseAsync方法。这两个方法可以通过传入URL地址或表达式来过滤等待的请求或响应,从而实现更加精细的控制。

下面,我们来看一个例子。我们首先访问stackoverflow网站首页并等待jquery请求,然后点击“Log In”按钮并等待登录页面的响应:

await page.RunAndWaitForRequestAsync(async () =>
{
await page.GotoAsync("https://stackoverflow.com/", new PageGotoOptions { WaitUntil = WaitUntilState.Commit });
}, request =>
{
Console.WriteLine(request.Url);
return request.Url.Contains("jquery.min.js");
});

await page.RunAndWaitForResponseAsync(async () =>
{
await page.ClickAsync("text='Log in'");
}, response => response.Url.Contains("users/login"));

需要注意的是,上述所有等待方法都是阻塞式的。也就是说,如果我们等待的情况一直未出现,后续的代码将不会执行。在某些情况下,我们只需要在情况出现时进行一些操作,即使情况不出现也不应该影响整体的运行。这时,我们可以使用监听事件来实现。

等待页面事件

在面向对象编程中,类或对象可以通过事件向其他类或对象通知发生的相关事情。发送(或引发)事件的类称为“发布者”,接收(或处理)事件的类称为“订阅者”。作为订阅者,我们可以为这些事件附加一个函数,这样我们就可以监听这些事件并相应地做出反应。

在 Playwright 的 Page 对象中,有许多事件可供使用,这些事件可以帮助我们更好地控制和管理页面。以下是详细的列表:

  • Close:当页面关闭时触发
  • Console:当控制台输出时触发
  • Crash:当页面崩溃时触发。例如,试图分配太多内存,浏览器页面可能会崩溃。
  • Dialog:当 JavaScript 对话框出现时触发。例如,alert、prompt、confirm等。
  • DOMContentLoaded:当页面的 DOMContentLoaded 事件触发时触发
  • Download:当下载开始时触发
  • FileChooser:当文件选择对话框出现时触发
  • FrameAttached:当 frame 连接到页面时触发
  • FrameDetached:当 frame 与页面分离时触发
  • FrameNavigated:当 frame 导航到一个新的 url 时触发
  • Load: 当页面的 load 事件触发时触发
  • PageError:当页面内发生未捕获异常时触发
  • Popup:当页面弹出新的窗口时触发
  • Request:当页面发起一个新的请求时触发
  • RequestFailed:当页面发起的请求失败时触发
  • RequestFinished:当页面发起的请求完成时触发
  • Response:当页面收到一个新的响应时触发

下面以 RequestFailed 事件为例,来看看如何使用事件来监听页面:

page.RequestFailed += (sender, request) =>
{
Console.WriteLine($"请求 {request.Url} 发生错误 {request.Failure}");
};

await page.GotoAsync("https://stackoverflow.com/");

玩转浏览器自动化(5)Playwright 等待

在上述代码中,我们为 RequestFailed 事件附加了一个函数。当页面发起的某些请求失败时,这个函数就会被触发。在函数中,我们可以对失败的请求进行一些处理,比如记录日志等。

使用事件机制可以让我们更好地控制和管理页面,同时也可以帮助我们更好地调试和排除错误。在实际开发中,我们可以结合不同的事件来实现更加复杂的功能,提高代码的可读性和可维护性。

总结

在本章中,我们深入探讨了如何解决浏览器自动化应用的不稳定性问题。我们介绍了一些技术和方法,包括等待页面加载、等待元素可操作、执行操作并等待以及等待页面事件。这些技术和方法可以帮助我们构建更稳定、可靠的自动化应用。

在下一章中,我们将学习如何在浏览器中执行 JavaScript。