Java使用Playwright自動(dòng)化測(cè)試的超詳細(xì)教程
前言
Playwright 是由微軟(Microsoft)開(kāi)發(fā)的開(kāi)源自動(dòng)化測(cè)試框架,它被設(shè)計(jì)用來(lái)克服 Selenium 這種“老牌工具”在現(xiàn)代 Web 應(yīng)用(尤其是復(fù)雜的單頁(yè)應(yīng)用 React/Vue)中的種種局限性。
1. 什么是 Playwright?
Playwright 是一個(gè)能夠跨瀏覽器(Chromium, Firefox, WebKit)進(jìn)行網(wǎng)頁(yè)自動(dòng)化的庫(kù)。它最顯著的特點(diǎn)是快、穩(wěn)、強(qiáng)。它不僅支持多種編程語(yǔ)言(Java, Python, JS, C#),還自帶了強(qiáng)大的錄制代碼、調(diào)試控制臺(tái)和網(wǎng)絡(luò)攔截功能。
2. Playwright vs Selenium:核心差異
| 維度 | Selenium (傳統(tǒng)派) | Playwright (現(xiàn)代派) |
|---|---|---|
| 等待機(jī)制 | 手動(dòng)等待。通常需要編寫(xiě)復(fù)雜的 WebDriverWait 或顯式等待,否則容易因元素未加載導(dǎo)致報(bào)錯(cuò)。 | 內(nèi)置自動(dòng)等待。在執(zhí)行點(diǎn)擊、輸入等操作前,會(huì)自動(dòng)檢查元素是否可見(jiàn)、穩(wěn)定、可操作。 |
| 瀏覽器驅(qū)動(dòng) | 依賴驅(qū)動(dòng)程序。需要手動(dòng)下載并配置與瀏覽器版本嚴(yán)格一致的 chromedriver 或 geckodriver。 | 內(nèi)置瀏覽器內(nèi)核。安裝后直接使用 Chromium、Firefox 和 WebKit 內(nèi)核,無(wú)需擔(dān)心驅(qū)動(dòng)版本沖突。 |
| 通信協(xié)議 | 基于 HTTP (JSON Wire)。每一個(gè)指令都要發(fā)送一次 HTTP 請(qǐng)求并等待響應(yīng),通信開(kāi)銷較大。 | 基于 WebSocket。與瀏覽器建立長(zhǎng)連接,指令實(shí)時(shí)發(fā)送,執(zhí)行速度顯著提升。 |
| 多標(biāo)簽/窗口 | 切換復(fù)雜。需要通過(guò) window_handles 手動(dòng)管理和切換句柄。 | 原生支持上下文。通過(guò) BrowserContext 輕松管理多個(gè)獨(dú)立頁(yè)面,捕獲新窗口非常簡(jiǎn)單。 |
| 錄屏與追蹤 | 功能有限。通常需要集成第三方庫(kù)來(lái)完成截圖或視頻錄制。 | 全能追蹤 器 (Trace Viewer)。原生支持錄制視頻、自動(dòng)保存點(diǎn)擊軌跡、網(wǎng)絡(luò)請(qǐng)求以及控制臺(tái)日志。 |
| 網(wǎng)絡(luò)攔截 | 難以實(shí)現(xiàn)。很難直接在代碼中修改或攔截網(wǎng)頁(yè)發(fā)出的 API 請(qǐng)求。 | 原生支持??梢暂p松攔截并篡改(Mock)網(wǎng)絡(luò)請(qǐng)求和返回?cái)?shù)據(jù),方便進(jìn)行前端邊界測(cè)試。 |
| 并行執(zhí)行 | 需要配合 Selenium Grid 才能實(shí)現(xiàn)高效并行。 | 單個(gè)瀏覽器實(shí)例即可開(kāi)啟多個(gè)獨(dú)立上下文,原生支持極速并行運(yùn)行。 |
3. 為什么現(xiàn)在大家都轉(zhuǎn)向 Playwright?
- 自動(dòng)等待(Auto-waiting)
Selenium 就像一個(gè)盲人,你得告訴他:“先等5秒,然后再摸按鈕”。如果網(wǎng)絡(luò)慢了,5秒不夠,程序就崩了。 Playwright 就像一個(gè)智能助手,你告訴他“點(diǎn)擊按鈕”,他會(huì)盯著屏幕看,直到按鈕出現(xiàn)并準(zhǔn)備就緒才下手。 - 瀏覽器上下文(Context)
Playwright 引入了“上下文”的概念。你可以把它想象成瀏覽器的無(wú)痕模式。啟動(dòng)一個(gè)瀏覽器實(shí)例后,可以開(kāi)啟多個(gè)完全隔離的上下文,每個(gè)上下文都有自己的 Cookie 和緩存,互不干擾,且啟動(dòng)速度極快。 - 網(wǎng)絡(luò)攔截
Playwright 可以攔截網(wǎng)頁(yè)發(fā)送的 API 請(qǐng)求。比如你可以偽造后臺(tái)返回的 JSON 數(shù)據(jù),測(cè)試前端在接口報(bào)錯(cuò)時(shí)的反應(yīng),而 Selenium 很難直接做到這一點(diǎn)。
如果你在做新項(xiàng)目,追求測(cè)試速度、穩(wěn)定性,或者需要處理復(fù)雜的現(xiàn)代網(wǎng)頁(yè),Playwright 是目前的最佳選擇。
一、 maven
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.58.0</version>
<scope>compile</scope>
</dependency>二、第一個(gè)示例
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import java.nio.file.Paths;
public class MainServer {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.webkit().launch();
Page page = browser.newPage();
page.navigate("https://blog.csdn.net/qq_33697094");
//截圖
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("example.png")));
}
}
}
運(yùn)行上面的代碼,會(huì)運(yùn)行無(wú)頭瀏覽器在后臺(tái)打開(kāi) https://blog.csdn.net/qq_33697094 網(wǎng)頁(yè),然后截圖為example.png 保存到當(dāng)前項(xiàng)目目錄下。
如果是第一次使用 Playwright ,運(yùn)行代碼時(shí) Playwright 會(huì)將 Chromium、WebKit 和 Firefox 最新版本的瀏覽器和截圖錄頻工具 ffmpeg 下載到操作系統(tǒng)特定的緩存文件夾中:
- Windows 系統(tǒng):%USERPROFILE%\AppData\Local\ms-playwright
- macOS 系統(tǒng):~/Library/Caches/ms-playwright
- Linux 系統(tǒng):~/.cache/ms-playwright
三、有頭瀏覽器和無(wú)頭瀏覽器
有頭瀏覽器會(huì)打開(kāi)一個(gè)瀏覽器界面進(jìn)行操作
try (Playwright playwright = Playwright.create()) {
// 使用有頭瀏覽器,并且增加慢動(dòng)作,方便看清操作
Browser browser = playwright.chromium().launch(
new BrowserType.LaunchOptions().setHeadless(false)
//如果在開(kāi)發(fā)階段你可以使用setSlowMo來(lái)放慢執(zhí)行速度,方便在界面上觀看
//.setSlowMo(100)
);
}
無(wú)頭瀏覽器不會(huì)打開(kāi)瀏覽器界面,只會(huì)在后臺(tái)運(yùn)行
try (Playwright playwright = Playwright.create()) {
// 使用無(wú)頭瀏覽器
Browser browser = playwright.chromium().launch();
}
四、使用不同的瀏覽器
使用有頭谷歌瀏覽器
try (Playwright playwright = Playwright.create()) {
// 使用有頭谷歌瀏覽器
Browser browser = playwright.chromium().launch(
new BrowserType.LaunchOptions().setHeadless(false)
);
}
使用有頭火狐瀏覽器
try (Playwright playwright = Playwright.create()) {
// 使用有頭火狐瀏覽器
Browser browser = playwright.firefox().launch(
new BrowserType.LaunchOptions().setHeadless(false)
);
}
五、單頁(yè)面與多頁(yè)面
如果你的頁(yè)面點(diǎn)擊或者其他操作會(huì)打開(kāi)新的 tab 頁(yè)面,并且你需要在新頁(yè)面進(jìn)行操作,那么你需要使用創(chuàng)建上下文管理多頁(yè)面
多頁(yè)面:
// 創(chuàng)建上下文(Context),用于管理多個(gè)頁(yè)面 BrowserContext context = browser.newContext(); Page page = context.newPage();
單頁(yè)面:
Page page = browser.newPage();
六、定位 器
在 Playwright 中,通過(guò)標(biāo)簽、Class、ID 定位主要通過(guò) page.locator() 方法實(shí)現(xiàn)。它支持標(biāo)準(zhǔn)的 CSS 選擇器 語(yǔ)法。
1. 基礎(chǔ)定位方式
以下是 Java 版本的語(yǔ)法對(duì)照表:
| 定位目標(biāo) | CSS 語(yǔ)法 | Playwright 代碼示例 |
|---|---|---|
| ID | #id | page.locator("#mSearchInput") |
| Class | .class | page.locator(".m-search-btn") |
| 標(biāo)簽名 | tag | page.locator("input") |
| 屬性匹配 | [attr=val] | page.locator("[type='submit']") |
| 包含匹配 | [attr*=val] | page.locator("[href*='csdn.net']") |
| 層級(jí)(后代) | parent child | page.locator(".container .title") |
| 層級(jí)(直接子級(jí)) | parent > child | page.locator("ul > li") |
2. 組合定位(更精準(zhǔn))
當(dāng)單個(gè)屬性不唯一時(shí),你可以將它們組合起來(lái):
- 標(biāo)簽 + ID:input#search-input(定位 ID 為 search-input 的 input 標(biāo)簽)
- 標(biāo)簽 + Class:div.content-box(定位含有 content-box 類的 div)
- 多類名組合:.btn.btn-primary.active(同時(shí)擁有這三個(gè) class 的元素)
3. 層級(jí)定位(尋找子元素)
在實(shí)際業(yè)務(wù)中,由于 ID 可能會(huì)重復(fù)(雖然不合規(guī)范)或者 Class 太通用,我們常使用層級(jí)關(guān)系:
- 后代選擇器 (空格):#container .title(尋找 ID 為 container 下的所有含 title 類的元素)
- 直接子元素 (>):ul > li(只尋找 ul 下的第一層 li 標(biāo)簽)
// 定位搜索列表下的第一個(gè)標(biāo)題鏈接
page.locator(".search-list-con .title-box > a").first().click();
4. 屬性選擇器進(jìn)階
如果你不想寫(xiě)全稱,或者某些屬性值是動(dòng)態(tài)生成的,可以使用“模糊匹配”:
- 包含某字符串 (=):[href=“csdn.net”](鏈接中包含 csdn.net)
- 以某字符串開(kāi)頭 (=):[id=“post-”](ID 以 post- 開(kāi)頭)
- 以某字符串結(jié)尾 ( = ) : [ s r c =):[src =):[src=“.png”](圖片后綴是 .png)
5. Playwright 特有的增強(qiáng)定位
Playwright 對(duì) CSS 選擇器做了一些非常好用的擴(kuò)展:
- 內(nèi)部文本匹配 (:has-text):
// 找到一個(gè) div,它里面必須包含 "登錄" 這兩個(gè)字
page.locator("div:has-text('登錄')");
- 根據(jù)內(nèi)部元素定位 (:has):
// 找到一個(gè) div,它里面必須包含一個(gè) <button> 標(biāo)簽
page.locator("div:has(button)");
6.實(shí)戰(zhàn)建議:如何在控制臺(tái)調(diào)試?
在瀏覽器 F12 的 Console 面板中,你可以直接測(cè)試你的 CSS 選擇器是否正確:
使用 $$(“.your-class”):這會(huì)返回頁(yè)面上所有匹配該 CSS 的元素。
使用 $(“.your-class”):這會(huì)返回匹配到的第一個(gè)元素。
七、打開(kāi)指定鏈接
page.navigate("https://blog.csdn.net/qq_33697094?type=blog");
八、等待加載完成
等待頁(yè)面加載完成
page.waitForLoadState();
等待指定元素標(biāo)簽加載完成
// 等待id是content_views的標(biāo)簽內(nèi)容加載完成
rticlePage.locator("#content_views").waitFor();
等元素出現(xiàn)(最常用)
//等待指定元素標(biāo)簽加載完成
page.waitForSelector("#submit");
等元素可見(jiàn)
page.waitForSelector(
"#submit",
new Page.WaitForSelectorOptions().setState(WaitForSelectorState.VISIBLE)
);
等元素消失
page.waitForSelector(
".loading",
new Page.WaitForSelectorOptions().setState(WaitForSelectorState.DETACHED)
);
其他點(diǎn)擊、文本填充操作時(shí)會(huì)自動(dòng)等待
Playwright 內(nèi)置自動(dòng)等待 Playwright 在執(zhí)行 click / fill / hover 等操作時(shí),會(huì)自動(dòng)進(jìn)行: - 元素存在檢查 - 元素可見(jiàn)性檢查 - 元素是否被遮擋 - 元素是否處于穩(wěn)定狀態(tài)(動(dòng)畫(huà)結(jié)束) - 是否可接收事件 這些行為被稱為 Actionability Checks。
九、獲取所有的 p 標(biāo)簽內(nèi)容
Page page = context.newPage();
//獲取id是#content_views 的標(biāo)簽下的所有p標(biāo)簽
List<String> allParagraphs = page.locator("#content_views p").allInnerTexts();
十、獲取某一指定文本的 p 標(biāo)簽內(nèi)容
Page page = context.newPage();
//獲取id是#content_views 的標(biāo)簽下的所有p標(biāo)簽,然后二次過(guò)濾搜索含有"手寫(xiě)大樂(lè)透"的p標(biāo)簽,獲取標(biāo)簽中的內(nèi)容
String specificText = articlePage.locator("#content_views p")
.filter(new Locator.FilterOptions().setHasText("手寫(xiě)大樂(lè)透"))
.innerText();
十一、獲取某一指定文本的 a 標(biāo)簽然后點(diǎn)擊
Page page = context.newPage();
//獲取含有"Java實(shí)現(xiàn)彩票大樂(lè)透、雙色球機(jī)選號(hào)"的a標(biāo)簽 (使用 getByRole 定位(語(yǔ)義化,推薦))
Locator articleLink = page.getByRole(AriaRole.LINK,
new Page.GetByRoleOptions().setName("Java實(shí)現(xiàn)彩票大樂(lè)透、雙色球機(jī)選號(hào)"));
//點(diǎn)擊
articleLink.click();
或者:
Page page = context.newPage();
//獲取含有"Java實(shí)現(xiàn)彩票大樂(lè)透、雙色球機(jī)選號(hào)"的a標(biāo)簽 (使用 CSS 選擇器結(jié)合文本過(guò)濾(更靈活))
Locator articleLink = page.locator("a.block-title")
.filter(new Locator.FilterOptions().setHasText("Java實(shí)現(xiàn)彩票大樂(lè)透"));
articleLink.click();
十二、跳轉(zhuǎn)到新頁(yè)簽后 Page 上下文的切換
// 創(chuàng)建上下文(Context),用于管理多個(gè)頁(yè)面
BrowserContext context = browser.newContext();
Page page = context.newPage();
Locator searchInput = page.locator("#mSearchInput");
searchInput.fill("Java實(shí)現(xiàn)彩票大樂(lè)透、雙色球機(jī)選號(hào)");
// 處理新標(biāo)簽頁(yè)跳轉(zhuǎn),即點(diǎn)擊后會(huì)打開(kāi)新窗口,我們需要告訴 Playwright "等待新頁(yè)面彈出"
Page newPage = context.waitForPage(() -> {
// 點(diǎn)擊搜索按鈕,這里執(zhí)行觸發(fā)跳轉(zhuǎn)的動(dòng)作,可以是點(diǎn)擊按鈕,也可以是回車
page.locator(".m-search-sure").click();
});
十三、如果按鈕可見(jiàn)則點(diǎn)擊按鈕
Locator readMoreBtn = page.locator("#btn-readmore");
if (readMoreBtn.isVisible()) {
readMoreBtn.click();
}
十四、模擬按下鍵盤回車
page.keyboard().press("Enter");
十五、網(wǎng)絡(luò)攔截與監(jiān)聽(tīng)(接口過(guò)濾)。
如果我使用Playwright打開(kāi)某一個(gè)鏈接,它會(huì)請(qǐng)求很多接口,你可以過(guò)濾搜索到指定的接口,然后獲取接口里的響應(yīng)。
你不需要為了數(shù)據(jù)去解析復(fù)雜的 HTML DOM,你可以直接“監(jiān)聽(tīng)”后臺(tái)接口返回的 JSON 數(shù)據(jù),這通常比爬取頁(yè)面元素更準(zhǔn)確、更豐富。
在 Playwright (Java) 中,主要有兩種方式來(lái)實(shí)現(xiàn):
方案一:waitForResponse (最推薦,適合“觸發(fā)-等待”場(chǎng)景)
這種方式的邏輯是:“我點(diǎn)擊了一個(gè)按鈕,我知道它會(huì)觸發(fā)一個(gè) API 請(qǐng)求,我讓程序暫停,直到捕獲到這個(gè)請(qǐng)求并拿回?cái)?shù)據(jù)后再繼續(xù)。”
這是處理搜索、翻頁(yè)、加載更多等操作的最穩(wěn)健方式。
代碼示例:
假設(shè)你在 CSDN 點(diǎn)擊搜索,想獲取點(diǎn)擊某個(gè)按鈕觸發(fā)的接口的返回 JSON 結(jié)果
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.Response;
import java.io.IOException;
import java.util.function.Predicate;
public class MainServer {
public static void main(String[] args) throws IOException {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(
new BrowserType.LaunchOptions().setHeadless(false)
);
// 創(chuàng)建上下文(Context),用于管理多個(gè)頁(yè)面
BrowserContext context = browser.newContext();
Page page = context.newPage();
// 1. 定義你想要捕獲的接口特征 (Predicate)
// 比如:URL 包含 "community-search-pc" 且狀態(tài)碼是 200
Predicate<Response> searchResponsePredicate = response ->
response.url().contains("community-search-pc") && response.status() == 200;
page.navigate("https://blog.csdn.net/qq_33697094?type=blog");
// 2. 執(zhí)行動(dòng)作并等待響應(yīng)
// waitForResponse 會(huì)等待內(nèi)部的 callback (點(diǎn)擊操作) 觸發(fā)的那個(gè)請(qǐng)求
Response response = page.waitForResponse(searchResponsePredicate, () -> {
// 這里的頁(yè)面點(diǎn)擊會(huì)觸發(fā)一個(gè)網(wǎng)絡(luò)接口請(qǐng)求
page.locator(".search-btn").click();
});
// 3. 獲取接口數(shù)據(jù)
System.out.println("接口 URL: " + response.url());
// 獲取響應(yīng)體 (通常是 JSON 字符串)
String jsonResponse = response.text();
System.out.println("接口返回?cái)?shù)據(jù): " + jsonResponse);
//關(guān)閉瀏覽器
browser.close();
}
}
}
方案二:onResponse (監(jiān)聽(tīng)模式,適合“全局監(jiān)控”)
這種方式的邏輯是:“不管我做什么操作,只要瀏覽器收到了符合條件的接口響應(yīng),就立刻記錄下來(lái)。”
這適合用來(lái)被動(dòng)收集數(shù)據(jù),或者監(jiān)聽(tīng)頁(yè)面加載時(shí)自動(dòng)發(fā)出的多個(gè)請(qǐng)求。
代碼示例:
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import java.io.IOException;
public class MainServer {
public static void main(String[] args) throws IOException {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(
new BrowserType.LaunchOptions().setHeadless(false)
);
// 創(chuàng)建上下文(Context),用于管理多個(gè)頁(yè)面
BrowserContext context = browser.newContext();
Page page = context.newPage();
// 1. 在頁(yè)面操作之前,先注冊(cè)一個(gè)監(jiān)聽(tīng)器
page.onResponse(response -> {
// 過(guò)濾條件:只關(guān)心包含 "get-business-list" 的接口
if (response.url().contains("get-business-list") && response.status() == 200) {
System.out.println("捕獲到目標(biāo)接口: " + response.url());
// 注意:response.text() 可能會(huì)拋出異常(如果響應(yīng)還沒(méi)完全接收),建議加 try-catch
try {
System.out.println("接口響應(yīng)的數(shù)據(jù)內(nèi)容: " + response.text());
} catch (Exception e) {
e.printStackTrace();
}
}
});
//打開(kāi)鏈接,會(huì)自動(dòng)監(jiān)聽(tīng)所有請(qǐng)求,監(jiān)聽(tīng)里然后會(huì)自動(dòng)獲取url含有 get-business-list 的接口,并打印它的響應(yīng)
page.navigate("https://blog.csdn.net/qq_33697094?type=blog");
//關(guān)閉瀏覽器
browser.close();
}
}
}
十六、登錄狀態(tài)持久化
方案一:?jiǎn)?dòng)持久化上下文 (Persistent Context)
這種方式更接近于“指定特定賬號(hào) Profile”。它會(huì)像你平常用的 Chrome 瀏覽器一樣,在磁盤上創(chuàng)建一個(gè)文件夾來(lái)存放所有的緩存、Cookie 和歷史記錄。
public static void main(String[] args) {
// 指定一個(gè)文件夾路徑來(lái)存放用戶數(shù)據(jù)
Path userDataDir = Paths.get("C:\\playwright_profiles\\my_user");
Playwright playwright = Playwright.create();
// 注意:這里用的是 launchPersistentContext,而不是 launch
BrowserContext context = playwright.chromium()
.launchPersistentContext(
userDataDir,
new BrowserType.LaunchPersistentContextOptions()
.setHeadless(false)
);
Page page = context.newPage();
page.navigate("https://csdn.net");
//你在彈出的瀏覽器里手動(dòng)操作完成登錄(驗(yàn)證碼等)
page.pause();
// 完成登錄后,點(diǎn) Play ? 繼續(xù),或者關(guān)閉瀏覽器,所有登錄狀態(tài)都將保存
}
以后:直接用該賬號(hào)打開(kāi)網(wǎng)頁(yè)
public static void main(String[] args) {
Path userDataDir = Paths.get("C:\\playwright_profiles\\my_user");
Playwright playwright = Playwright.create();
BrowserContext context = playwright.chromium()
.launchPersistentContext(
userDataDir,
new BrowserType.LaunchPersistentContextOptions()
.setHeadless(false)
);
Page page = context.newPage();
page.navigate("https://csdn.net");
// 已經(jīng)是登錄狀態(tài)
//....
//關(guān)閉瀏覽器
Browser browser = context.browser();
browser.close();
}
方案二:使用 Storage State
public static void main(String[] args) {
Playwright playwright = Playwright.create();
Browser browser = playwright.chromium()
.launch(new BrowserType.LaunchOptions()
.setHeadless(false)); // 必須 false,方便你手動(dòng)登錄
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://csdn.net");
//程序在這里“凍結(jié)”,等你手動(dòng)操作完成登錄(驗(yàn)證碼等)
page.pause();
// 完成登錄后,點(diǎn) Play ? 繼續(xù),手動(dòng)觸發(fā)保存
context.storageState(
new BrowserContext.StorageStateOptions()
.setPath(Paths.get("csdn-login.json"))
);
browser.close();
}
隨后:直接復(fù)用登錄狀態(tài)
public static void main(String[] args) {
Playwright playwright = Playwright.create();
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext(
new Browser.NewContextOptions()
.setStorageStatePath(Paths.get("csdn-login.json"))
);
Page page = context.newPage();
page.navigate("https://csdn.net");
// 已經(jīng)是登錄狀態(tài)
}
兩種方式的本質(zhì)對(duì)比
| 維度 | Persistent Context | Storage State |
|---|---|---|
| 保存內(nèi)容 | 整個(gè)瀏覽器環(huán)境 | cookies + localStorage |
| 是否像真實(shí) Chrome | ????? | ??? |
| 是否包含插件/擴(kuò)展 | ? | ? |
| 是否占用 Profile 鎖 | 是 | 否 |
| 多賬號(hào)并行 | 一般 | ????? |
| CI / 服務(wù)器 | 一般 | ????? |
| 穩(wěn)定性 | 極高 | 高 |
| 推薦場(chǎng)景 | 長(zhǎng)期賬號(hào)、真人模擬 | 批量賬號(hào)、自動(dòng)化 |
十七、示例
示例一:
執(zhí)行下面的操作,會(huì)打開(kāi)我的博客主頁(yè),然后在輸入框里輸入 “Java實(shí)現(xiàn)彩票大樂(lè)透、雙色球機(jī)選號(hào)”,然后點(diǎn)擊搜索,在跳轉(zhuǎn)后的搜索頁(yè)面點(diǎn)擊 “Java實(shí)現(xiàn)彩票大樂(lè)透、雙色球機(jī)選號(hào)”
文章標(biāo)題,跳轉(zhuǎn)到新的博客文章頁(yè)面,獲取博客里的第一行文章內(nèi)容。
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.options.AriaRole;
public class MainServer {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(
new BrowserType.LaunchOptions().setHeadless(false)
);
// 創(chuàng)建上下文(Context),用于管理多個(gè)頁(yè)面
BrowserContext context = browser.newContext();
Page page = context.newPage();
// 導(dǎo)航到博客主頁(yè)
page.navigate("https://blog.csdn.net/qq_33697094?type=blog");
// 點(diǎn)擊博客內(nèi)的搜索圖標(biāo)(展開(kāi)搜索框),click點(diǎn)擊時(shí)內(nèi)部會(huì)自動(dòng)等待元素加載
page.locator(".m-search-btn").click();
// 輸入搜索關(guān)鍵詞
Locator searchInput = page.locator("#mSearchInput");
searchInput.fill("Java實(shí)現(xiàn)彩票大樂(lè)透、雙色球機(jī)選號(hào)");
System.out.println("--------------------------------------------------------------------------");
// 處理點(diǎn)擊跳轉(zhuǎn)到新標(biāo)簽頁(yè)跳轉(zhuǎn),CSDN 點(diǎn)擊搜索后會(huì)打開(kāi)新窗口,我們需要告訴 Playwright "等待新頁(yè)面彈出"
Page searchPage = context.waitForPage(() -> {
// 點(diǎn)擊搜索按鈕
page.locator(".m-search-sure").click();
});
// 等待新頁(yè)面加載完成
searchPage.waitForLoadState();
System.out.println("新頁(yè)面標(biāo)題: " + searchPage.title());
// 篩選并點(diǎn)擊目標(biāo)文章標(biāo)題(使用 getByRole 定位(語(yǔ)義化,推薦))
Locator articleLink = searchPage.getByRole(AriaRole.LINK,
new Page.GetByRoleOptions().setName("Java實(shí)現(xiàn)彩票大樂(lè)透、雙色球機(jī)選號(hào)"));
System.out.println("--------------------------------------------------------------------------");
// 處理點(diǎn)擊跳轉(zhuǎn)到新標(biāo)簽頁(yè)博客文章
Page articlePage = context.waitForPage(() -> {
// 點(diǎn)擊文章標(biāo)題的動(dòng)作
articleLink.click();
});
// 等待新頁(yè)面加載完成
articlePage.waitForLoadState();
// 等待正文內(nèi)容加載完成
articlePage.locator("#content_views").waitFor();
// 獲取特定的某一段文字
String specificText = articlePage.locator("#content_views p")
.filter(new Locator.FilterOptions().setHasText("手寫(xiě)大樂(lè)透"))
.innerText();
System.out.println("找到的目標(biāo)文字: " + specificText);
//關(guān)閉瀏覽器
browser.close();
}
}
}
示例二:
下面的代碼會(huì)每隔5分鐘運(yùn)行一次,每次會(huì)打開(kāi) slack 聊天網(wǎng)站檢查對(duì)方有沒(méi)有給自己發(fā)送新的消息,如果有新消息則使用 ffemng 播放視頻提醒
import com.hai.tang.util.FfmpegUtils;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.options.AriaRole;
import com.microsoft.playwright.options.WaitUntilState;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
public class MainServer {
public static void main(String[] args) throws IOException {
//程序運(yùn)行時(shí)間,每隔5分鐘運(yùn)行一次
long intervalMinus= 5;
//對(duì)話里最后一條消息,用來(lái)和最新的消息比對(duì),如果新消息里不包含該則文字,說(shuō)明有新消息
String lastMessage = "Okay, this is noted. I will send an update if it's ready.";
long interval = intervalMinus * 60 * 1000;
System.out.println("=== 自動(dòng)化監(jiān)控腳本啟動(dòng) ===");
while (true) {
try {
System.out.println("正在執(zhí)行任務(wù),當(dāng)前時(shí)間: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// 執(zhí)行具體的業(yè)務(wù)邏輯
messageApiResponseGet_Kathryn(lastMessage);
System.out.println("本次任務(wù)結(jié)束,等待 20 分鐘后繼續(xù)...");
System.out.println("----------------------------------------");
// 休眠 2 分鐘
Thread.sleep(interval);
} catch (InterruptedException e) {
e.printStackTrace();
break; // 如果被手動(dòng)停止,跳出循環(huán)
} catch (Exception e) {
System.err.println("任務(wù)執(zhí)行過(guò)程中發(fā)生異常: " + e.getMessage());
e.printStackTrace();
// 即使報(bào)錯(cuò),通常也希望繼續(xù)等待下一次運(yùn)行,而不是直接崩潰退出
try {
Thread.sleep(interval);
} catch (InterruptedException ex) {
ex.printStackTrace();
break;
}
}
}
}
public static void messageApiResponseGet_Kathryn(String lastMessage) {
Path userDataDir = Paths.get("C:\\Users\\Public\\google_my_user");
Playwright playwright = Playwright.create();
// 注意:這里用的是 launchPersistentContext,而不是 launch
BrowserContext context = playwright.chromium()
.launchPersistentContext(
userDataDir,
new BrowserType.LaunchPersistentContextOptions()
.setHeadless(false)
);
Page page = context.newPage();
// 監(jiān)聽(tīng)所有響應(yīng)
page.onResponse(response -> {
String url = response.url();
// 過(guò)濾指定接口
if (url.contains("thethinklivebeteam.slack.com/api/conversations.view")) {
System.out.println("捕獲到目標(biāo)接口: " + url);
try {
String body = response.text(); // 獲取響應(yīng)體
System.out.println("接口響應(yīng)數(shù)據(jù):");
System.out.println(body);
//調(diào)用工具類解析響應(yīng),查找history下的messages
List<Map<String, Object>> jsonArrayAsListOfMap = LabelSearchUtil.getJsonArrayAsListOfMap(body, "history.messages");
if (null!=jsonArrayAsListOfMap){
Map<String, Object> stringObjectMap = jsonArrayAsListOfMap.get(0);
String text = String.valueOf(stringObjectMap.get("text"));
System.out.println(text);
if (!text.contains(lastMessage)){
System.out.println("you have new message from Kathryn!");
//調(diào)用工具類 使用ffemng 播放視頻播放并指定循環(huán)次數(shù)
FfmpegUtils.playVideoAudio("C:\\Users\\LENOVO\\Downloads\\Videos2026-02-13_030052_345.mp4",10);
}else{
System.out.println("guan bi");
context.browser().close();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
page.navigate("https://thethinklivebeteam.slack.com/archives/D0A3V61JPQF",
new Page.NavigateOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED));
System.out.println("111");
// 1. 定義定位 器:尋找文字包含 "open this link" 的鏈接
Locator targetLink = page.getByRole(AriaRole.LINK,
new Page.GetByRoleOptions().setName("open this link in your browser"));
System.out.println("222");
// 2. 判斷邏輯:如果元素在當(dāng)前頁(yè)面可見(jiàn),則點(diǎn)擊
if (targetLink.isVisible()) {
System.out.println("檢測(cè)到目標(biāo)鏈接,正在點(diǎn)擊...");
targetLink.click();
} else {
System.out.println("目標(biāo)鏈接未出現(xiàn),跳過(guò)。");
}
page.pause();
}
}上面每 5 分鐘創(chuàng)建一次瀏覽器實(shí)例
,這會(huì)導(dǎo)致:內(nèi)存泄漏、每次循環(huán)創(chuàng)建 browser Chrome 實(shí)例疊加 ,并且使用 page.onResponse 來(lái)異步監(jiān)聽(tīng),這并不可靠。
下面是優(yōu)化版本:
import com.hai.tang.util.FfmpegUtils;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.Response;
import com.microsoft.playwright.TimeoutError;
import com.microsoft.playwright.options.AriaRole;
import com.microsoft.playwright.options.LoadState;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
public class MainServer {
public static void main(String[] args) {
long intervalMinus = 5;
String lastMessage = "Okay, this is noted. I will send an update if it's ready.";
long interval = intervalMinus * 60 * 1000;
System.out.println("=== 自動(dòng)化監(jiān)控腳本啟動(dòng) ===");
Path userDataDir = Paths.get("C:\\Users\\Public\\google_my_user");
Playwright playwright = Playwright.create();
// 注意:這里用的是 launchPersistentContext,而不是 launch
BrowserContext context = playwright.chromium()
.launchPersistentContext(
userDataDir,
new BrowserType.LaunchPersistentContextOptions()
.setHeadless(false)
);
Page page = context.newPage();
page.navigate("https://thethinklivebeteam.slack.com/archives/D0A3V61JPQF");
while (true) {
try {
System.out.println("正在執(zhí)行任務(wù),當(dāng)前時(shí)間: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// 執(zhí)行具體的業(yè)務(wù)邏輯
messageApiResponseGet_Kathryn(lastMessage, page);
System.out.println("本次任務(wù)結(jié)束,等待 20 分鐘后繼續(xù)...");
System.out.println("----------------------------------------");
// 休眠 2 分鐘
Thread.sleep(interval);
} catch (InterruptedException e) {
e.printStackTrace();
break; // 如果被手動(dòng)停止,跳出循環(huán)
} catch (TimeoutError e) {
System.out.println("未檢測(cè)到接口響應(yīng),自動(dòng)重試...");
page.reload();
}catch (Exception e) {
System.err.println("任務(wù)執(zhí)行過(guò)程中發(fā)生異常: " + e.getMessage());
e.printStackTrace();
// 即使報(bào)錯(cuò),通常也希望繼續(xù)等待下一次運(yùn)行,而不是直接崩潰退出
try {
Thread.sleep(interval);
} catch (InterruptedException ex) {
ex.printStackTrace();
break;
}
}
}
}
public static void messageApiResponseGet_Kathryn(String lastMessage, Page page) {
// 使用 waitForResponse 等待特定API響應(yīng)
Response response = page.waitForResponse(
r -> r.url().contains("thethinklivebeteam.slack.com/api/conversations.view"),
new Page.WaitForResponseOptions().setTimeout(15000),
() -> {
page.reload();
page.waitForLoadState(LoadState.DOMCONTENTLOADED);
// 1. 定義定位 器:尋找文字包含 "open this link" 的鏈接
Locator targetLink = page.getByRole(
AriaRole.LINK,
new Page.GetByRoleOptions().setName("open this link in your browser")
);
// 2. 判斷邏輯:如果元素在當(dāng)前頁(yè)面可見(jiàn),則點(diǎn)擊
if (targetLink.count() > 0 &&targetLink.isVisible()) {
System.out.println("檢測(cè)到中間頁(yè),點(diǎn)擊跳轉(zhuǎn)...");
targetLink.click();
}
}
);
String body = response.text();
System.out.println("接口響應(yīng)數(shù)據(jù):");
System.out.println(body);
List<Map<String, Object>> jsonArrayAsListOfMap = LabelSearchUtil.getJsonArrayAsListOfMap(body, "history.messages");
if (null != jsonArrayAsListOfMap) {
Map<String, Object> stringObjectMap = jsonArrayAsListOfMap.get(0);
String text = String.valueOf(stringObjectMap.get("text"));
System.out.println(text);
if (!text.contains(lastMessage)) {
System.out.println("you have new message from Kathryn!");
//ffemng play music
//視頻播放并指定循環(huán)次數(shù)
FfmpegUtils.playVideoAudio("C:\\Users\\LENOVO\\Downloads\\Videos2026-02-13_030052_345.mp4", 10);
}
}
}
}十八、使用CLI 工具運(yùn)行自動(dòng)代碼生成器(Codegen)
不會(huì)寫(xiě)代碼?直接錄制!
Playwright 提供了極其強(qiáng)大的 codegen 工具。只需在終端輸入一行命令,它就會(huì)打開(kāi)瀏覽器并記錄你的所有操作,自動(dòng)生成對(duì)應(yīng)的 Java 代碼。
前提條件:
- 你必須已經(jīng)安裝了 Maven,并且 mvn 命令在你的環(huán)境變量中。
- 你的 maven項(xiàng)目的 pom.xml 必須已經(jīng)引入了 Playwright 的依賴(因?yàn)?Maven 需要通過(guò)項(xiàng)目配置去下載并找到那個(gè) CLI 類所在的 JAR 包)。
- 項(xiàng)目的 pom.xml 所在目錄打開(kāi) cmd 命令行
命令行中執(zhí)行命令:
mvn exec:java -e -Dexec.mainClass="com.microsoft.playwright.CLI" -Dexec.args="codegen https://blog.csdn.net"
這條命令的意思是通過(guò) Maven 運(yùn)行 Playwright 自帶的 CLI 工具,啟動(dòng) codegen(錄制腳本)功能,并打開(kāi)https://blog.csdn.net,如果你想操作其他網(wǎng)址,可替換命令行中的鏈接。
第一次執(zhí)行命令可能會(huì)有點(diǎn)慢,它會(huì)下載必要的一些依賴,請(qǐng)耐心等待。
執(zhí)行效果:
你會(huì)看到兩個(gè)窗口同時(shí)彈出:
- 一個(gè)真實(shí)的瀏覽器窗口:你在這里面正常的點(diǎn)擊、輸入。
- 一個(gè) Playwright Inspector 窗口:它會(huì)實(shí)時(shí)生成對(duì)應(yīng)的 Java 代碼。
最后你只需要直接把生成的代碼貼進(jìn)你的 main 方法里運(yùn)行和微調(diào)即可。
下面的代碼是生成的代碼,和上面第十五的示例效果一樣,但是更簡(jiǎn)潔
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.options.AriaRole;
public class MainServer {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
.setHeadless(false));
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate("https://blog.csdn.net/qq_33697094?type=blog");
page.locator(".m-search-btn").click();
page.getByRole(AriaRole.TEXTBOX, new Page.GetByRoleOptions().setName("搜TA的內(nèi)容")).click();
page.getByRole(AriaRole.TEXTBOX, new Page.GetByRoleOptions().setName("搜TA的內(nèi)容")).fill("Java實(shí)現(xiàn)彩票大樂(lè)透、雙色球機(jī)選號(hào)");
Page page1 = page.waitForPopup(() -> {
page.locator("#navList-box").getByText("搜索").click();
});
Page page2 = page1.waitForPopup(() -> {
page1.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Java實(shí)現(xiàn)彩票大樂(lè)透、雙色球機(jī)選號(hào)")).click();
});
String specificText = page2.locator("#content_views p")
.filter(new Locator.FilterOptions().setHasText("手寫(xiě)大樂(lè)透"))
.innerText();
System.out.println("找到的目標(biāo)文字: " + specificText);
//關(guān)閉瀏覽器
browser.close();
}
}
}
以上就是Java使用Playwright自動(dòng)化測(cè)試的超詳細(xì)教程的詳細(xì)內(nèi)容,更多關(guān)于Java Playwright自動(dòng)化測(cè)試的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mybatis中association標(biāo)簽的使用解讀
這篇文章主要介紹了mybatis中association標(biāo)簽的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
java并發(fā)數(shù)據(jù)包Exchanger線程間的數(shù)據(jù)交換器
這篇文章主要為大家介紹了java并發(fā)數(shù)據(jù)包使用數(shù)據(jù)交換器Exchanger來(lái)進(jìn)行線程之間的數(shù)據(jù)交換。有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-03-03
詳解Java如何實(shí)現(xiàn)一個(gè)BlockingQueue
這篇文章主要為大家詳細(xì)介紹了Java如何實(shí)現(xiàn)一個(gè)BlockingQueue阻塞隊(duì)列,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-06-06
SpringBoot MockMvc單元測(cè)試的示例代碼
這篇文章主要介紹了SpringBoot MockMvc單元測(cè)試的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-11-11
IntelliJ IDEA 2020 安裝和常用配置(推薦)
這篇文章主要介紹了IntelliJ IDEA 2020 安裝和常用配置(推薦),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
Java基礎(chǔ)學(xué)習(xí)之IO流應(yīng)用案例詳解
這篇文章主要為大家詳細(xì)介紹了Java?IO流的三個(gè)應(yīng)用案例:點(diǎn)名器、集合到文件和文件到集合,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-09-09
Java中使用instanceof判斷對(duì)象類型的示例
在List<Object>中遍歷Object時(shí),先判斷類型,再定向轉(zhuǎn)換,本文給大家介紹Java中使用instanceof判斷對(duì)象類型,感興趣的朋友跟隨小編一起看看吧2023-08-08
Java計(jì)算兩個(gè)時(shí)間相差的秒數(shù)怎么算
這篇文章主要介紹了Java計(jì)算兩個(gè)時(shí)間相差的秒數(shù),通過(guò)實(shí)例代碼補(bǔ)充介紹了Java 獲取兩個(gè)時(shí)間的時(shí)間差(時(shí)、分、秒)問(wèn)題,感興趣的朋友跟隨小編一起看看吧2024-03-03
RabbitMQ高級(jí)應(yīng)用之消費(fèi)端限流策略basicQos詳解
這篇文章主要介紹了RabbitMQ高級(jí)應(yīng)用之消費(fèi)端限流策略basicQos詳解,高并發(fā)情況下,隊(duì)列里面一瞬間就就積累了上萬(wàn)條數(shù)據(jù),但是消費(fèi)者無(wú)法同時(shí)處理這么多請(qǐng)求,這種場(chǎng)景下我們就需要對(duì)消費(fèi)端進(jìn)行限流,需要的朋友可以參考下2023-08-08

