翼度科技»论坛 编程开发 python 查看内容

aardio实战篇) 下载微信公众号文章为pdf和html

11

主题

11

帖子

33

积分

新手上路

Rank: 1

积分
33
首发地址: 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
  1. import win.ui;
  2. import console
  3. import web.view;
  4. import web.socket.chrome;
  5. /*DSG{{*/
  6. var winform = win.form(text="测试";right=759;bottom=469;bgcolor=16777215)
  7. winform.add()
  8. /*}}*/
  9. var wb = web.view(winform,,"--remote-debugging-port=29999");
  10. winform.text = "正在打开网页,请稍候 ……"
  11. winform.show();
  12. var ws = wb.openRemoteDebugging();
  13. ws.Page.navigate(
  14.     url = "https://mp.weixin.qq.com/s/Nik8fBF3hxH5FPMGNx3JFw";
  15. );
  16. wb.wait("Nik8fBF3hxH5FPMGNx3JFw");
  17. win.delay(3000)
  18. import crypt;
  19. ws.Page.captureSnapshot().end = function(result,err){
  20.    if(result[["data"]]){
  21.        string.save("示例.mhtml", result.data)
  22.        winform.text = "保存mhtml成功"
  23.    }
  24. }
  25. win.loopMessage();
复制代码
虽然保存了,但是图片并没有显示,应该是图片还没加载就已经开始保存了,并且有些图片只有滑动到底部时才会加载。所以还需要先下拉到底部,让页面把图片全部加载出来再进行保存。
异步改同步

这是个异步库,上面的写法看起来不太顺眼,可以将它稍微封装一下改为同步库使用。
  1. callWait = function(ws, method,params,timeout,interval){
  2.         if(!ws) return;
  3.         var done = null;
  4.         var t = ..string.split(method,".");
  5.         var func = ws;
  6.         for(i=1;#t;1){
  7.                 func = func[t[i]];
  8.         }
  9.         var result;
  10.         func(params).end = function(r,err){
  11.                 if(!err) {
  12.                         done = true;
  13.                         result = r;
  14.                 }
  15.         };
  16.         ..win.wait(lambda() done,winform,timeout:15000,interval);
  17.         return result;
  18. }
复制代码
这样调用就顺眼多了,当然习惯了异步的话也可以不改。
  1. var result = callWait(ws, "Page.captureSnapshot", {});
  2. string.save("示例.mhtml", result.data)
复制代码
滑动到底部

滑动操作用JavaScript比cdp接口要简单的多,所以先找gpt写一段JavaScript滑动到底部的代码(需要多调教几次,最初版本肯定是有错误的)。
  1. scrollPageBottom = function(ws){
  2.         ..win.delay(1000);
  3.         var scrollToEnd = `(async function scrollPage() {
  4.             return new Promise(async (resolve) => {
  5.                 var distance = 500;
  6.                 var count = 0;
  7.                 window.scrollTo(0, 0);
  8.                 window.scrollTo(0, 0);
  9.                 var scroll = async () => {
  10.                     var lastScrollTop = document.documentElement.scrollTop;
  11.                     window.scrollBy(lastScrollTop, distance);
  12.                     await new Promise(r => setTimeout(r, 500));
  13.                     var newScrollTop = document.documentElement.scrollTop;
  14.                     var scrollHeight = document.body.scrollHeight;
  15.                     console.log(lastScrollTop, newScrollTop, scrollHeight);
  16.                     if(lastScrollTop === newScrollTop) count += 1;
  17.                     if ((lastScrollTop === newScrollTop && newScrollTop/scrollHeight > 0.8) || count > 2) {
  18.                         resolve();
  19.                     } else {
  20.                         await scroll();
  21.                     }
  22.                 };
  23.                 await scroll();
  24.             });
  25.         })();`;
  26.         var params = {
  27.                 "expression": scrollToEnd,
  28.                 "awaitPromise": true,
  29.                 "returnByValue": true
  30.         }
  31.         // 开始滑动
  32.         callWait(ws, "Runtime.evaluate", params);
  33.         // 有时候滑动还未结束,上面的代码就返回了,所以继续等待
  34.         ..win.wait(function(){
  35.                 var r= callWait(ws, "Runtime.evaluate", {
  36.                         expression="document.documentElement.scrollTop/document.body.scrollHeight > 0.8";
  37.                         awaitPromise=true;
  38.                         returnByValue=true
  39.                 });
  40.                 return r;
  41.         },,15000,500)
  42. }
复制代码
封装成库

全部放出来代码会太多,所以将代码封装成了库(cdpdriver),放到了之前写的aardio教程) 搭建自己的扩展库仓库里,有兴趣的可以去github自己看怎么实现的。
封装的库使用示例如下:
  1. import cdpdriver;
  2. import web.view;
  3. import win.ui;
  4. import console
  5. /*DSG{{*/
  6. var winform = win.form(text="cdp协议";right=759;bottom=469)
  7. winform.add()
  8. /*}}*/
  9. var initWebView = function(){
  10.         var cmdArgs = `--remote-debugging-port=29999`;
  11.         winform.webView = web.view(winform,,cmdArgs);
  12.         if(!_STUDIO_INVOKED) winform.webView.enableDevTools(false);
  13.         winform.show();
  14.        
  15.         winform.stateTable = {
  16.                 pageReady=null;//页面加载完成
  17.         }
  18.         var ws = winform.webView.openRemoteDebugging();
  19.         var cdpClient = cdpdriver(ws);
  20.         // 启用Page事件
  21.         ws.Page.enable();
  22.         // Page.domContentEventFired和Page.loadEventFired事件触发表示页面加载完成
  23.         ws.on("Page.domContentEventFired",function(param){
  24.                 winform.stateTable.pageReady = true;
  25.     })
  26.         ws.on("Page.loadEventFired",function(param){
  27.                 winform.stateTable.pageReady = true;
  28.     })
  29.         winform.stateTable.pageReady = null;
  30.         var url = "https://mp.weixin.qq.com/s/Nik8fBF3hxH5FPMGNx3JFw";
  31.         winform.webView.go(url);
  32.         win.wait(lambda() winform.stateTable.pageReady, winform.hwnd, 15000, 50);  
  33.         win.delay(1000)
  34.         if(winform.stateTable.pageReady){
  35.                 cdpClient.scrollPageBottom();
  36.             var mhtml = cdpClient.outerMHTML;
  37.             string.save("测试.mhtml", mhtml)
  38.         }
  39. }
  40. initWebView()
  41. winform.show();
  42. 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封装后的使用案例
  1. import win.ui;
  2. import web.view;
  3. /*DSG{{*/
  4. mainForm = win.form(text="hpsocket cdp协议";right=757;bottom=467)
  5. mainForm.add()
  6. /*}}*/
  7. var threadMain = function(debugPort){
  8.         import win;
  9.         import cdpdriver.hpcdp;
  10.         import cdpdriver.jsonrpc;
  11.         import kilogging;
  12.        
  13.         var logger = kilogging();
  14.         ..cdpdriver.jsonrpc.waitDebuggingPages(debugPort);
  15.         var wsClient = ..cdpdriver.jsonrpc();
  16.         wsClient.connect(debugPort);
  17.         wsClient.send("Page.enable");
  18.         wsClient.on("Page.domContentEventFired", function(){
  19.                 ..thread.set("pageReady" + owner.guid, true);
  20.         })
  21.         wsClient.on("Page.loadEventFired", function(){
  22.                 ..thread.set("pageReady" + owner.guid, true);
  23.         })
  24.         var cdpClient = ..cdpdriver.hpcdp(wsClient);
  25.         var url = "https://mp.weixin.qq.com/s/Nik8fBF3hxH5FPMGNx3JFw";
  26.         var pageReadyFlag = "pageReady" + wsClient.guid;
  27.         ..thread.set(pageReadyFlag, null);
  28.         logger.info("开始下载 (%s) pdf和html", url);
  29.         wsClient.send("Page.navigate",{"url":url})
  30.         win.wait(function(){
  31.                 return thread.get(pageReadyFlag);
  32.         },, 10000, 100);
  33.         if(!thread.get(pageReadyFlag)) {
  34.                 logger.info("页面(%s)访问失败", url);
  35.                 return;
  36.         }
  37.         cdpClient.scrollPageBottom();
  38.         // 计算网页图片的数量
  39.         var imgCount = cdpClient.runJsCode('document.querySelectorAll("#img-content img").length;')
  40.         // 如果获取数量失败,则默认是40
  41.         imgCount := 40;
  42.         // 每张图片会多等待300毫秒
  43.         ..win.delay(imgCount * 300);
  44.         var mhtmlData = cdpClient.getOuterMHTML();
  45.         var mhtml = mhtmlData ? mhtmlData.data;
  46.         var pdfData = cdpClient.getPdf();
  47.         var pdf = pdfData ? pdfData.data;
  48.         logger.info("获取到的文件大小,pdf(%s), mhtml(%s)",tostring(#pdf), tostring(#mhtml));
  49.         if(pdf) {
  50.                 var pdfBytes = ..crypt.bin.decodeBase64(pdf);
  51.                 ..string.save("测试.pdf", pdfBytes);
  52.                 logger.info("保存pdf成功,路径:%s", io.fullpath("测试.pdf"));
  53.         }
  54.         if(mhtml) {
  55.                 ..string.save("测试.mhtml", mhtml);
  56.                 logger.info("保存mhtml成功,路径:%s", io.fullpath("测试.mhtml"));
  57.         }       
  58. }
  59. var initWebView = function(){
  60.         var cmdArgs = `--remote-debugging-port=29999`;
  61.         mainForm.webView = web.view(mainForm,,cmdArgs);
  62.         mainForm.show();
  63.        
  64.         var debugPort = mainForm.webView.remoteDebuggingPort;
  65.         thread.invoke(threadMain,debugPort)       
  66. }
  67. initWebView()
  68. mainForm.show();
  69. 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 发布!

来源:https://www.cnblogs.com/kanadeblisst/p/18252255
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具