node js 后台解析HTML(获取微博热门搜搜)

一,背景

最近自己准备写一个热门排行榜的功能,自己百度一下Node js 能够解析html的库,自己找到cheerio,这个库发现非常适合自己,因为他的用法跟jquery 类似。于是我拿微博热门搜索来练手,熟悉cheerio库,顺便用一个node js 网络库got

二,逻辑

通过网页源代码,可以分析每个tr下面td class=td-02 下面a标签就是我们要找的元素,那么通过jquery 语法写法 $(“tbody>tr>td.td-02>a”),逻辑就这么简单。

三,代码

const html_parse = require("cheerio")
const got = require("got")

async function getWeibo(){
    var all_data = []
    var respond = await got("https://s.weibo.com/top/summary?cate=realtimehot", {headers:{
        "user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36 Edg/92.0.902.78"
    }})

    if(respond.body){
        let $ = html_parse.load(respond.body)
        let items = $("tbody>tr>td.td-02>a")

        for(let i = 1; i < 10 && i < items.length; i++){
            let item = "https://s.weibo.com";
            let href = $(items[i]).attr("href")
            let text = $(items[i]).text()
            all_data.push({
                href: item + href,
                text: text
            })
        }
    }
    return all_data
}


async function test(){
    let r = await getWeibo()
    console.log(r)
}

test()

代码是不是非常简单,await 只是异步语法糖,自从我用c# 就喜欢用这个语法,避免死亡回调,网络库用got ,我请求替换user-agent,貌似微博也没有做限制,你不设置照样也能获取数据,但我还是加了,以免带来不必要的麻烦,这里面let i = 1 从开始循环,因为它有一个置顶元素,设置1直接过滤掉,这里10 只是我自己主要10条,你可以通过参数传递获取条数。

四:效果图

五,补充

我以前解析html,我都是当成字符串切割,然后再自己解析,虽然这种比较快,但写代码每次都要计算切割,不够通用,如果直接用Html 解析库,就很容易获取了,特别这种类似jquery的查找方法的。


自己最近写前端比较多,经常看到说jquery 落寞之类,我自己反复思考这个问题,我觉得这个无稽之谈。jquery 只是封装操作dom的类而已,vue 或者 react 他们另外数据库绑定而已,他们只是隐藏了dom操作而已,但他们底层也必须实现dom操作元素。对于新手来说用vue 或者react 等等非常舒服,但毕竟不是根本。html + javascript + css 这个永远不会变,那么dom操作永远都需要。这个就好像windows开发程序员,你用了mfc封装 window 操作类,你就觉得windows api 是垃圾可笑。

我觉得开发不能完全只管业务,不去了解任何有关底层知识,这种就是舍本逐源的做法。

javascript this 探索

一,背景

自己是以C入门的,然后c++,然后c#,然后java,然后python 然后node js,然后其他。。。对javascript this 理解与以前其他语言有出入,导致使用的时候会遇到this指向问题,我估计对于新手应该都会遇到这个问题。

二,坑

javascript中 funtion是对象,这句话我相信很多新手都会听到,那么自然认为在function 中的this就是指向本身,因为这句话本身是错误,那么我们用this怎么可能是对呢。反正我被同事错误说法误导很多次了。。。

三,代码

<!DOCTYPE html>
<html>
    <script>
        function foo(){
            console.log(this.a)
        }

        var a = "我在windows作用域里面"
        var obj = {
            a:"我在obj的作用域",
            foo: foo
        }

        foo()   //全局作用域 
        obj.foo()   //obj作用域
        var temp_function = obj.foo
        temp_function() //全局,跟调用的时候绑定

        
    </script>
</html>

四,说明

this 是在调用时候绑定,跟敲代码作用域没有关系,而跟调用作用域有关系。

五,书籍

<你不知道的javascript>,看他的第二部分this内容写的这边博客,自己总结一下。


六,补充

可以看mdn 官网权威文档介绍,js 万物皆对象,函数的this是动态绑定,这个跟我们其他高级语言不一样,编译的器已经确定this指向了

this – JavaScript | MDN (mozilla.org)

electron 拦截下载,启动浏览器下载

一,说明

electron 不像浏览器会带下载管理,electron 页面调用下载的话,无法感知下载进度,我这里偷懒,直接丢给默认浏览器下载。

二,代码

    one_plugin_ui.webContents.session.on("will-download", (event, item, webContents)=>{
      let url = item.getURL()
      item.cancel()
      shell.openExternal(url);
      webContents.loadFile("download.html")
    })
  • one_plugin:electron的 windows
  • win-download:触发下载事件
  • item.cancel():取消electron默认下载逻辑
  • shell.openExternal:调用默认程序(浏览器)打开url
  • webContents.loadFile:加载自己定义下载界面【因为点击文件下载,会弹出新的窗口,默认是空白的,为了体验好一点,我增加一个提示下载的页面】
  • item: 对应的electron的对象 downloaditem

三,文档

downloaditem的介绍

fishtools 1.0.0.5 更新

一,作用

本软件目的扩展现有web网页,变成自己本地应用。

二,更新

  • 增加无痕模式,方便多个账号使用
  • 增加右键菜单,方便使用
  • 使用系统浏览器下载
  • 其他小的优化

三,细节

1:右键菜单

增加置顶和开发者模式,这样子方便自己调试脚本

2:增加系统下载

要electron下载走系统默认浏览器,因为electron没有下载页面,必须自己实现才可以。我懒的写下载页面,直接要他跳默认浏览器即可。这个功能我主要用于下载考虑。

四,总结

目前自己用起来非常顺手,后续慢慢增加优化,优化给自己用。

链接:https://pan.baidu.com/s/1CbE9oUISgyTyVg5Pv5Ug2w
提取码:1234

同屏,协作,PC控制手机技术总结

一,背景

最近用一直都有研究这类技术,我总算把大概技术搞清楚了。

二,开源代码

1,minicap

openstf/minicap: Stream real-time screen capture data out of Android devices. (github.com)

用c++ 编译ndk 可执行程序,用的ndk私有API,所以他依赖aosp代码,所以有适配问题,如果系统魔改很厉害化,可能就无法运行,兼容性比其他开源感觉会差一些,毕竟底层so变化挺大,不是接口。

2,scrcpy

Genymobile/scrcpy: Display and control your Android device (github.com)

利用app_process+ java层反射,从而可以进行录屏,而从达到实时传送手机屏幕效果,兼容性理论比minicap要好很多,不管怎么说也是 java层,业务层代码,给上层调用。minicap fps 理论比采用录屏的效果差,采集方式不一样,性能不一样。

3:qtscrcpy

增加许多附加功能,用scrpty开发的

三,技术点

本质技术:通过分析aosp 源代码查看截屏代码的实现和录屏实现。

scrcpy:app_proccess 启动 apk或者 jar(必须dex化),这样子可以直接调用java程序,获取shell 用户权限,通过这个特征可以过系统权限问题。应用层直接反射获取实现截屏或者录制屏幕是没有权限surface权限的,所以失败了,并不是class hide。

app_process 其他博客介绍

android:app_process两种用法_玩出品-CSDN博客

android录制屏幕或者截屏大概流程

深入浅出,Andorid 端屏幕采集技术实践 – 知乎 (zhihu.com)

Android PC投屏简单尝试- 自定义协议章(Socket+Bitmap) – 简书 (jianshu.com)

Android PC投屏简单尝试(录屏直播)2—硬解章(MediaCodec+RMTP) – 简书 (jianshu.com)

使用mediaprojection进行获取屏幕,看这几篇大概就能知道了

Android PC投屏简单尝试—最终章1 – 简书 (jianshu.com)

这个就能明白scrcpy截屏原理,别人写的很好,我也是看他的看明白的。

四,vysor

一款同屏软件,非开源,网上说他反射surface 调用screenshot,我自己反编译发现他也用scrcpy 录屏的功能,可能他的低fps用的 surface 调用screenshot

我自己看了一下android 11 源代码,写了一下反射,截屏成功了。

这里demo 用小米10 android 11测试的,这个screenshot改动比较少。

import android.graphics.Bitmap;

import java.io.File;
import java.io.FileOutputStream;

public class Main {
    public static void main(String args[]) {
        Bitmap bitmap = EncoderFeeder.screenshot(360, 640);
        saveBitmap(bitmap);
    }


    private static boolean saveBitmap(Bitmap bitmap){
        try {
            File file = new File("/sdcard/" + "test11.jpg");
            FileOutputStream out = new FileOutputStream(file);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
            out.flush();
            out.close();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
}


import android.graphics.Bitmap;
import android.os.Build;
import android.util.Log;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import android.graphics.Rect;
import android.view.SurfaceControl;

public class EncoderFeeder {
    public static String TAG = "EncoderFeeder";
    public static Bitmap screenshot(int screenWidth, int screenHeight) {
        String surfaceClassName;
        Log.d(TAG, "screenshot: 开始截图");
        if (Build.VERSION.SDK_INT <= 17) {
            surfaceClassName = "android.view.Surface";

        } else {
            surfaceClassName = "android.view.SurfaceControl";
        }
        Class<?> classname;
        Bitmap bm = null;
        try {
            Log.d(TAG, "screenshot: 截图的类名:" + surfaceClassName);
            classname = Class.forName(surfaceClassName);
            //printMethods(classname);
            if(false){
                return null;
            }

            Method method = classname.getDeclaredMethod("screenshot",
                    new Class[] { Rect.class, int.class, int.class, int.class });
            bm = (Bitmap) method.invoke(
                    null,
                    new Object[] { new Rect(), Integer.valueOf(screenWidth),//分辨率
                            Integer.valueOf(screenHeight), 0 });
        } catch (Exception e) {
            e.printStackTrace();
        }

        Log.d(TAG, "screenshot: 截图的后的数据:");
        //Log.d(TAG, "screenshot: 截图的后的数据:" + bm.toString());
        return bm;
    }


    public static void printMethods(Class c1){
        Method[] methods=c1.getDeclaredMethods();
        for (Method m:methods) {
            Class retType=m.getReturnType();
            String name=m.getName();

            System.out.print("  ");
            String modifiers= Modifier.toString(m.getModifiers());
            if(modifiers.length()>0)
                System.out.print(modifiers+" ");
            System.out.print(retType.getName()+" "+name+"(");
            Class[] paraTypes=m.getParameterTypes();
            for (int i = 0; i < paraTypes.length; i++) {
                if(i>0)
                    System.out.print(", ");
                System.out.print(paraTypes[i].getName());
            }
            System.out.println(");");
        }
    }
}

测试截屏时间:20~30ms的延迟,那么可以20~50fps的样子,那么性能还是够可以的。

可编辑Div焦点问题

一,背景

自己最近用jquery-emoji发现光标有问题,因为他用可以编辑div导致,当他点击按钮弹出表情选择框的时候,光标会回到最开始的地方,这样子导致插入表情位置不对。

二,代码

/**
 * 扩展JS
 * by 鱼儿
 */

/**
 * 模块:div 可编辑支持光标修复
 */

//上次光标选中
var last_div_editor_sel

//上次选中范围
var last_div_editor_range = []

/**
 * 
 * @param {Element} div 
 */
function extendDivEditor(div){
    div.addEventListener('blur', onDivBlur)
    div.addEventListener('focus', onDivFocus)
}

function onDivBlur() {
    if(document.selection){
        //ie10或以下
        //暂时我还没有支持
    }else{
        var sel = window.getSelection()
        if(sel.rangeCount > 0){
            last_div_editor_range = []
            for(var index = 0; index < sel.rangeCount; index++){
                last_div_editor_range.push(sel.getRangeAt(index))
            }
        }
    }
}

function onDivFocus(){
    if(document.selection){
        //ie 10 或以下
        //暂时我还没有支持
    }else{
        var sel = window.getSelection()
        for(var index = 0; index < last_div_editor_range.length; index++){
            sel.removeAllRanges()
            sel.addRange(last_div_editor_range[index])
        }
    }
}

三,说明

调用extendDivEditor,传入div元素,这样子记录失去焦点的位置,当他获取焦点就回到原来点。我暂时没有写支持ie 10。

网上对selection 和range 介绍很少,自己参考别人代码,然后带蒙写的。

植物大战僵尸 国际版本

一,背景

以前给小孩子下载这个版本,可以设置年纪,如果设置3岁,可以无限太阳,这样子给小孩子锻炼逻辑挺不错的。

二,下载

链接:https://pan.baidu.com/s/1swCIn5Zs5WAKoQW7pxhINQ
提取码:1234

三,使用

因为这个google play 下载,不过国内版本。需要goole play 服务,需要自己安装google 服务。如果你嫌弃麻烦,你可以下载 vmos 或 其他手机虚拟机(光速虚拟机,X8沙箱等等),基本他们带google服务,同时带有xp和root权限。

打开基本就能玩了,这个带有一点广告,我感觉能接受的。如果你不想看广告,直接禁止该APP访问网络即可

android 高效截图研究总结【用于远程控制】

一,背景

自己无意见发现虫洞这个软件,PC可以控制android,发现FPS 非常流程,于是触发我的好奇心,研究几天,大概知道商业用哪种技术。

二,思路

1,网上直接找就是adb shell screencap -p /stcard/xx.png,然后拉取图片,显示图片

这种用我自己的手机测试大概是2秒左右,太迟太大,结合我自己写的c语言的代码,每次启动一个新的adb来截图,所以时间基本更长。

2:上面进化版本 adb exec-out screencap -p > test.png

在手机端不存放普通,通过adb传送数据,写到本地磁盘,这个方案大概1秒左右。但还是像PPT,起码要20FPS稍微流畅一点。不能用adb shell screencap 因为控制台输出,会导致数据不对,貌似换行符号导致。我也是在google好久才找到这个办法。

3:使用ADB协议,不用每次截图启动进程,

我用的node js adbkit,实现ADB协议,貌似各种语言的都有,用脚本或者c#感觉比较方便一些。这个时候我就抛弃了c++,因为对一些图像渲染不熟悉,我把一个老外的sdl1版本改成sdl2,增加模拟点击算法,但用起来nodejs 就简单更多,直接用html 加载 base64 image即可。这个消耗时间大概260ms,所以看起来大概有一秒左右延迟,但只是偶尔棘突,采用这个方案未必不是好事情,因为不需要那么流程。而且类库很少就是使用adb 协议即可。简单远程也没有太多问题。【有时间我整理一下这个代码,直接丢出来,给有用的人,起码不要自己研究了】

4:使用adbkit frambuffer 接口,获取原始图像的数据(rgba)

貌似他这个有bug,代码肯定不对,只能拿到元素rgba的数据,他虽然提供格式png等等,但貌似代码是错误的,他根本就没有传送原始流,貌似接口回调跟他们文档对不上,我自己用jimp转换成png,然后传给网页,结果延迟大概要5秒,因为原始流没有压缩,我自己用小米10,得到元素流有8M,加上转换,传送发送5秒左右,难道adbkit 说screencap 接口比frambuffer快几倍。这个时候我在如果在android获取原始像素,然后压缩png或者其他格式,传送是不是快很多。

5:使用Minicap,达到实时传送【基本0延迟】

github上面有他们代码,我直接用airtest编译好的,用他们工具,他直接会放到local\tmp目录,我主要嫌弃编译麻烦,自己对android ndk编译没有玩过。你可以用airtest直接跑,直接连接设备,就可以点击。然后我使用了一下命令测试一下,基本实时。

三,minicap大概原理

调用aosp 私有API 获取屏幕像是图像,因为用私有API,所以必须编译aosp ,导出SO,给minicap调用,github libs里面含有到29的版本,网易的airtest到30版本了,不同android 必须编译对应so. 这个是我的理解,具体很深细节不太了解。


我思考一个技术实现,通过ADB开启远程调试,然后在android用java实现一个adb协议读取framebuffer数据然后压缩再传送给PC端,这样子延迟不是慢不慢,但不知道用adb 读取framebuffer到底慢不慢。

实现远程点击坐标转换很简单,x1/x2 = y1 /y2 ,只要计算比例即可,显示图片大小与手机大小比就可以了,html 直接监听点击转换手机坐标即可。

上面我心理路程全部写了,如果想实现实时控制手机,基本用minicap就可以了,其他方案都太慢了。这里前提是使用adb 模拟点击,你用辅助服务器 加上android 5.0 截图 api 那是另外一种事情


更新:

minicap 编译是可执行程序,可以直接adb shell 执行指定程序,然后利用编译android 底层so,调用私有api,每个版本android都要单独编译,如果系统魔改估计会有问题,毕竟二进制不一样。adnroid c++底层比较不是给外部用的,这个比java 非SDK api稳定性差多了。

srccpy 和 vysor 用的是java 层的反射 + 加上 app_process【获取高级权限】,这个就不用底层系统so,而且这些软件基本采用录屏,具体逻辑和细节在下一篇文章

我后面分析虫洞他的技术用的就是srccpy开源代码改的【基本差不多】

fishtools实现QQ软件管理独立版本

用途

国内下载软件,如果从下载站软件,要特别小心下载是捆绑软件,所以我一般用QQ管家独立版下载软件,但现在发现独立版本貌似官方不再提供,因为我知道QQ软件管理是web,所以结合自己工具fishtools变成一个独立版本。

使用

  • 1. 添加QQ url地址(这个必须)
  • 2. 点击JS插件里面云插件,QQ软件web增强

 

这样子就默认全部都是普通下载,就不会捆绑其他软件。

软件下载地址

链接:https://pan.baidu.com/s/1CbE9oUISgyTyVg5Pv5Ug2w
提取码:1234

c# 打开浏览器商用代码

一,背景

这个需求看起来不复杂,但考虑通用话比较麻烦,一些用户还是用的win7,各种魔改版本。导致各种问题,通过一段时间测试,自己整理出来代码比较通用,可以处理各种用户情况。

二,代码

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Browser
{
    class BrowserHelperV2
    {
        public void Open(string url)
        {
            //微软edge最新浏览器 google浏览器 360极速版本 360安全浏览器 火狐浏览器 默认浏览器
            //循序自己调整,建议explorer 放在最后,最后用默认方式
            //其他浏览器的执行,可以自己添加
            //不一定成功,根据自己测试,只要注册表正常安装是没有什么问题的
            string[] browserNameList = { "msedge.exe", "chrome.exe", "360chrome.exe", "360se.exe", "firefox.exe" };
            processUrl(ref url);
            foreach (var name in browserNameList)
            {
                bool ret = OpenBrowser(url, name);
                if (ret)
                {
                    return;
                }
            }

            //默认浏览器
            OpenDefaultBrowser(url);
        }

        private bool OpenBrowser(string url, string name)
        {
            ProcessStartInfo startInfo = new ProcessStartInfo();
            try
            {
                startInfo.FileName = name;
                startInfo.Arguments = url;
                Process.Start(startInfo);
                return true;
            }
            catch (Exception e)
            {
                Debug.WriteLine("打开异常:" + e.Message);
            }

            return false;
        }

        private bool OpenDefaultBrowser(string url)
        {
            try
            {
                //要引入1.2的版本,网上单独下载,但微软官方没有找到有为什么不用1.0的
                Shell32.Shell shell = new Shell32.Shell();
                shell.Open(url);
                return true;
            }
            catch (Exception)
            {
                return OpenBrowser(url, "explorer.exe");
            }
        }

        /// <summary>
        /// 处理URL,防止解析默认浏览器无法解析格式导致问题
        /// </summary>
        /// <param name="url"></param>
        private void processUrl(ref string url)
        {
            if(url.Length > 5)
            {
                var begin_url = url.Substring(0, 5);
                if (!(begin_url.StartsWith("https") || begin_url.StartsWith("http")))
                {
                    url = "http://" + url;
                }
            }  
        }
    }
}

三,逻辑

先遍历主流的浏览器,一般情况是可以的,不需要完整路径,可以打开。如果打不开,就使用默认浏览器打开,我直接用window shell 打开的,这种比较通用,为什么不用explorer打开一个浏览器,因为他多开一个explorer,这样子感觉体验不是那么好。