aardio實(shí)戰(zhàn)篇) 下載微信公眾號(hào)文章為pdf和html

首發(fā)地址: https://mp.weixin.qq.com/s/w6v3RhqN0hJlWYlqTzGCxA

前言

之前在PC微信逆向) 定位微信瀏覽器打開鏈接的call提過要寫一個(gè)保存公眾號(hào)歷史文章的工具锅纺。這篇文章先寫一個(gè)將文章保存成pdf和html的工具礼华,后面再補(bǔ)充一個(gè)采集歷史的工具,搭配使用就能保存所有歷史文章到本地乏梁。

如果是在瀏覽器打開文章芝囤,想保存成pdf和html很簡(jiǎn)單瓮具,右鍵打印(pdf)和另存為(html)就可以了荧飞。想在程序里實(shí)現(xiàn)則需要一些自動(dòng)化工具,例如playwright名党、puppeteer等叹阔,但這些都沒有移植到aardio。

cdp

先科普一個(gè)知識(shí):大部分自動(dòng)化工具都是基于chromium內(nèi)核瀏覽器自帶的一個(gè)叫Chrome DevTools Protocol[1]的協(xié)議(后面簡(jiǎn)稱cdp)传睹,它涵蓋了對(duì)谷歌瀏覽器的所有自動(dòng)化操作耳幢。

cdp協(xié)議使用jsonrpc和谷歌瀏覽器通信,所以完全可以在aardio也實(shí)現(xiàn)一個(gè)類似drissionpage的庫欧啤,但是工程量不小睛藻,我沒那么多時(shí)間去實(shí)現(xiàn)。所以只在用到哪部分的時(shí)候完善哪部分接口邢隧,不會(huì)去完整實(shí)現(xiàn)一個(gè)drissionpage店印。

用到的cdp接口

保存成html

cdp協(xié)議里并沒有直接獲取頁面html的接口,但是可以通過獲取頁面document.body.outerHTML的值來得到倒慧。而獲取該值則是通過Runtime.evaluate[2]接口執(zhí)行js表達(dá)式并返回結(jié)果按摘。

不過這樣保存的html打開之后,會(huì)顯示一直轉(zhuǎn)圈纫谅,并且圖片無法加載炫贤。這是因?yàn)橛行﹫D片用的相對(duì)鏈接,解決方法就是替換相對(duì)鏈接為絕對(duì)鏈接付秕。不過我更推薦保存成mhtml兰珍,這樣圖片就會(huì)被嵌入到html里,不需要從網(wǎng)絡(luò)加載询吴。

保存成mhtml

cdp協(xié)議里保存成mhtml的接口是Page.captureSnapshot[3]

保存成pdf

接口是Page.printToPDF[4]

簡(jiǎn)單使用

aardio其實(shí)提供了cdp協(xié)議的封裝庫web.socket.chrome掠河,用法可以在案例里搜索這個(gè)。

保存成mhtml

import win.ui;
import console
import web.view;
import web.socket.chrome;
/*DSG{{*/
var winform = win.form(text="測(cè)試";right=759;bottom=469;bgcolor=16777215)
winform.add()
/*}}*/

var wb = web.view(winform,,"--remote-debugging-port=29999");
winform.text = "正在打開網(wǎng)頁猛计,請(qǐng)稍候 ……"
winform.show();

var ws = wb.openRemoteDebugging();
 
ws.Page.navigate(
    url = "https://mp.weixin.qq.com/s/Nik8fBF3hxH5FPMGNx3JFw";
);

wb.wait("Nik8fBF3hxH5FPMGNx3JFw");
win.delay(3000)
import crypt;
ws.Page.captureSnapshot().end = function(result,err){
   if(result[["data"]]){
       string.save("示例.mhtml", result.data)
       winform.text = "保存mhtml成功"
   } 
} 

win.loopMessage();

雖然保存了口柳,但是圖片并沒有顯示,應(yīng)該是圖片還沒加載就已經(jīng)開始保存了有滑,并且有些圖片只有滑動(dòng)到底部時(shí)才會(huì)加載跃闹。所以還需要先下拉到底部,讓頁面把圖片全部加載出來再進(jìn)行保存毛好。

異步改同步

這是個(gè)異步庫望艺,上面的寫法看起來不太順眼,可以將它稍微封裝一下改為同步庫使用肌访。

callWait = function(ws, method,params,timeout,interval){
    if(!ws) return;
    var done = null;
    var t = ..string.split(method,".");
    var func = ws;
    for(i=1;#t;1){
        func = func[t[i]];
    }
    var result;
    func(params).end = function(r,err){
        if(!err) {
            done = true;
            result = r;
        }
    };
    ..win.wait(lambda() done,winform,timeout:15000,interval);
    return result;
}

這樣調(diào)用就順眼多了找默,當(dāng)然習(xí)慣了異步的話也可以不改。

var result = callWait(ws, "Page.captureSnapshot", {});
string.save("示例.mhtml", result.data)

滑動(dòng)到底部

滑動(dòng)操作用JavaScript比cdp接口要簡(jiǎn)單的多吼驶,所以先找gpt寫一段JavaScript滑動(dòng)到底部的代碼(需要多調(diào)教幾次惩激,最初版本肯定是有錯(cuò)誤的)店煞。

scrollPageBottom = function(ws){
    ..win.delay(1000);
    var scrollToEnd = `(async function scrollPage() {
        return new Promise(async (resolve) => {
            var distance = 500; 
            var count = 0;
            window.scrollTo(0, 0);
            window.scrollTo(0, 0);
            var scroll = async () => {
                var lastScrollTop = document.documentElement.scrollTop;
                window.scrollBy(lastScrollTop, distance);
                await new Promise(r => setTimeout(r, 500)); 
                var newScrollTop = document.documentElement.scrollTop;
                var scrollHeight = document.body.scrollHeight;
                console.log(lastScrollTop, newScrollTop, scrollHeight);
                if(lastScrollTop === newScrollTop) count += 1;
                if ((lastScrollTop === newScrollTop && newScrollTop/scrollHeight > 0.8) || count > 2) {
                    resolve(); 
                } else {
                    await scroll(); 
                }
            };
            await scroll();
        });
    })();`;
    var params = {
        "expression": scrollToEnd,
        "awaitPromise": true,
        "returnByValue": true
    }
    // 開始滑動(dòng)
    callWait(ws, "Runtime.evaluate", params);
    // 有時(shí)候滑動(dòng)還未結(jié)束,上面的代碼就返回了风钻,所以繼續(xù)等待
    ..win.wait(function(){
        var r= callWait(ws, "Runtime.evaluate", {
            expression="document.documentElement.scrollTop/document.body.scrollHeight > 0.8";
            awaitPromise=true;
            returnByValue=true
        });
        return r;
    },,15000,500)
}

封裝成庫

全部放出來代碼會(huì)太多顷蟀,所以將代碼封裝成了庫(cdpdriver),放到了之前寫的aardio教程) 搭建自己的擴(kuò)展庫倉庫里骡技,有興趣的可以去github自己看怎么實(shí)現(xiàn)的鸣个。

封裝的庫使用示例如下:

import cdpdriver;
import web.view;
import win.ui;
import console
/*DSG{{*/
var winform = win.form(text="cdp協(xié)議";right=759;bottom=469)
winform.add()
/*}}*/

var initWebView = function(){
    var cmdArgs = `--remote-debugging-port=29999`;
    winform.webView = web.view(winform,,cmdArgs);
    if(!_STUDIO_INVOKED) winform.webView.enableDevTools(false);
    winform.show();
    
    winform.stateTable = {
        pageReady=null;//頁面加載完成
    }
    var ws = winform.webView.openRemoteDebugging();
    var cdpClient = cdpdriver(ws);
    // 啟用Page事件
    ws.Page.enable();
    // Page.domContentEventFired和Page.loadEventFired事件觸發(fā)表示頁面加載完成
    ws.on("Page.domContentEventFired",function(param){
        winform.stateTable.pageReady = true;
    })
    ws.on("Page.loadEventFired",function(param){
        winform.stateTable.pageReady = true;
    })
    winform.stateTable.pageReady = null;
    var url = "https://mp.weixin.qq.com/s/Nik8fBF3hxH5FPMGNx3JFw";
    winform.webView.go(url);
    win.wait(lambda() winform.stateTable.pageReady, winform.hwnd, 15000, 50);  
    win.delay(1000) 
    if(winform.stateTable.pageReady){
        cdpClient.scrollPageBottom();
        var mhtml = cdpClient.outerMHTML;
        string.save("測(cè)試.mhtml", mhtml)
    }
}

initWebView()

winform.show();
win.loopMessage();

這樣保存的mhtml圖片顯示也正常

pdf也是正常的

嚴(yán)重bug

當(dāng)某個(gè)網(wǎng)頁的圖片特別多的時(shí)候,保存的mhtml文件特別大的時(shí)候(比如八九十兆)布朦,這時(shí)候控制臺(tái)就會(huì)出現(xiàn)no enough memory的錯(cuò)誤囤萤,經(jīng)過多天的排查,沒有找到具體原因是趴,不過我猜測(cè)是aardio異步傳輸數(shù)據(jù)時(shí)涛舍,申請(qǐng)的內(nèi)存空間小于這個(gè)文件大小,所以當(dāng)傳輸文件的數(shù)據(jù)時(shí)就會(huì)出錯(cuò)唆途。

解決方法

這個(gè)解決不了只能不用這個(gè)異步庫富雅,自己基于官方擴(kuò)展庫里的hpsocket實(shí)現(xiàn)一個(gè)jsonrpc。

但是官方擴(kuò)展庫的hpsocket使用的dll還是2017年的版本窘哈,為了避免之前版本有未修復(fù)的bug,去github更新一下hpsocket的dll亭敢。

hpsocket的dll下載地址: https://github.com/ldcsaa/HP-Socket/releases

hpsocket封裝后的使用案例

import win.ui;
import web.view;
/*DSG{{*/
mainForm = win.form(text="hpsocket cdp協(xié)議";right=757;bottom=467)
mainForm.add()
/*}}*/

var threadMain = function(debugPort){
    import win;
    import cdpdriver.hpcdp;
    import cdpdriver.jsonrpc;
    import kilogging;
    
    var logger = kilogging();
    ..cdpdriver.jsonrpc.waitDebuggingPages(debugPort);
    var wsClient = ..cdpdriver.jsonrpc();
    wsClient.connect(debugPort);
    wsClient.send("Page.enable");
    wsClient.on("Page.domContentEventFired", function(){
        ..thread.set("pageReady" + owner.guid, true);
    })
    wsClient.on("Page.loadEventFired", function(){
        ..thread.set("pageReady" + owner.guid, true);
    })
    var cdpClient = ..cdpdriver.hpcdp(wsClient);
    var url = "https://mp.weixin.qq.com/s/Nik8fBF3hxH5FPMGNx3JFw";
    var pageReadyFlag = "pageReady" + wsClient.guid;
    ..thread.set(pageReadyFlag, null);
    logger.info("開始下載 (%s) pdf和html", url);
    wsClient.send("Page.navigate",{"url":url})
    win.wait(function(){
        return thread.get(pageReadyFlag);
    },, 10000, 100);
    if(!thread.get(pageReadyFlag)) {
        logger.info("頁面(%s)訪問失敗", url);
        return;
    }
    cdpClient.scrollPageBottom();
    // 計(jì)算網(wǎng)頁圖片的數(shù)量
    var imgCount = cdpClient.runJsCode('document.querySelectorAll("#img-content img").length;')
    // 如果獲取數(shù)量失敗滚婉,則默認(rèn)是40
    imgCount := 40;
    // 每張圖片會(huì)多等待300毫秒
    ..win.delay(imgCount * 300);
    var mhtmlData = cdpClient.getOuterMHTML();
    var mhtml = mhtmlData ? mhtmlData.data;
    var pdfData = cdpClient.getPdf();
    var pdf = pdfData ? pdfData.data;
    logger.info("獲取到的文件大小,pdf(%s), mhtml(%s)",tostring(#pdf), tostring(#mhtml));
    if(pdf) {
        var pdfBytes = ..crypt.bin.decodeBase64(pdf);
        ..string.save("測(cè)試.pdf", pdfBytes);
        logger.info("保存pdf成功帅刀,路徑:%s", io.fullpath("測(cè)試.pdf"));
    }
    if(mhtml) {
        ..string.save("測(cè)試.mhtml", mhtml);
        logger.info("保存mhtml成功让腹,路徑:%s", io.fullpath("測(cè)試.mhtml"));
    }   
}

var initWebView = function(){
    var cmdArgs = `--remote-debugging-port=29999`;
    mainForm.webView = web.view(mainForm,,cmdArgs);
    mainForm.show();
    
    var debugPort = mainForm.webView.remoteDebuggingPort;
    thread.invoke(threadMain,debugPort) 
}

initWebView()

mainForm.show();
return win.loopMessage();

很明顯,hpsocket寫代碼要比web.socket.chrome麻煩的多扣溺,因?yàn)樗腔诙嗑€程的骇窍,所以正常情況下推薦使用web.socket.chrome,只有當(dāng)你遇到不能使用的情況锥余,才換hpsocket腹纳。

引用鏈接

  • [1] https://chromedevtools.github.io/devtools-protocol/
  • [2] https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-evaluate
  • [3] https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureSnapshot
  • [4] https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市驱犹,隨后出現(xiàn)的幾起案子嘲恍,更是在濱河造成了極大的恐慌,老刑警劉巖雄驹,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佃牛,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡医舆,警方通過查閱死者的電腦和手機(jī)俘侠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門象缀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人爷速,你說我怎么就攤上這事央星。” “怎么了遍希?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵等曼,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我凿蒜,道長(zhǎng)禁谦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任废封,我火速辦了婚禮州泊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘漂洋。我一直安慰自己遥皂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布刽漂。 她就那樣靜靜地躺著演训,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贝咙。 梳的紋絲不亂的頭發(fā)上样悟,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音庭猩,去河邊找鬼窟她。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蔼水,可吹牛的內(nèi)容都是我干的震糖。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼趴腋,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼吊说!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起优炬,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤疏叨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后穿剖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚤蔓,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年糊余,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了秀又。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片单寂。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖吐辙,靈堂內(nèi)的尸體忽然破棺而出宣决,到底是詐尸還是另有隱情,我是刑警寧澤昏苏,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布尊沸,位于F島的核電站,受9級(jí)特大地震影響贤惯,放射性物質(zhì)發(fā)生泄漏洼专。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一孵构、第九天 我趴在偏房一處隱蔽的房頂上張望屁商。 院中可真熱鬧,春花似錦颈墅、人聲如沸蜡镶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽官还。三九已至,卻和暖如春毒坛,著一層夾襖步出監(jiān)牢的瞬間望伦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國打工粘驰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留屡谐,地道東北人述么。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓蝌数,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親度秘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子顶伞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容