最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java使用Playwright自動(dòng)化測(cè)試的超詳細(xì)教程

 更新時(shí)間:2026年06月11日 08:54:28   作者:西涼的悲傷  
Playwright 是由微軟開(kāi)發(fā)的開(kāi)源自動(dòng)化測(cè)試框架,它被設(shè)計(jì)用來(lái)克服 Selenium 這種老牌工具在現(xiàn)代 Web 應(yīng)用(尤其是復(fù)雜的單頁(yè)應(yīng)用 React/Vue)中的種種局限性,本文給大家介紹了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)格一致的 chromedrivergeckodriver內(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#idpage.locator("#mSearchInput")
Class.classpage.locator(".m-search-btn")
標(biāo)簽名tagpage.locator("input")
屬性匹配[attr=val]page.locator("[type='submit']")
包含匹配[attr*=val]page.locator("[href*='csdn.net']")
層級(jí)(后代)parent childpage.locator(".container .title")
層級(jí)(直接子級(jí))parent > childpage.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 ContextStorage 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)簽的使用解讀

    這篇文章主要介紹了mybatis中association標(biāo)簽的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-05-05
  • Spring5路徑匹配器PathPattern解析

    Spring5路徑匹配器PathPattern解析

    這篇文章主要介紹了Spring5路徑匹配器PathPattern,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • java并發(fā)數(shù)據(jù)包Exchanger線程間的數(shù)據(jù)交換器

    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

    詳解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è)試的示例代碼

    這篇文章主要介紹了SpringBoot MockMvc單元測(cè)試的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-11-11
  • IntelliJ IDEA 2020 安裝和常用配置(推薦)

    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)用案例詳解

    Java基礎(chǔ)學(xué)習(xí)之IO流應(yīng)用案例詳解

    這篇文章主要為大家詳細(xì)介紹了Java?IO流的三個(gè)應(yīng)用案例:點(diǎn)名器、集合到文件和文件到集合,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下
    2022-09-09
  • Java中使用instanceof判斷對(duì)象類型的示例

    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ù)怎么算

    這篇文章主要介紹了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詳解

    這篇文章主要介紹了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

最新評(píng)論

额敏县| 五峰| 萝北县| 砀山县| 平邑县| 民勤县| 漳浦县| 鄂尔多斯市| 五指山市| 德保县| 栖霞市| 饶河县| 德庆县| 兴化市| 荆门市| 新乐市| 驻马店市| 海城市| 武山县| 龙口市| 尉氏县| 高清| 东港市| 太仆寺旗| 交城县| 老河口市| 古蔺县| 堆龙德庆县| 奈曼旗| 中宁县| 永昌县| 泉州市| 阿拉善盟| 会泽县| 广饶县| 托克托县| 鸡泽县| 毕节市| 莱阳市| 达州市| 清水河县|