普通视图

发现新文章,点击刷新页面。
昨天以前Mereith's Blog

手撸一个nodejs分布式爬虫,还要可视化

2023年6月30日 19:13

曾经我最开始入编程就是从爬虫开始的,后来很久都么接触过相关的技术。最近我有一堆 vps 闲置,配置都不高,不知道干什么用,好像就是 ip 资源比较值钱,为啥不做个爬虫呢?

它必须是分布式的,比如有日志,有重试机制,有任务管理,有可视化看板,还要能检测源站状态。

以前其实学过一些爬虫框架,但现在我想试试以目前的水平能不能手撸一个,毕竟温习原来的框架也需要时间不是。

断断续续写了大半天,写完了,还配了个看板,大概这样子:

spider.png

写完了盯着自己的看板,一种成就感不予言表。 不知不觉我好像真的具有了用代码来完成一整个事情的能力🤨(以前只能完成某件事的一部分),所以写一篇文章纪念一下。

介绍

整个架构并不复杂,大概分为几个组件(文件):

  • 任务生成器
  • 任务执行器(spider)
  • 状态上报器

任务生成器

这个组件会根据我的需求生成任务上传到数据库中,后面的执行器就会拉取任务执行,任务的状态信息也会存在数据库里。

以小说爬虫举例子,大概这个东西分为两步:

  • 探测小说站的所有小说 url 范围
  • 根据这个范围生成任务

每个任务大概有这两个核心字段:

  • 任务 url
  • 任务状态: idle | processing | success | failed

执行器(爬虫)

大概是这样的:

  • 根据最大并行限制,从数据库获取上一步生成的任务。把任务状态改成 processing
  • 爬取数据,保存
  • 如果成功了,状态就是 success,不然就是 failed
  • 中间哪里失败了,就回滚状态

当然,还有一些其他参数,不一一说明。另外我把每一个组件都抽成个类,对于这个组件,只要输入特定几个参数,并实现核心爬取函数就可以了。 改动很少的代码就可以完成不同种类任务的适配。

状态上报

一个很简单的 prometheus 上报器,他会每间隔一定时间,抓取数据库中任务表里不同状态的任务数量人后上报。

另外还会定时 ping 一下源站,看看延时啥的,也上报一下。 最后配一个看板就好啦。

配看板的过程中顺便温习了一下 grafanaprometheus 的部署和配置 - -

结尾

也许以后我会更好的抽象一下,抽象程度够高的话,我就做一个 nodejs 的爬虫框架(起码自己用着很舒服),然后写个 UI、写个后台,做一个爬虫解决方案?

其实我知道,有很多现有的方案,我这么折腾也没啥用,也算是重复造轮子。但是造轮子本身可以检验自己的技术,增加经验,实现之后真的是很快乐的事情,不是吗?

HomeLab 分享

2023年6月28日 17:32

作为一名从初中就开始自己装机的垃圾佬,工作之后当然要组一台服务器放家里了!(不然为什么我们两个人我要租三居室,当然是一个拿出来做书房放🐔箱了呀!)

image.png

概览

大概的里面放了:

  • 联通 1000M 和移动 200M 两条宽带
  • 一台 UPS
  • 一台 EPYC 服务器(7302 16c32t 、128G 、8TB P4510、Tesla P40 24G)
  • 一台 e5 洋垃圾服务器(e5 2670v2 x2、64G)
  • 一台万兆 5 口交换机
  • 一台 8 口 2.5G 交换机
  • 一台软路由 n5095(装了 esxi )
  • 一台威联通 TS-551 的 NAS

架构

流程图.jpg

  • 同一个网段,只有一层 NAT
  • 提供网络的软硬件与服务器分开,方便维护和管理(之前 ALL IN BOOM 过)
  • 软路由内使用 Esxi
  • iKuai 负责流控、DHCP、多线负载均衡
  • Openwrt 负责魔法上网
  • Debian 负责提供内网的 http/https 反代服务(nginx-ingress-controller)、一键回家的 WireGuard,和一些网络基本设施监控运维(prometheus、grafana 等)
  • 两台服务器均使用 PVE 并组成集群,网卡用了万兆双口电卡 X540-AT2
  • EPYC Server 内虚拟了 win2022,直通 p3510 8TB SSD 提供内网高速存储服务。
  • NAS 接了一个 usb 2.5g 网卡到交换机上,提供存储、照片、视频等服务+ PVE 集群的备份服务
  • 有一台 UPS 接到了 nas 上,通过 nas 的 nut server,同步 ups 状态到所有设备,实现断电自动关机

PVE 集群

用 pve 是因为开源,很多东西可以折腾,整体上两台服务器都是超融合的架构。
image.png
目前的话,主要是干这些活儿的:

  • 有一台 ubuntu 的虚机机,负责我的所有开源项目的开发(用了 code-server 作为我的云开发方案),有点事随时随地,想怎么开发就怎么开发,有浏览器就行,非常舒服。
  • Tesla P40 24G 计算卡直通给 ubuntu 开发虚拟机中,通过 nvidia docker 方案,跑一些感兴趣的模型啥的?(但是后面发现性能还是不行,画个画要画半天哦)折腾过 virtualGPU,但感觉也没啥用,后来就老老实实直通了。
  • 一台跑 MC server 的虚拟机,里面有大学时期做的大学的我的世界地图(但是没人玩了,当个纪念)
  • 一台 win2022 开启了 NFS、SMB3、ISCSI 协议,共享 P3510 8TB 固态。(我折腾过 TrueNAS 、unRaid、黑裙啥的),但最后还是跑了 win2022(因为各种原因)
  • 原来还有几个各个版本的虚拟机(win7、xp、win10、win11、mac os。。。),后来重置都删了,留了一个 windows 的虚拟机,RDP 连接玩玩用。
  • 有一堆,各种主流操作系统的模板,可以在想玩的时候,随时起一个主流的操作系统虚拟机。
  • 玩了一圈,发现 linux 老老实实 ssh、windows 老老实实 rdp 最方便,其次就建议用 SPICE 协议
  • 试过各种能找到的 VDI 解决方案,最后老老实实买了台主力台式机,放弃了虚拟桌面代替电脑。

NAS

Nas 对我来说最大的作用就是保存资料和重要照片,对照片进行分类整理了。
用过 Prism 等开源照片管理方案,但感觉都有很大缺陷,最后上了威联通(买了个便宜二手的)。
NAS 的型号是 TS-551,里面装了:

  • 两块 4T 机械,Raid 1
  • 一块2T 的企业级 stat 固态做系统盘
  • 一块 4T 机械,无 Raid
    另外跑了一堆服务。

服务合影

image.png

image.png

动手写一个超简单的编译器

2023年6月28日 16:52

不知不觉已经写了两年代码了,每过一阵子回头看看原来的代码,都觉得有不少值得改进的地方,这真是一件高兴的事。
原来我从来不会关心编译原理这种东西,感觉离自己很远,但做项目的过程中,操作了一些 AST,杂七杂八的接触过一些相关的知识,又发现了一个很好的教程,就跟着写了一遍,颇有收获。
在此把大概的步骤和思路记下来。

教程: Create Your Own Compiler
代码: https://github.com/Mereithhh/simple-compiler

目标

我们的目标是用 js 写一个编译器,把 lisp 编译成 js 代码。

add 123 ( sub 4 3))

可以编译成下面的 js 代码:

add(123, sub(4, 3));

用起来可能是这样的:

const compiler = require("./compiler");
const input = "(add 123 ( sub 4 3))"
const output = compiler(input);
// add(123, sub(4, 3));
const add = (a,b) => a + b;
const sub = (a, b) => a - b;
const result = eval(output)
console.log(result)
// 124

概述

要实现这样一个编译器,大概要 4 步:

  • 词法分析
  • 语法分析
  • AST转换
  • 代码生成

我们一步一步来。

词法分析

这步的目的是解析我们传入代码字符串中的有效单词,并给他们分类。我们的输入是:

add 123 ( sub 4 3))

这步完成后,就会输出这样的数组:

"type": "paren",
    "value": "("
  },
  {
    "type": "name",
    "value": "add"
  },
  {
    "type": "number",
    "value": "123"
  },
  {
    "type": "paren",
    "value": "("
  },
  {
    "type": "name",
    "value": "sub"
  },
  {
    "type": "number",
    "value": "4"
  },
  {
    "type": "number",
    "value": "3"
  },
  {
    "type": "paren",
    "value": ")"
  },
  {
    "type": "paren",
    "value": ")"
  }
]

要想实现这样的效果,我们只需要简单从头到尾捋一遍我们的输入,根据不同的情况判断就好了,大概是这样的:

const PATTERNS = [
  "(",
  ")"
]
const NUMBER = /[0-9]/;
const LETTER = /[a-zA-Z]/;
const SPACE = /\s/;

module.exports = function tokenizer(input) {
  const tokens = [];
  let current = 0;
  while (current < input.length) {
    let char = input[current];
    if (PATTERNS.includes(char)) {
      tokens.push({
        type: "paren",
        value: char,
      });
      current++;
      continue;
    }
    if (NUMBER.test(char)) {
      let value = "";
      while (NUMBER.test(char)) {
        value += char;
        char = input[++current];
      }
      tokens.push({
        type: "number",
        value,
      });
      continue;
    }

    if (LETTER.test(char)) {
      let value = "";
      while (LETTER.test(char)) {
        value += char;
        char = input[++current];
      }
      tokens.push({
        type: "name",
        value,
      });
      continue;
    }

    if (SPACE.test(char)) {
      current++;
      continue;
    }


    throw new Error(`unknow charactor: ${char}`)
  }
  return tokens;
}

语法分析

语法分析是把我们的上一步获得的词组,重新组合成树状的结构,也就是 AST。
处理好之后,会得到这样的树:

"type": "Program",
  "body": [
    {
      "type": "CallExpression",
      "name": "add",
      "params": [
        {
          "type": "NumberLiteral",
          "value": "123"
        },
        {
          "type": "CallExpression",
          "name": "sub",
          "params": [
            {
              "type": "NumberLiteral",
              "value": "4"
            },
            {
              "type": "NumberLiteral",
              "value": "3"
            }
          ]
        }
      ]
    }
  ]
}

这一步实现的核心思路是递归,定义一个当前下标的变量,和一个递归执行的函数。 在递归执行的函数里分别处理不同类型的数据,如果遇到了符号类型的数据,那就递归调用这个函数。不废话直接上代码:

module.exports = function parser(tokens) {
  let current = 0;
  // walk 函数每次运行完,都增加一个指针,以被下一次 walk
  function walk() {
    let token = tokens[current];
    if (token.type === 'number') {
      current++;
      return {
        type: 'NumberLiteral',
        value: token.value,
      }
    }

    if (token.type === 'paren' && token.value === '(') {
      // 下一个 token 才是函数名称
      token = tokens[++current];
      const expression = {
        type: "CallExpression",
        name: token.value,
        params: []
      }
      token = tokens[++current];
      while (token.value !== ")") {
        expression.params.push(walk())
        token = tokens[current]
      }
      current++;
      return expression;
    }


    throw new TypeError(`Unknow token: ${token.type}`)
  }

  const ast = {
    type: 'Program',
    body: [walk()],
  };

  return ast;
}

AST转换

上一步我们得到了 lisp 语言得 AST 树,现在我们要把这个树转换成 js 的 AST 树。
核心思路是遍历整个树,在遍历过程中针对每一种节点类型,使用不同的函数转化成另一种树,并且要定义一个位置变量,来把转换后的节点插入到新树中适合的位置。代码如下:

const traverse = require("./traverse");

module.exports = function transformer(originalAST) {
  const jsAST = {
    type: 'Program',
    body: [],
  };

  let position = jsAST.body;

  traverse(originalAST, {
    NumberLiteral(node) {
      position.push({
        type: "NumericLiteral",
        value: node.value,
      })
    },
    CallExpression(node, parent) {
      let expression = {
        type: "CallExpression",
        callee: {
          type: "Identifier",
          name: node.name
        },
        arguments: []
      };
      const prevPosition = position;
      position = expression.arguments;
      if (parent.type !== "CallExpression") {
        expression = {
          type: "ExpressionStatement",
          expression,
        };
      };
      prevPosition.push(expression)
    }
  })

  return jsAST;
}

其中遍历函数的实现如下:

module.exports = function traverse(ast, visitors) {
  function walkNode(node, parent) {
    const method = visitors[node.type];
    if (method) method(node, parent);
    if (node.type === "Program") walkNodes(node.body, node);
    else if (node.type === "CallExpression") walkNodes(node.params, node);
  }

  function walkNodes(nodes, parent) {
    nodes.forEach(node => walkNode(node, parent))
  }
  walkNode(ast, null)
}

代码生成

这步很简单,只要把我们需要的 jsAST 生成成相应的代码就好了,因为我们的编译器支持的语法非常简单(简陋),所以代码也很简洁🫤

module.exports = function generateCode(node) {
  if (node.type === "NumericLiteral") {
    return node.value;
  }
  if (node.type === "Identifier") {
    return node.name;
  }
  if (node.type === "CallExpression") {
    // name(arg1, arg2, arg3)
    return `${generateCode(node.callee)}(${node.arguments.map(generateCode).join(", ")})`
  }
  if (node.type === "ExpressionStatement") {
    return `${generateCode(node.expression)};`
  }
  if (node.type === "Program") {
    return node.body.map(generateCode).join("\n")
  }
}

总结

跟着教程走下来,发现编译器(入门)并没有我想的那么复杂,写一个最基础的编译器并不需要多高深的算法,代码也没有多难懂。 简洁的代码又非常巧妙,很少的代码实现了需要的功能。 其中递归是个很重要的应用,合理的利用递归可以写出有些“魔力”的代码。

实际上在平时的开发中,我发现“抽象”是一种非常重要的能力,一个会合理抽象的程序员,可以用很少的代码完成很强的功能,同时具有很强的扩展性和鲁棒性。

之前我看过“计算机程序的构造与解释”这门网课,结合今天的代码又有了些新的思考,很推荐这门课,有空我应该会再看一遍👀

nginx proxy manager 非标准端口反代 Host 不对

2023年4月27日 15:57

我自己家的用了 nginx proxy manager 作为提供服务的统一出口,因为没办法用 443 端口,所以用的 8443,也没改 nginx proxy manager 的默认端口,直接路由器端口转发到了 8443 上,这时候其实 Host 的请求头是错误的。

具体而言会导致我用最新版本的 code-serverorigin 校验失败,报错 1006,因为 host 端口号不对。

解决方法

最简单的应该是让 nginx proxy manager 的默认端口和路由器的转发端口一致。但我懒的改(没确定行不行)。所以用了下面的方法:

进入 nginx proxy manager 的容器内,修改 /etc/nginx/conf.d/include/proxy.conf 文件的内容,直接加上端口号就好了。

/etc/nginx/config.d/include/proxy.conf

因为选择艰难症,自己写了一套开源博客系统

2023年6月30日 19:15

合并.png

项目主页: https://vanblog.mereith.com

开源地址: https://github.com/mereithhh/van-blog

Demo 站: https://blog-demo.mereith.com

喜欢的话可以给个 star 哦 🙏

前因

我大二那年,第一次接触到了个人博客这个东东。看着别人炫酷的个人网站很羡慕,于是第一次买了一台云服务器,在网上到处搜教程,用 hexonext 主题部署了我的第一版博客。

那时候抱着巨大的热情,我折腾了背景,折腾了 live2d,折腾了鼠标特效,等等。

但用了一阵子觉得有些很不方便。因为 hexo 这类的静态网站生成器本身是没有后台的,所以我必须用自己的方式写 markdown 文件、敲命令行、发布到网上(那时候还不会搞 CI/CD)。

后面我陆续尝试了其他带后台的博客系统,比如 typechowordpress ,后者给我的感觉有些臃肿,前者感觉挺依赖主题的,很多也没有满足我的审美,有些特效加多了还挺卡,而且自带的编辑器和图床也没有很好用。

毕业的时候我用 react 写了一版带前后台的博客,SSR 渲染的博客,但是因为当时没有一个统一的规划,小问题不断,也不支持暗色模式,也没有内置图床,加载速度也并没有很快。

工作后闲暇时间,我又用 gastby 重构了一版博客,加载速度快了很多,但本质上 gastby 也是个静态页面生成器,而且每次发版都要全量构建。

后果

辞职后在家有时间了,我又想折腾一下博客,我的核心要求大概是:

  1. 最好是静态页面(SSG),方便 SEOCDN
  2. 要带一个方便的后台。
  3. 要内置图床,支持剪切板上传图片,支持不同的图床。
  4. 前后台都要支持移动端,都要支持暗色模式且能自动切换。
  5. SSG 的话希望不要每次发版都全量构建。
  6. 不要花里胡哨的特效,首屏加载一定要快。
  7. 可以 docker 一键部署。
  8. 支持访客统计和评论。

于是我调研了一番,发现现有的没有特别符合要求的,于是干脆自己写了一个,具有以下的特点:

  • 快到极致的响应速度,Lighthouse 接近满分。
  • 独一份的按需全自动 HTTPS,甚至不用填域名。
  • 包括完整的前后台和服务端。
  • 前台和后台都为响应式设计,完美适配移动端和多尺寸设备。
  • 前台和后台都支持黑暗模式,并可自动切换。
  • 前台为静态网页(SSG),并支持秒级的增量渲染,每次改动无需重新构建全部页面。
  • 静态网页,CDN 友好。
  • 基于 React,项目工程化,二次开发友好。
  • SEO 和无障碍友好。
  • 版本号展示和更新提醒。
  • 内置强大的分析功能,可统计访客等数据。并配有精美看板。
  • 内嵌评论系统。
  • 强大的 markdown 编辑器,支持图表和数学公式,一键插入 more 标记,一键剪切板及本地图片上传,
  • TOC、草稿、代码复制、访客数、评论数、分类、标签、搜索、加密、友链、打赏、自定义导航栏。
  • 多个布局设置,可自定义页面细节。
  • 支持自定义页面。
  • 可添加具有指定权限的协作者。
  • 高度客制化,可添加自定义 CSS、HTML 和 JS 代码。
  • 内置图床,并支持各种 OSS 图床、github 图床(外部图床基于 picgo)等。
  • 极致轻量化,没有花里胡哨。页面秒切换、图片懒加载。
  • docker 一键部署,支持 ARM 平台。
  • 支持 GA、百度分析
  • 简单易用的后台,支持数据的导出与导入。
  • 完善的 API,完全利用本项目后台和服务端,自己写前端或适配其他页面生成器
  • 有较完善的日志记录,后台可直接查看登录日志和 Caddy 日志。

我把它命名为 VanBlog,有兴趣的话可以试一下哦。

项目主页:https://vanblog.mereith.com
开源地址:https://github.com/mereithhh/van-blog
Demo 站: https://blog-demo.mereith.com

一键脚本部署

chmod +x vanblog.sh && ./vanblog.sh

其他部署方式和详细说明请移步 项目文档

PS: 不然的话每次改一点部署文档,所有平台都要改好麻烦233

预览图

前台-白色.png

前台-黑色.png

后台-白色.png

后台-黑色.png

debian10安装java

2022年8月5日 22:05

最近开始玩我的世界了,想给我的chromebook也装一个玩玩,但是因为里面的linux容器时debian10的,而apt直接装的openjdk没有 javaFX,没办法运行HMCL启动器,折腾了一番,还是装 oracle的官方java吧。但是网上找的有些添加仓库的,debian10没法用添加仓库的命令,只好从官网下载安装了。

下载java

在 Debian 上安装 Oracle JDK 需要从官网上下载可供安装的软件包。

版本看个人需要,在这里我玩我的世界1.15.2直接下载最新的JRE14就行了,而且直接有deb的安装包可以用。

如果下的版本没有安装包,或者像手动装,那么可以下载 Linux Compressed Archive版本的,下载下来后缀是 tar.gz

deb 安装

官网下载对应的包,直接 dpkg -i 安装,如果缺少依赖,按照提示修复就行了。

然后配置一下环境变量

# 找一下位置
whereis jvm
# 增加下面的到 /etc/profile 末尾, 具体目录要按照自己的版本情况
export JAVA_HOME=/usr/lib/jvm/jdk-18
export PATH=$PATH:$JAVA_HOME/bin
export CLASSPATH=$JAVA_HOME/lib

安装java

对于直接下载 deb安装包的,直接 dpkg -i xxxx.deb安装就好了。

而下载了可执行文件压缩包的,先创建一个目录:

sudo mkdir /usr/local/oracle-java-14

然后使用 tar命令将刚才下载的文件解压:

tar zxvf xxxxxxx.tar.gz -C /usr/local/oracle-java-14

最后,运行下面的命令创建新的方案:

sudo update-alternatives --install "/usr/bin/java" "java" "/usr/local/oracle-java-14/jdkxxxxx(你的下的版本)/bin/java" 1500

sudo update-alternatives --install "/usr/bin/javac" "javac" "/usr/local/oracle-java-14/jdkxxxxx(你的下的版本)/bin/javac" 1500

sudo update-alternatives --install "/usr/bin/javaws" "javaws" "/usr/local/oracle-java-14/jdkxxxxx(你的下的版本)/bin/javaws" 1500

完事儿了,可以测试一下 java -version哦。

❌
❌