aardio實戰篇) 下載微信公眾號文章為pdf和html

来源:https://www.cnblogs.com/kanadeblisst/p/18252255
-Advertisement-
Play Games

首發地址: https://mp.weixin.qq.com/s/w6v3RhqN0hJlWYlqTzGCxA 前言 之前在PC微信逆向) 定位微信瀏覽器打開鏈接的call提過要寫一個保存公眾號歷史文章的工具。這篇文章先寫一個將文章保存成pdf和html的工具,後面再補充一個採集歷史的工具,搭配使用 ...


首發地址: https://mp.weixin.qq.com/s/w6v3RhqN0hJlWYlqTzGCxA

前言

之前在PC微信逆向) 定位微信瀏覽器打開鏈接的call提過要寫一個保存公眾號歷史文章的工具。這篇文章先寫一個將文章保存成pdf和html的工具,後面再補充一個採集歷史的工具,搭配使用就能保存所有歷史文章到本地。

如果是在瀏覽器打開文章,想保存成pdf和html很簡單,右鍵列印(pdf)和另存為(html)就可以了。想在程式里實現則需要一些自動化工具,例如playwright、puppeteer等,但這些都沒有移植到aardio。

cdp

先科普一個知識:大部分自動化工具都是基於chromium內核瀏覽器自帶的一個叫Chrome DevTools Protocol[1]的協議(後面簡稱cdp),它涵蓋了對谷歌瀏覽器的所有自動化操作。

cdp協議使用jsonrpc和谷歌瀏覽器通信,所以完全可以在aardio也實現一個類似drissionpage的庫,但是工程量不小,我沒那麼多時間去實現。所以只在用到哪部分的時候完善哪部分介面,不會去完整實現一個drissionpage。

用到的cdp介面

保存成html

cdp協議里並沒有直接獲取頁面html的介面,但是可以通過獲取頁面document.body.outerHTML的值來得到。而獲取該值則是通過Runtime.evaluate[2]介面執行js表達式並返回結果。

不過這樣保存的html打開之後,會顯示一直轉圈,並且圖片無法載入。這是因為有些圖片用的相對鏈接,解決方法就是替換相對鏈接為絕對鏈接。不過我更推薦保存成mhtml,這樣圖片就會被嵌入到html里,不需要從網路載入。

保存成mhtml

cdp協議里保存成mhtml的介面是Page.captureSnapshot[3]

保存成pdf

介面是Page.printToPDF[4]

簡單使用

aardio其實提供了cdp協議的封裝庫web.socket.chrome,用法可以在案例里搜索這個。

保存成mhtml

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

var wb = web.view(winform,,"--remote-debugging-port=29999");
winform.text = "正在打開網頁,請稍候 ……"
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();

雖然保存了,但是圖片並沒有顯示,應該是圖片還沒載入就已經開始保存了,並且有些圖片只有滑動到底部時才會載入。所以還需要先下拉到底部,讓頁面把圖片全部載入出來再進行保存。

非同步改同步

這是個非同步庫,上面的寫法看起來不太順眼,可以將它稍微封裝一下改為同步庫使用。

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;
}

這樣調用就順眼多了,當然習慣了非同步的話也可以不改。

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

滑動到底部

滑動操作用JavaScript比cdp介面要簡單的多,所以先找gpt寫一段JavaScript滑動到底部的代碼(需要多調教幾次,最初版本肯定是有錯誤的)。

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
	}
	// 開始滑動
	callWait(ws, "Runtime.evaluate", params);
	// 有時候滑動還未結束,上面的代碼就返回了,所以繼續等待
	..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)
}

封裝成庫

全部放出來代碼會太多,所以將代碼封裝成了庫(cdpdriver),放到了之前寫的aardio教程) 搭建自己的擴展庫倉庫里,有興趣的可以去github自己看怎麼實現的。

封裝的庫使用示例如下:

import cdpdriver;
import web.view;
import win.ui;
import console
/*DSG{{*/
var winform = win.form(text="cdp協議";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事件觸發表示頁面載入完成
	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("測試.mhtml", mhtml)
	}
}

initWebView()

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

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

pdf也是正常的

嚴重bug

當某個網頁的圖片特別多的時候,保存的mhtml文件特別大的時候(比如八九十兆),這時候控制台就會出現no enough memory的錯誤,經過多天的排查,沒有找到具體原因,不過我猜測是aardio非同步傳輸數據時,申請的記憶體空間小於這個文件大小,所以當傳輸文件的數據時就會出錯。

解決方法

這個解決不了只能不用這個非同步庫,自己基於官方擴展庫里的hpsocket實現一個jsonrpc。

但是官方擴展庫的hpsocket使用的dll還是2017年的版本,為了避免之前版本有未修複的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協議";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();
	// 計算網頁圖片的數量
	var imgCount = cdpClient.runJsCode('document.querySelectorAll("#img-content img").length;')
	// 如果獲取數量失敗,則預設是40
	imgCount := 40;
	// 每張圖片會多等待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("測試.pdf", pdfBytes);
		logger.info("保存pdf成功,路徑:%s", io.fullpath("測試.pdf"));
	}
	if(mhtml) {
		..string.save("測試.mhtml", mhtml);
		logger.info("保存mhtml成功,路徑:%s", io.fullpath("測試.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麻煩的多,因為它是基於多線程的,所以正常情況下推薦使用web.socket.chrome,只有當你遇到不能使用的情況,才換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

本文由博客一文多發平臺 OpenWrite 發佈!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1 Zero-shot learning 零樣本學習。 1.1 任務定義 利用訓練集數據訓練模型,使得模型能夠對測試集的對象進行分類,但是訓練集類別和測試集類別之間沒有交集;期間需要藉助類別的描述,來建立訓練集和測試集之間的聯繫,從而使得模型有效。 Zero-shot learning 就是希望我們 ...
  • 本文基於 OpenJDK17 進行討論,垃圾回收器為 ZGC。 提示: 為了方便大家索引,特將在上篇文章 《以 ZGC 為例,談一談 JVM 是如何實現 Reference 語義的》 中討論的眾多主題獨立出來。 FinalReference 對於我們來說是一種比較陌生的 Reference 類型,因 ...
  • 目錄智能指針場景引入 - 為什麼需要智能指針?記憶體泄漏什麼是記憶體泄漏記憶體泄漏的危害記憶體泄漏分類如何避免記憶體泄漏智能指針的使用及原理RAII簡易常式智能指針的原理智能指針的拷貝問題智能指針的發展歷史std::auto_ptr模擬實現auto_ptr常式:這種方案存在的問題:Boost庫中的智能指針un ...
  • NumPy 助你處理數學問題:計算序列的差分用`np.diff()`,示例返回`[5, 10, -20]`;找最小公倍數(LCM)用`np.lcm()`,數組示例返回`18`;最大公約數(GCD)用`np.gcd.reduce()`,數組示例返回`4`;三角函數如`np.sin()`,`np.deg... ...
  • 本框架JSON元素組成和分析,JsonElement分三大類型JsonArray,JsonObject,JsonString。 JsonArray:數組和Collection子類,指定數組的話,使用ArrayList來add元素,遍歷ArrayList再使用Array.newInstance生成數組 ...
  • 最近項目中使用了PowerJob做任務調度模塊,感覺這個框架真香,今天我們就來深入瞭解一下新一代的定時任務框架——PowerJob! 簡介 PowerJob是基於java開發的企業級的分散式任務調度平臺,與xxl-job一樣,基於web頁面實現任務調度配置與記錄,使用簡單,上手快速,其主要功能特性如 ...
  • 百度的,後面再補一個Linux文檔操作手冊,是不是很大膽? 準備工作 1、首先得有兩個軟體Xftp(用來上傳文件到)和XShell(連接伺服器執行命令) 2、Linux上有JDK(怎麼安裝可以轉到Linux安裝JDK流程) 3、項目的JAR包 項目jar包 導jar <build> <plugins ...
  • 本文介紹基於Python中GDAL模塊,實現基於一景柵格影像,對另一景柵格影像的像元數值加以疊加提取的方法。 本文期望實現的需求為:現有一景表示6種不同植被類型的.tif格式柵格數據,以及另一景與前述柵格數據同區域的、表示植被參數的.tif格式柵格數據;我們希望基於前者中的植被類型數據,分別提取6種 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...