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 是垃圾可笑。

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

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的介绍

nodejs console 日志统一增加时间戳

一,背景

node js 一般打印日志使用console.log ,如果现有项目希望增加打印日志,那么我们可以重载打印日志函数,我们直接用现有的功能模块

log-timestamp

二,使用

require(‘log-timestamp’);

导入即可,默认时间戳是用的国际时间,你可以传入你要写的时间戳。

const log_time = require('log-timestamp');   //增加日志时间戳
log_time(function(){
    return "[" + new Date().toLocaleString() + "]"
})

三,原理

重载函数

类似console.log = function(…){ console.old_log(…)}

fishtools 插件工具1.0.0.2

背景

这个工具我很早之前就想开发了,陆陆续续开发一段事件时间,基本功能开发完全了

目的

软件快速通过关键词或者关键词的缩写打开对应的网址,因为自己常常会开一些web,进行访问。 这个软件有点借鉴utools,这个工具可以对接任何web网页,加入js注入。
本软件只是练手electron 技术而已,后续慢慢补充各种插件和web,实现自己软件自给自足。提高自己快速编码能力和产品的驾驭能力。

用途

  • 快速访问网址,不用自己记录各种网址
  • 可以常驻后台,避免浏览器关闭,导致网页关闭
  • 自带js注入框架,方便自己扩展各种网页功能,满足更多功能【具有无限可能】
  • 后续可能的功能是窗口大小记忆

使用教程

添加自己的网址,这里主要设置关键词,这里设置js,那么当自己ctrl + alt + k 调出输入框 ,然后输入js,就会列出访问的网址。

下载云端js 插件,你也可以右键自己添加js插件,这样子就可以控制任何网页界面代码。

这里我设置正则,匹配语雀文档时候,弹出密码验证框,这样子相当于给自己的网页语雀增加了密码验证。这里密码我写死为helloworld123

下载

链接:https://pan.baidu.com/s/172DBUEKF_Y911hNik7Gtsw
提取码:1234
复制这段内容后打开百度网盘手机App,操作更方便哦–来自百度网盘超级会员V5的分享

代码

因为electron练手项目,所以暂时不开源,但electron asar只要解压就可以看到代码,所以你也可以获取到代码

TODO

  • 增加窗口大小记录
  • 增加导航栏,方便使用

electron 加载第三方页面,无法jquery问题

如题,这个官方问题里面已经写了解决方案,我只是记录一下方便整理

我在 Electron 中无法使用 jQuery、RequireJS、Meteor、AngularJS。

因为 Electron 在运行环境中引入了 Node.js,所以在 DOM 中有一些额外的变量,比如 moduleexports 和 require。 这导致 了许多库不能正常运行,因为它们也需要将同名的变量加入运行环境中。

我们可以通过禁用 Node.js 来解决这个问题,在Electron里用如下的方式:

// 在主进程中.
const { BrowserWindow } = require('electron')
const win = new BrowserWindow(format@@
  webPreferences: {
    nodeIntegration: false
  }
})
win.show()

假如你依然需要使用 Node.js 和 Electron 提供的 API,你需要在引入那些库之前将这些变量重命名,比如:

<head>
<script>
window.nodeRequire = require;
delete window.require;
delete window.exports;
delete window.module;
</script>
<script type="text/javascript" src="jquery.js"></script>
</head>

electron 打包 与 inno setup使用

背景

自己之前用electron-builder 在windows打包,默认安装到user local 目录,自己也对elecron-builder不熟悉,对inno setup打包比较熟悉,同时inno setup 可以任意目录,各种高级功能

过程

  • electron-builer 在windows打包
  • 找到生成目录里面的win-unpacked文件夹
  • inno setup 直接打包即可(直接可视化操作,你也可以修改脚本)

总结

这里用electron-builder 主要打包成asar而已,其实也可以完全用asar打包成asar。再用inno 打包即可

高效导入sql语句工具

一,背景

因为最近项目需要迁移数据库,于是我问了一下后台开发导入数据要多久?结果他说要一个小时,我觉得时间太长了,于是我搜索了一下有关快速插入sql的知识,从而用Node js 写这个小工具,方便快速导入sql。

二,原理

因为我们导出Sql数据是一行一条插入语句,执行sql时候按照每行执行一次,这样子导致写入特别多,大量消耗在网络传输中,于是我们只要把多行插入语句整合一条语句,但这里要知道mysql的一条sql最大长度是1M,所以我合并成一条时候需要注意长度,我用Node js写的工具已经处理这个问题。

三,代码

/**
 * 一个sql多张表处理,针对整体处理
 */
 const fs = require('fs')
 var sql_path = 'd:/t3.sql'
 var sava_sql_path = 'd:/batch.sql'
 const max_sql_len = 1024*1000   //mysql单个最大包1m,怕其他字节占用,我就用1000 比 1024小一些
 var global_sort_table_name = []        //全局表名,用来按照顺序插入
 
 //读取mysql文件
 function read_mysql_file(path){
     let data = fs.readFileSync(path)
     if(data.length > 0){
         data = data.toString()
     }
     return data
 }
 
 //基本解析
 //返回对应的数据
 /**
  * 
  * @param {string} data 
  * @returns 
  */
 function parse(data){
     let line_insert_key = 'INSERT INTO'
     let arrya = data.split('\n')
     let new_inert_sql = []
     var m = new Map();
     arrya.forEach(element => {
         if(element.indexOf(line_insert_key) != -1){
             let real_data = get_insert_sql_value(element)
             if(real_data){
                 if(m.has(real_data.table_name)){
                    let array = m.get(real_data.table_name)
                    array.push(real_data.data)
                 }else{
                    let  array = []
                    array.push(real_data.data)
                    global_sort_table_name.push(real_data.table_name)
                    m.set(real_data.table_name, array)
                 }
             }
         }
     });

     return m
 }
 
 /**
  * 获取插入的sql的语句
  * @param {String} inert_sql 
  */
 function get_insert_sql_value(insert_sql){
     let begin_key = 'VALUES ('
     let end_key = ');'
     let start_pos = insert_sql.indexOf(begin_key)
     let end_pos = insert_sql.indexOf(end_key)

     if(start_pos > 0 && end_pos > start_pos){
         start_pos = start_pos+begin_key.length
         let len = end_pos - start_pos
         
         let insert_right_value = insert_sql.substr(start_pos, len)
         let insert_left_value = insert_sql.substr(0, start_pos - 1)
         let table_name = get_insert_table_name(insert_left_value)

         return {
             table_name: table_name,
             data: insert_right_value
         }
     }
     return null
 }

 //获取插入的表
 function get_insert_table_name(insert_left_value){
    let begin_key = 'INSERT INTO `'
    let end_key = '` VALUES'

    if(insert_left_value && insert_left_value.indexOf('INSERT INTO') >= 0){
        let start_pos = insert_left_value.indexOf(begin_key)
        let end_pos = insert_left_value.indexOf(end_key)
        if(start_pos >= 0 && end_pos > start_pos){
            start_pos = start_pos + begin_key.length
            let len = end_pos - start_pos
            let table_name = insert_left_value.substr(start_pos, len)
            return table_name
        }
    }
    return ""
 }
 
 //创建新的插入数组
 /**
  * 
  * @param {Map} data 
  * @returns 
  */
 function create_new_insert_array(data){
    var data_array
     const template = "INSERT INTO `[表名]` VALUES"
     let current_insert_sql = []
     var new_insert_sql = template
 
     for(let ele of global_sort_table_name){
        data_array = data.get(ele)
        new_insert_sql = template
        let insert_comment = '-- ----------------------------\n'
        insert_comment += '-- ' + 'Records of ' + ele + '\n'
        insert_comment += '-- ----------------------------\n'
        new_insert_sql = insert_comment + new_insert_sql
        new_insert_sql = new_insert_sql.replace('[表名]',ele)
        

        for(let index = 0; index < data_array.length; index++){
            new_insert_sql += '(' + data_array[index]
            new_insert_sql += ')'
            if(new_insert_sql.length + 500 >= max_sql_len || (index+1 == data_array.length)){
                new_insert_sql+= ";"
                current_insert_sql.push(new_insert_sql)
                new_insert_sql = template
                new_insert_sql = new_insert_sql.replace('[表名]',ele)
            }else{
                new_insert_sql += ','
            }
        }
     }
 
     return current_insert_sql
 }
 
 /**
  * 解析完成sql对象
  * 新的文件路径
  * @param {*} sql_object 
  * @param {*} new_file_path 
  */
 function create(data_array, new_file_path){
     let new_data_string = ""
     data_array.forEach(element=>{
         new_data_string += element
         new_data_string += '\n'
     })
     fs.writeFileSync(new_file_path,new_data_string)
 }
 
 function init(){
     console.log("请设置自己原始sql路径和生成sql路径")
     let data = read_mysql_file(sql_path)
     let r = parse(data)
     let data_array = create_new_insert_array(r)
     create(data_array, sava_sql_path)
     console.log('生成' + sava_sql_path + ' 成功')
 }
 
 init()
 

四,使用

你使用需要修改sql_path改成你自己路径

node xxx.js 即可,成功输出文档到sava_sql_path路径

校验数据不否正确,可以对比2份数据库的sql数据即可

electron 安全问题思考

背景

新手使用electron 会遇遇到自己的html页面加载的Js无法使用node js功能,Preload加载干什么,上下文隔离的问题,这个对于我使用electron确实造成困扰。

关键词

  • nodeIntegration
  • preload
  • contextIsolation

详解

nodeIntegration

控制渲染进程加载页面是否能够调用node js功能,高版本默认关闭,意思就是加载的html是无法使用node js代码,如果你想用就必须设置 nodeIntegration 为 true。

preload

可以设置加载js,这个页面没有加载前就可以被调用,感觉可以用拦截器来用,具备nodejs 能力,不管nodeIntegration是否打开

contextIsolation

上下文隔离,具体是值proload 加载JS与加载普通的浏览器的dom的隔离,如果不隔离,普通页面和Preload js共享 window对象,你可以在preload js 设置 window.test = 1 ,那么普通页面页可以访问这个。在electron 12后就默认隔离上下文

为什么要这么多限制,对我们新手太不友好了?

如果你只是加载本地文件,那么全部打开都没有关系,防止你加载第三方页面导致安全问题,比喻你electron加载一个https://www.xxx.com 结果这个页面加一段electron 检测代码,然后执行恶意代码,由于你开启了nodeIntegration ,那么代码就可以跑起来。这么多限制防你引用第三方的页面,而不是开发者。但对于新手来说,可能就会带来一点门槛。

总结

按照官方的意思:preload + nodeIntegration【关闭】 + contextIsolation【开启】+普通页面开发,这样子比较安全。我感觉这样子开发有另外的好处,前端页面通用,跟node 弱关系,前端没有node js,通过proload导出函数或者对象即可。

electron 打包后路径使用问题

背景

今天自己给快速打开增加自动启动,但发现开机启动后 无法正常运行。

排查

通过开发者模式发现路径到C盘系统的目录,我原来用的process.cwd(),返回进程的当前工作目录。后面我换成process.execPath 然后通过path 模块获取文件夹路径,这样子获取一定是安装目录执行的exe。同时不要__dirname 这个代表源码目录,因为打包了,所以目录对不上。

function getexecDir(){
    var p = process.execPath;
    return path.dirname(p)
}

c++ 调用node js 回调方法

背景

node js 自己的demo ,直接把node js 回调传给c++ 然后直接回调,这样子是没有问题,但我们需要一个长期的回调,node js没有说明,网上貌似也没有对应的文档,我找了好久也没有找到。后面自己翻看node js 官网文档API ,一个个看,找到一个能用的api。

解决方法:napi_create_reference

回调对象,如果你直接保存,你下次回调的时候发现会发生错误,我分析错误信息是对象已经不能用,我看了官网文档,node js 自己管理申明周期,所以我猜测声明周期的问题,于是找他们引用的API,于是就是上面的API

napi_ref my_call_ref = nullptr;	//全局
napi_value call_back = args[1];	//传递过来的参数
status = napi_create_reference(env, call_back, 1, &my_call_ref);
//然后底层回调过来时候,调用node js回调函数就可以了
status = napi_get_reference_value(global_env, msg_recv_ref, &result);
	if (result != nullptr) {
		status = napi_call_function(global_env, global, result, 1, &buffer, &result);
		//assert(status == napi_ok); //如果上层报错,这里失败,导致程序停止
	}

上面是一个大概的代码,就是把一个回调引用是1,永远不会释放,因为我没有调用引用减少,所以永远不会释放。

备注

目前觉得小项目用node js 和 python 真的太方便,脚本写上层业务的时候,基本不好用关心语言细节,我发现好多程序员喜欢在意语法细节,我内心觉得好笑,你会用就可以了,你又不是开发语言或者写编译器的(我说那些死转语法的人,不死说不用了解语法)