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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...