aardio实战篇) 下载微信公众号文章为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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/716583.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python安装包中的.dist-info作用

在使用pip install 包名 进行python第三方库的时候,安装完库之后通常会出现一个库名,还有一个.dist-info的文件,以安装yolov8所依赖的框架ultralytics为例,成功安装后会出现以下文件夹: 第一个ultralytics是概该框架包…

移动操作系统更新管理

移动操作系统更新管理是大多数移动设备管理(MDM)解决方案中提供的一项功能,它允许组织管理移动设备上的操作系统更新。MDM解决方案定期扫描设备以检查可用的移动操作系统更新,并根据配置的策略管理操作系统更新。操作系统更新管理…

Java I/O操作

引言 在Java编程中,输入和输出(I/O)操作是必不可少的部分。Java I/O通过一系列流(Stream)类和方法,支持文件操作、控制台输入输出、网络I/O等多种I/O操作。本文将详细介绍Java I/O的基础概念、文件操作、字…

C++之函数重载

函数重载概念&#xff1a; 是函数的一种特殊情况&#xff0c; C 允许在 同一作用域中 声明几个功能类似 的同名函数 &#xff0c;这 些同名函数的 形参列表(参数个数 或 类型 或 类型顺序)不同 &#xff0c;常用来处理实现功能类似数据类型 不同的问题。 #include<iostre…

struts2框架漏洞

title: struts2框架漏洞 categories: 漏洞复现 abbrlink: 48203 date: 2024-06-14 15:45:27 前言知识 ognl表达式注入 对象导航图语言&#xff0c;用于访问对象的字段、方法。基于简化访问java对象属性和调用方法需求&#xff0c;实现字段类型转化等功能&#xff1b;访问列表…

详情资料SR560(斯坦福)SR570 低噪声前置放大器

SR560 低噪声前置放大器 SR560 是一款高性能、低噪声前置放大器&#xff0c;非常适合各种应用&#xff0c;包括低温测量、光学检测和音频工程。 输入 SR560 有一个差分前端&#xff0c;输入噪声为 4 nV/√Hz&#xff0c;输入阻抗为 100 MΩ。完整的噪声系数轮廓如下图所示。…

深度解析响应式异步编程模型

上一篇文章中我们聊了一下线程池,基于线程池的多线程编程是我们在高并发场景下提升系统处理效率的有效手段,但却不是唯一的。今天我们来看一下另一种异步开发的常用手段-响应式编程模型 传统多线程模型的缺陷 多线程模型是目前应用最为广泛的并发编程手段,但凡遇到什么性能…

斯坦福SR810和SR830 DSP锁定放大器

SR810 和 SR830 DSP 锁定放大器 SR810 锁定放大器和 SR830锁定放大器以合理的成本提供高性能。SR830 同时显示信号的幅度和相位&#xff0c;而 SR810 仅显示幅度。两种仪器都使用数字信号处理 (DSP) 来代替传统锁定中的解调器、输出滤波器和放大器。SR810 和 SR830 具有 1 mHz…

泛微开发修炼之旅--18泛微OA节点后操作代码自动退回流程的代码示例

文章链接&#xff1a;17泛微OA节点后操作代码自动退回流程的代码示例

6月15号作业

使用手动连接&#xff0c;将登录框中的取消按钮使用第二中连接方式&#xff0c;右击转到槽&#xff0c;在该槽函数中&#xff0c;调用关闭函数 将登录按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin"&#xff0…

大型Web应用的模块化与组织实践:Flask Blueprints深入解析

目录 一、引言 二、Flask Blueprints概述 三、Flask Blueprints的使用 创建Blueprint对象 定义路由和视图函数 注册Blueprint 使用Blueprints组织代码 四、案例分析 创建模块目录结构 创建Blueprint对象 注册Blueprint 五、代码示例与最佳实践 1. 代码示例 …

Proxmox VE 超融合集群扩容后又平稳运行了170多天--不重启的话,488天了

五个节点的Proxmox VE 超融合集群&#xff0c;扩从了存储容量&#xff0c;全NVMe高速盘&#xff0c;单机4条3.7TB容量&#xff08;扩容前是两块NVMe加两块16TB的慢速SATA机械盘&#xff0c;拔掉机械盘&#xff0c;替换成两块NVMe&#xff09;&#xff0c;速度那叫一个快啊。 当…

专业酒窖的布局与设计:为红酒提供理想保存条件

对于云仓酒庄雷盛红酒的爱好者而言&#xff0c;拥有一个专业的酒窖是保存和欣赏这些珍贵佳酿的方式。一个布局合理、设计精良的专业酒窖不仅能提供稳定的保存条件&#xff0c;还能确保红酒在理想状态下陈年&#xff0c;释放其深邃的香气和口感。 首先&#xff0c;酒窖的位置选择…

胡说八道(24.6.9)——离散时间系统及simulink仿真

上回说道拉普拉斯变换的定义、性质以及在电路分析中的应用。今天先来谈谈simulink仿真&#xff0c;可为是让我非常的震惊&#xff0c;今天做了三种模型的应用。第一个是simulink中有限状态机的应用&#xff0c;用来解决一些复杂的逻辑问题&#xff0c;实现状态之间的转换。第一…

高考志愿填报选专业,把兴趣和职业进行结合!

把兴趣和职业进行结合&#xff0c;可以获取更加丰富的物质回报和精神回报。如果说兴趣因为职业化而消失&#xff0c;那么我只能说&#xff0c;这个是兴趣是假的。 一个真正的兴趣&#xff0c;应该具备以下几个问题&#xff0c;不问清楚的兴趣&#xff0c;只能叫一时兴起&#…

Clearedge3d EdgeWise 5.8 强大的自动化建模软件

EdgeWise是功能强大的建模软件&#xff0c;提供领先的建模功能和先进的技术&#xff0c;让您的整个过程更快更准确&#xff01;您可以获得使用自动特征提取和对象识别的 3D 建模&#xff0c;ClearEdge3D 自动建模和对象识别软件通过创建竣工文档和施工验证完成该过程。拓普康和…

LabVIEW、Matlab与Python的比较:从多角度详解三大编程工具

LabVIEW、Matlab和Python是工程和科学领域中常用的编程工具&#xff0c;各具特色。本文将从开发效率、计算性能、应用场景、学习曲线、成本和社区支持等多个角度&#xff0c;详细比较这三者的优缺点&#xff0c;帮助读者选择最适合其项目需求的编程工具。 比较维度 开发效率 La…

【每日LeetCode】递归、记忆化搜索

递归、记忆化搜索 【leetcode70 爬楼梯】 class Solution {public int climbStairs(int n) {int[] memo new int[n 1];return dfs(n, memo);}private int dfs(int i, int[] memo){if(i < 1){return 1;}if(memo[i] ! 0){return memo[i];}return memo[i] dfs(i-1,memo) d…

Web应用安全测试-权限缺失

Web应用安全测试-权限缺失 Flash跨域访问 漏洞描述&#xff1a;flash跨域通信&#xff0c;依据的是crossdomain.xml文件。该文件配置在服务端&#xff0c;一般为根目录下&#xff0c;限制了flash是否可以跨域获取数据以及允许从什么地方跨域获取数据。举个例子&#xff1a; 1、…

全域推广和标准推广可以一起做吗?可行性分析结果如何?

作为全域时代的新赛道&#xff0c;全域推广从出现之日起便备受关注&#xff0c;许多创业者经常将其与标准推广进行对比或捆绑&#xff0c;类似于全域推广和标准推广的区别、全域推广和标准推广哪个好以及全域推广和标准推广可以一起做吗等问题也因此长期霸占该赛道相关话题榜单…