puppeteer( Nodejs 版 selenium )快速入门
?
From:https://www.cnblogs.com/CyLee/p/9310839.html
puppeteer 官網(wǎng):https://pptr.dev/
Puppeteer 中文文檔 (與官方同步更新):https://segmentfault.com/a/1190000015913821
Puppeteer 中文文檔?:https://learnku.com/docs/puppeteer/3.1.0
Puppeteer v1.5.0 中文翻譯:https://blog.csdn.net/DeepLies/article/details/80861761
puppeteer api 與 教程:https://pptr.dev/#?product=Puppeteer&version=v1.6.0&show=api-class-puppeteer
github 地址 以及 doc :https://github.com/GoogleChrome/puppeteer
Puppeteer的入門(mén)教程和實(shí)踐:https://juejin.im/post/59f1ef1a6fb9a045211df069
大前端神器安利之 Puppeteer:https://jeffjade.com/2017/12/17/134-kinds-of-toss-using-puppeteer/
Puppeteer初探--爬取并生成《ES6標(biāo)準(zhǔn)入門(mén)》PDF:https://segmentfault.com/a/1190000010736797
詳解 Puppeteer 入門(mén)教程:https://www.jb51.net/article/139808.htm
puppeteer初探:https://juejin.im/post/5b58a1a051882519790c9295
爬蟲(chóng)利器 Puppeteer 實(shí)戰(zhàn):https://www.jianshu.com/p/a9a55c03f768
puppeteer 爬蟲(chóng)入門(mén)教程:https://blog.csdn.net/u011350541/article/details/85469918
Puppeteer之爬蟲(chóng)入門(mén):https://www.e-learn.cn/content/qita/845998
Puppeteer 實(shí)戰(zhàn)-爬取動(dòng)態(tài)生成的網(wǎng)頁(yè):https://blog.csdn.net/weixin_33724059/article/details/88031866
puppeteer實(shí)戰(zhàn)之網(wǎng)頁(yè)爬蟲(chóng),模擬操作《二》:https://blog.csdn.net/mr_xiatian/article/details/79240978
puppeteer破解滑動(dòng)驗(yàn)證碼方法:http://www.php.cn/js-tutorial-387019.html
Node:使用Puppeteer完成一次復(fù)雜的爬蟲(chóng):https://www.jianshu.com/p/97eeffa3bf3a
puppeteer的簡(jiǎn)單使用_爬取頁(yè)面信息:https://segmentfault.com/a/1190000013037078
puppeteer進(jìn)階版_爬取小說(shuō)站:https://segmentfault.com/a/1190000013055389
?
?
API 文檔
完整?API 文檔?和?例子.
?
?
Puppeteer 出現(xiàn)的背景
?
Chrome59 (linux、macos)、 Chrome60(windows)之后,Chrome自帶headless(無(wú)界面)模式很方便做自動(dòng)化測(cè)試或者爬蟲(chóng)。但是如何和 headless 模式的 Chrome 交互則是一個(gè)問(wèn)題。通過(guò)啟動(dòng) Chrome 時(shí)的命令行參數(shù)僅能實(shí)現(xiàn)簡(jiǎn)易的啟動(dòng)時(shí)初始化操作。Selenium、Webdriver 等是一種解決方案,但是往往依賴眾多,不夠扁平。
Puppeteer 是谷歌官方出品的一個(gè)通過(guò) DevTools 協(xié)議 控制 headless Chrome 的 Node 庫(kù)。可以通過(guò) Puppeteer 的提供的 api 直接控制 Chrome 模擬大部分用戶操作來(lái)進(jìn)行 UI Test 或者 作為 爬蟲(chóng) 訪問(wèn)頁(yè)面 來(lái) 收集數(shù)據(jù)。
Puppeteer(中文翻譯”木偶”) 是 Google Chrome 團(tuán)隊(duì)官方的無(wú)界面(Headless)Chrome 工具,它是一個(gè)?Node?庫(kù),提供了一個(gè)高級(jí)的 API 來(lái)控制?DevTools協(xié)議上的無(wú)頭版?Chrome 。也可以配置為使用完整(非無(wú)頭)的 Chrome。Chrome?素來(lái)在瀏覽器界穩(wěn)執(zhí)牛耳,因此,Chrome Headless 必將成為 web 應(yīng)用自動(dòng)化測(cè)試的行業(yè)標(biāo)桿。使用?Puppeteer,相當(dāng)于同時(shí)具有 Linux 和 Chrome 雙端的操作能力,應(yīng)用場(chǎng)景可謂非常之多。此倉(cāng)庫(kù)的建立,即是嘗試各種折騰使用 GoogleChrome Puppeteer;以期在好玩的同時(shí),學(xué)到更多有意思的操作。
?
?
Puppeteer 是什么,以及能做些什么
?
Puppeteer is a Node library which provides a high-level API to control headless Chrome or Chromium over the DevTools Protocol. It can also be configured to use full (non-headless) Chrome or Chromium.
簡(jiǎn)而言之,這貨是一個(gè)提供高級(jí)API的node庫(kù),能夠通過(guò)devtool控制headless模式的chrome或者chromium,它可以在headless模式下模擬任何的人為操作。
?
你可以在瀏覽器中手動(dòng)完成的大部分事情都可以使用?Puppeteer?完成!你可以從以下幾個(gè)示例開(kāi)始:
- 生成頁(yè)面的截圖和PDF。
- 抓取SPA并生成預(yù)先呈現(xiàn)的內(nèi)容(即“SSR”)。
- 從網(wǎng)站抓取你需要的內(nèi)容。
- 自動(dòng)表單提交,UI測(cè)試,鍵盤(pán)輸入等
- 創(chuàng)建一個(gè)最新的自動(dòng)化測(cè)試環(huán)境。使用最新的JavaScript和瀏覽器功能,直接在最新版本的Chrome中運(yùn)行測(cè)試。
- 捕獲您的網(wǎng)站的時(shí)間線跟蹤,以幫助診斷性能問(wèn)題。
總之:chrome 瀏覽器能干的事情 puppeteer 都能干。puppeteer 通俗來(lái)說(shuō)就是一個(gè) headless chrome瀏覽器 (當(dāng)然你也可以配置成有UI的,默認(rèn)是沒(méi)有的)。既然是瀏覽器,那么我們手工可以在瀏覽器上做的事情 Puppeteer 都能勝任, 另外,Puppeteer 翻譯成中文是”木偶”意思,所以聽(tīng)名字就知道,操縱起來(lái)很方便,你可以很方便的操縱她去實(shí)現(xiàn):
1) 生成網(wǎng)頁(yè)截圖或者 PDF?
2) 高級(jí)爬蟲(chóng),可以爬取大量異步渲染內(nèi)容的網(wǎng)頁(yè)?
3) 模擬鍵盤(pán)輸入、表單自動(dòng)提交、登錄網(wǎng)頁(yè)等,實(shí)現(xiàn) UI 自動(dòng)化測(cè)試?
4) 捕獲站點(diǎn)的時(shí)間線,以便追蹤你的網(wǎng)站,幫助分析網(wǎng)站性能問(wèn)題如果你用過(guò) PhantomJS 的話,你會(huì)發(fā)現(xiàn)她們有點(diǎn)類似,但Puppeteer是Chrome官方團(tuán)隊(duì)進(jìn)行維護(hù)的,用俗話說(shuō)就是”有娘家的人“,前景更好。
備注:?鑒于?Puppeteer?需要?Chromium,但,即便處于 Science 上網(wǎng)的姿態(tài), 也會(huì)遇到 Chromium 無(wú)法成功下載的問(wèn)題;所以在最新的修改中,已經(jīng)其替換為?puppeteer-core?(默認(rèn)情況下不下載 Chromium,使用時(shí)需要確保您安裝的?puppeteer-core?版本與您要連接的瀏覽器兼容)。在實(shí)際使用時(shí)候,即便已然按照說(shuō)明操作,但依舊會(huì)報(bào)如下錯(cuò)誤:
Error: Chromium revision is not downloaded. Run “npm install” or “yarn install”
因此只好采取手動(dòng)下載?Chromium?的方式解決;因此在運(yùn)行此倉(cāng)庫(kù)時(shí)候,您需要在?Puppeteer API Tip-Of-Tree?根據(jù)指定 Puppeteer 下載對(duì)應(yīng) Chromium,然后放置到項(xiàng)根目錄即可(項(xiàng)目中已對(duì)各不同系統(tǒng)做了適配,國(guó)內(nèi)用戶可以在?Taobao Mirrors?根據(jù)系統(tǒng)按需下載)
?
?
Puppeteer 架構(gòu)圖
?
架構(gòu)圖:
?
- Puppeteer?通過(guò) devTools 與 browser 通信
- Browser?一個(gè)可以擁有多個(gè)頁(yè)面的瀏覽器(chroium)實(shí)例
- Page?至少含有一個(gè) Frame 的頁(yè)面
- Frame?至少還有一個(gè)用于執(zhí)行 javascript 的執(zhí)行環(huán)境,也可以拓展多個(gè)執(zhí)行環(huán)境
?
?
?
環(huán)境和安裝
?
Puppetee r本身依賴 6.4 以上的Node,但是為了異步超級(jí)好用的?async/await,推薦使用7.6版本以上的Node。另外headless Chrome本身對(duì)服務(wù)器依賴的庫(kù)的版本要求比較高,centos服務(wù)器依賴偏穩(wěn)定,v6很難使用headless Chrome,提升依賴版本可能出現(xiàn)各種服務(wù)器問(wèn)題(包括且不限于無(wú)法使用ssh),最好使用高版本服務(wù)器。
要在項(xiàng)目中使用?Puppeteer,只需要運(yùn)行如下命令安裝即可;不過(guò)要注意的是:Puppeteer?至少需要 Node v6.4.0,如要使用?async / await,只有 Node v7.6.0 或更高版本才支持;另外,安裝?Puppeteer?時(shí),它會(huì)下載最新版本的?Chromium(?71Mb Mac,?90Mb Linux,?110Mb Win),保證與 API 協(xié)同工作。
Puppeteer 因?yàn)槭且粋€(gè) npm 的包,所以安裝很簡(jiǎn)單:npm i puppeteer? ? 或者? ??yarn add puppeteer
Puppeteer 安裝時(shí)自帶一個(gè)最新版本的Chromium,可以通過(guò)設(shè)置環(huán)境變量或者 npm config 中的PUPPETEER_SKIP_CHROMIUM_DOWNLOAD 跳過(guò)下載。如果不下載的話,啟動(dòng)時(shí)可以通過(guò) puppeteer.launch([options]) 配置項(xiàng)中的 executablePath 指定 Chromium 的位置。
?
?
Puppeteer 輕松入門(mén)
?
運(yùn)行環(huán)境查看 Puppeteer 的官方 API 你會(huì)發(fā)現(xiàn)滿屏的 async, await 之類,這些都是 ES7 的規(guī)范,所以你需要: Nodejs 的版本不能低于 v7.6.0, 需要支持 async, await.
需要最新的 chrome driver,
基本用法先開(kāi)看看官方的入門(mén)的 DEMO
const puppeteer = require('puppeteer'); (async () => {const browser = await puppeteer.launch();const page = await browser.newPage();await page.goto('http://example.com');await page.screenshot({ path: 'example.png' }); await browser.close(); })();上面這段代碼就實(shí)現(xiàn)了網(wǎng)頁(yè)截圖,先大概解讀一下上面幾行代碼:?
- 1. 先通過(guò) puppeteer.launch() 創(chuàng)建一個(gè)瀏覽器實(shí)例 Browser 對(duì)象
- 2. 然后通過(guò) Browser 對(duì)象創(chuàng)建頁(yè)面 Page 對(duì)象
- 3. 然后 page.goto() 跳轉(zhuǎn)到指定的頁(yè)面
- 4. 調(diào)用 page.screenshot() 對(duì)頁(yè)面進(jìn)行截圖
下面就介紹一下 puppeteer 的常用的幾個(gè) API。
?
?
?
示例 1
Puppeteer 類似其他框架,通過(guò)操作 Browser 實(shí)例 來(lái)操作瀏覽器作出相應(yīng)的反應(yīng)。
const puppeteer = require('puppeteer');(async () => {const browser = await puppeteer.launch();const page = await browser.newPage();await page.goto('http://rennaiqian.com');await page.screenshot({path: 'example.png'});await page.pdf({path: 'example.pdf', format: 'A4'});await browser.close(); })();上述代碼通過(guò)puppeteer的launch方法生成了一個(gè)browser的實(shí)例,對(duì)應(yīng)于瀏覽器,launch方法可以傳入配置項(xiàng),比較有用的是在本地調(diào)試時(shí)傳入{headless:false}可以關(guān)閉headless模式。
const browser = await puppeteer.launch({headless:false})browser.newPage方法可以打開(kāi)一個(gè)新選項(xiàng)卡并返回選項(xiàng)卡的實(shí)例page,通過(guò)page上的各種方法可以對(duì)頁(yè)面進(jìn)行常用操作。上述代碼就進(jìn)行了截屏和打印pdf的操作。
一個(gè)很強(qiáng)大的方法是 page.evaluate(pageFunction, ...args),可以向頁(yè)面注入我們的函數(shù),這樣就有了無(wú)限可能。
const puppeteer = require('puppeteer');(async () => {const browser = await puppeteer.launch();const page = await browser.newPage();await page.goto('http://rennaiqian.com');// Get the "viewport" of the page, as reported by the page.const dimensions = await page.evaluate(() => {return {width: document.documentElement.clientWidth,height: document.documentElement.clientHeight,deviceScaleFactor: window.devicePixelRatio};});console.log('Dimensions:', dimensions);await browser.close(); })();需要注意的是evaluate方法中是無(wú)法直接使用外部的變量的,需要作為參數(shù)傳入,想要獲得執(zhí)行的結(jié)果也需要return出來(lái)。因?yàn)槭且粋€(gè)開(kāi)源一個(gè)多月的項(xiàng)目,現(xiàn)在項(xiàng)目很活躍,所以使用時(shí)自行查找api才能保證參數(shù)、使用方法不會(huì)錯(cuò)。
?
示例 2
對(duì)于如何使用?Puppeteer,這非常之容易;如下簡(jiǎn)易的示例,即實(shí)現(xiàn)了:導(dǎo)航到?https://example.com?并將截屏保存為 example.png;
const puppeteer = require('puppeteer');(async () => {const browser = await puppeteer.launch(); // 創(chuàng)建瀏覽器實(shí)例const page = await browser.newPage(); // 創(chuàng)建新的瀏覽器頁(yè)面await page.goto('https://example.com'); // 頁(yè)面訪問(wèn)地址 http://example.comawait page.screenshot({ path: 'example.png' }); // 頁(yè)面截圖 example.pngawait browser.close(); // 關(guān)閉瀏覽器 })();Puppeteer 設(shè)置瀏覽器頁(yè)面為 800像素 x 600像素, 屏幕截圖也依據(jù)這個(gè)大小. 如你需要調(diào)整頁(yè)面大小,可以通過(guò)?Page.setViewport().
更多示例可參考?GoogleChrome Puppeteer Usage;在略為熟悉?Puppeteer的 Api?之后,即可用來(lái)她操縱瀏覽器,來(lái)為你做些你想搞的事兒;不過(guò)值得一提的是,她現(xiàn)在還處于開(kāi)發(fā)階段,隨著版本的更替,Api 接口也有可能會(huì)跟著略有變動(dòng)。Toss Puppeteer,這是在 Github 創(chuàng)建的一個(gè)倉(cāng)庫(kù),以承載嘗試使用 GoogleChrome Puppeteer 做的各種的折騰,具體如下:
?
示例 3
舉例 - 創(chuàng)建PDF.
const puppeteer = require('puppeteer');(async () => {const browser = await puppeteer.launch();const page = await browser.newPage();await page.goto('https://news.ycombinator.com', { waitUntil: 'networkidle2' });await page.pdf({ path: 'hn.pdf', format: 'A4' });await browser.close(); })();上例中waitUntil表示等待的時(shí)長(zhǎng),參數(shù)定義在這里waitUntil?- 搜索waitUntil
Page.pdf()?訪問(wèn)這里有更多關(guān)于創(chuàng)建PDF的信息.
?
示例 4
舉例 - 通過(guò)頁(yè)面上下文 (context) 獲取頁(yè)面信息
const puppeteer = require('puppeteer');(async () => {const browser = await puppeteer.launch();const page = await browser.newPage();await page.goto('https://example.com');// Get the "viewport" of the page, as reported by the page.const dimensions = await page.evaluate(() => { // 通過(guò)evaluate執(zhí)行頁(yè)面jsreturn {width: document.documentElement.clientWidth, // 頁(yè)面寬度height: document.documentElement.clientHeight, // 頁(yè)面高度deviceScaleFactor: window.devicePixelRatio // 設(shè)備像素比};});console.log('Dimensions:', dimensions);await browser.close(); })();訪問(wèn)?Page.evaluate()?獲得更多關(guān)于?evaluate?和相關(guān)功能例如?evaluateOnNewDocumentand?exposeFunction的介紹。
?
?
調(diào)試技巧
?
顯示界面 - 最直觀的調(diào)試方法就是看到界面上發(fā)生了什么. 通過(guò)創(chuàng)建完整瀏覽器來(lái)實(shí)現(xiàn),選項(xiàng) headless: false:
const browser = await puppeteer.launch({headless: false});讓執(zhí)行慢下來(lái) - slowMo 選項(xiàng) 可以指定毫秒值,讓 Puppeteer 的執(zhí)行慢下來(lái) ,也對(duì)調(diào)試有幫助
const browser = await puppeteer.launch({headless: false,slowMo: 250 // slow down by 250ms });獲取Console的輸出 - 你既可以監(jiān)聽(tīng) console 事件, 也可以通過(guò) page.evaluate()來(lái)打印。
page.on('console', msg => console.log('PAGE LOG:', ...msg.args));await page.evaluate(() => console.log(`url is ${location.href}`));啟用詳細(xì)日志 - 所有API調(diào)用和內(nèi)部協(xié)議交互都會(huì)被記錄在 puppeteer 名字空間的 debug 模式下.
# 所有詳細(xì)的日志 env DEBUG="puppeteer:*" node script.js# 通過(guò)名字空間來(lái)控制調(diào)試日志的輸出 env DEBUG="puppeteer:*,-puppeteer:protocol" node script.js # 除了protocol外的所有消息 env DEBUG="puppeteer:session" node script.js # 只需要protocol session 消息 env DEBUG="puppeteer:mouse,puppeteer:keyboard" node script.js # 只輸出鼠標(biāo)和鍵盤(pán)日志# Protocol 的交互消息會(huì)很多. 這里的例子說(shuō)明了如何過(guò)濾掉所有Netwok的消息。 env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network'自定義運(yùn)行的 Chromium
默認(rèn)情況下, Puppeteer 會(huì)選擇自行選擇下載 Chromium 來(lái)確保其API 在當(dāng)前環(huán)境下正常運(yùn)行. 如果確認(rèn)需要運(yùn)行不同版本的 Chromium, 在創(chuàng)建瀏覽器的時(shí)候傳入executablePath參數(shù),值為目標(biāo)瀏覽器的可執(zhí)行路徑:
const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'});
?
?
額外的例子
?
這些例子從 Issue 頁(yè)面歸納而來(lái),如果有額外的需要請(qǐng)留言。
如何模擬頁(yè)面點(diǎn)擊?
通過(guò)以下page的接口, 相關(guān) issue
page.mouseMoved(x, y, options = {}) page.mousePressed(x, y, options = {}) page.mouseReleased(x, y, options = {}) page.tap(x, y, options = {}) page.touchmove() page.touchend()如何上下翻動(dòng)頁(yè)面?
通過(guò)調(diào)用page.evaluate中的 window.scrollBy來(lái)實(shí)現(xiàn), 相關(guān) issue
page.evaluate(_ => { window.scrollBy(0, window.innerHeight);避免頁(yè)面ssl認(rèn)證錯(cuò)誤信息
通過(guò)puppeteer option ignoredHTTPErrors 實(shí)現(xiàn)
page.evalute 能否返回page DOM?
你可以傳入 ObjectHandle到page.evaluate中成為DOM元素,但當(dāng)DOM被返回的時(shí)候則成 為對(duì)應(yīng)的 ObjectHandle. issue
如果需要返回,也可以返回實(shí)際需要的值,例如:
相關(guān) iusse
如何讀取和設(shè)置cookies?
通過(guò)page.setCookie 和 page.cookies 接口。 目前有一些關(guān)于該功能的使用問(wèn)題, 相關(guān) issue
如何上傳文件?
通過(guò)elementHandle.uploadFile(...filePaths) 接口。 目前只支持 input type="file" 類
型的文件提交。 相關(guān)issue
如何獲得頁(yè)面html代碼?
通過(guò) page.content()
如何關(guān)閉javascript彈框
通過(guò) dialog.accept, 相關(guān) issue
如何監(jiān)控頁(yè)面的網(wǎng)絡(luò)請(qǐng)求?
const page = await browser.newPage(); await page.setRequestInterceptionEnabled(true);page.on('request', request => {request.continue(); // pass it through. });page.on('response', response => {const req = response.request();console.log(req.method, response.status, req.url); });如何輸入內(nèi)容?
方法1 page.type
方法2 page.evaluate 后 element.value =
await page.evaluate((a, b) => {document.querySelector('#a').value = a;document.querySelector('#b').value = b;document.querySelector('#c').click();}, a, b);如何在頁(yè)面中不同的Frame中切換
通過(guò)page.frames()獲得frame的數(shù)組,使用 iframe.$ 來(lái)獲得對(duì)應(yīng)frame中的handle
例如:
獲取element中的自定義屬性值
通過(guò)page.evaluate 然后使用object.getAttribute
?
?
?
爬蟲(chóng)實(shí)踐
?
很多網(wǎng)頁(yè)通過(guò)user-agent來(lái)判斷設(shè)備,可以通過(guò)page.emulate(options)來(lái)進(jìn)行模擬。options有兩個(gè)配置項(xiàng),一個(gè)為userAgent,另一個(gè)為viewport可以設(shè)置寬度(width)、高度(height)、屏幕縮放(deviceScaleFactor)、是否是移動(dòng)端(isMobile)、有無(wú)touch事件(hasTouch)。
const puppeteer = require('puppeteer'); const devices = require('puppeteer/DeviceDescriptors'); const iPhone = devices['iPhone 6'];puppeteer.launch().then(async browser => {const page = await browser.newPage();await page.emulate(iPhone);await page.goto('https://www.example.com');// other actions...await browser.close(); });上述代碼則模擬了iPhone6訪問(wèn)某網(wǎng)站,其中devices是puppeteer內(nèi)置的一些常見(jiàn)設(shè)備的模擬參數(shù)。
很多網(wǎng)頁(yè)需要登錄,有兩種解決方案:
讓puppeteer去輸入賬號(hào)密碼
常用方法:點(diǎn)擊可以使用page.click(selector[, options])方法,也可以選擇聚焦page.focus(selector)。
輸入可以使用page.type(selector, text[, options])輸入指定的字符串,還可以在options中設(shè)置delay緩慢輸入更像真人一些。也可以使用keyboard.down(key[, options])來(lái)一個(gè)字符一個(gè)字符的輸入。
如果是通過(guò)cookie判斷登錄狀態(tài)的可以通過(guò)page.setCookie(...cookies),想要維持cookie可以定時(shí)訪問(wèn)。
?
Tip:有些網(wǎng)站需要掃碼,但是相同域名的其他網(wǎng)頁(yè)卻有登錄,就可以嘗試去可以登錄的網(wǎng)頁(yè)登錄完利用cookie訪問(wèn)跳過(guò)掃碼。
?
?
簡(jiǎn)單例子
?
示例代碼:
const puppeteer = require('puppeteer');(async () => {const browser = await puppeteer.launch({headless: false});const page = await browser.newPage();await page.goto('https://baidu.com');await page.type('#kw', 'puppeteer', {delay: 100});page.click('#su')await page.waitFor(1000);const targetLink = await page.evaluate(() => {return [...document.querySelectorAll('.result a')].filter(item => {return item.innerText && item.innerText.includes('Puppeteer的入門(mén)和實(shí)踐')}).toString()});await page.goto(targetLink);await page.waitFor(1000);browser.close(); })()運(yùn)行截圖:
?
?
?
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的puppeteer( Nodejs 版 selenium )快速入门的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python selenium 用法 和
- 下一篇: Wing IDE 5.0 破解之寻找注册