专栏文章

Node.js下完成洛谷冬日绘版

科技·工程参与者 46已保存评论 49

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
49 条
当前快照
1 份
快照标识符
@mhz5tyw7
此快照首次捕获于
2025/11/15 01:57
4 个月前
此快照最后确认于
2025/11/29 05:25
3 个月前
查看原文
F45cCD.md.jpg

前言

Node.js 本身使用事件驱动、非阻塞和异步输入输出模型等技术,非常方便, 我们也可以将其利用成为冬日绘版中的佼佼者
本篇将简单介绍Node.js的使用,完成冬日绘版的自动提交
您可以需要了解:
  • Javascript
  • Chrome开发者工具(元素审查大师

什么是Node.js

白话版:
Node.js 可以让JavaScript代码跑在服务器上
专业版:
Node.js 通过 v8 (Chrome内核), 实现高效率的Web服务器, 最后实现出了单线程/单进程系统

实现思路

利用 Node.js 事件驱动的优势, 根据每次事件间隔进行 post
主体代码如下:
JAVASCRIPT
const EventEmitter = require('events')

const poster = new EventEmitter()

poster.on('start', () => { 
  post('luogu.org/', {})
})

setInterval(() => poster.emit('start'), 30 * 1000)
我们设定30的间隔秒然后进行post
然后我们思考几个细节
  1. 如何处理post失败情况
忽略即可, 失败的原因可能是你post太频繁了, 但是这并不影响冷却. 或者是因为洛谷更新导致需要refer字段等各种玄学问题
  1. 如何把我们的图片转化成洛谷所提供的格式
我们先预处理出图片内容
  1. 如何防止重复提交一个点多次
我们事件中加入一个检查画板的事件, 他定时帮助我们检查哪些是不需要的任务
  1. 如何让我代码更具扩展性, 比如我立马需要添加其他小伙伴的Cookie/图片数据
我们使用模块化, 将多个部分拆分开来, 最后组成完整的 Poster

预处理

我们使用 Python3, 使用第三方库 Pillow, 读取图片每个像素的颜色, 和洛谷提供的数据进行一一比对, 然后选择最适合的值
伪代码如下:
PYTHON
def main():
    img = ReadImg(imgPath) # 读取图片
    height, width = img.size()
    data = []
    
    for i in range(0, height):
      for j in range(0, width):
        color = getLuoguColor(img[i][j].color)
        data.append([i, j, color])
        
    data.save()
注意, 最小颜色差值也并不是我们直观上的欧几里得距离
有几篇文章阐述了原理, 有关详细原理超出编者的知识水平, 我这里不过多阐述, 供上链接
我们直接使用Python自带库 colorsys
部分代码如下, 具体可以到我的项目中查看
PYTHON
map = {} # 这里是洛谷提供的颜色值
def get_color(pixel):
    return min_color_diff(pixel, colors)[1]
def to_hsv(color):
    return rgb_to_hsv(*[x / 255.0 for x in color])
def color_dist(c1, c2):
    return sum((a - b) ** 2 for a, b in zip(to_hsv(c1), to_hsv(c2)))
def min_color_diff(color_to_match, colors):
    return min(
        (color_dist(color_to_match, test), colors[test])
        for test in colors)

检查画板

洛谷给出了画板的链接 board
首先我们得知道的是哪个方向才是X/Y轴
多点几个点就能看出来了
然后我们发现里面有 a, b, c... 这样的内容, 洛谷为了方便起见10以上的分别用abc表示
我们将返回的字符串序列进行简单处理
JAVASCRIPT
function parseMap() {
  const _ = []
    map.trim().split('\n').forEach((v, k) => {
      _[k] = new Proxy({ value: v }, {
        get (target, p) {
          // 这里使用一个代理, 你可以理解为重载运算符, 将 map[x][y] 的操作进行转换
          // 避免 map[x][y] 返回字母, 而是要数字, 方便我们比对
          const val = target.value[p]
          return parseInt(val, 36) // 36进制足够用了, 省去手写转换的麻烦
        }
      })
    })
    return _
}

扩展性

我们通过继承 EventEmitter 实现其他功能
伪代码如下:
JAVASCRIPT
const EventEmitter = require('events')

class Poster extends EventEmitter {
  constructor (props) {
    super()
    this.tasks = loadTasks()
    this.users = loadUsers()
    this.map = getMap()

    this.registerEvent()
  }

  registerEvent () {
    this.on('checkMap', () => {
      this.tasks = reloadTask(this.tasks, this.map)
    })

    this.on('start', () => {
      for (const k in this.users) {
        const user = this.users[k]
        const cookie = user.cookie
        const data = this.tasks[0]
        this.tasks.shift()  // pop
        const [x, y, color] = data
        post('xxx', {
          x: x,
          y: y,
          color: color
        }, { cookie: cookie }).then(res => {
          if (res.data.status !== 200) {
            console.error('出错了!')
            this.tasks.push(data)  // 失败时候重新加入数组
            // other code
          } else {
            console.log('成功!')
            // other code
          }
        })
      }
    })
  }
}

const poster = new Poster()

setInterval(() => poster.emit('start'), 30 * 1000)
setInterval(() => poster.emit('checkMap'), 500 * 1000)  // 检查地图无需时间间隔太短

其他部分

  • 为了防止我们的服务器玄学崩溃, 我们需要再来一个进程来防止我们的进程崩溃
    为了防止那个进程也崩溃, 我们还需要一个进程来检测这个进程是否崩溃, 那么... (并不
    我们使用 pm2 进行检测
    直接运行
    pm2 start index.js # 你的脚本
    哪怕你的脚本一直 return 0 他也一直在重启 (逃
  • 古人有云, 动态类型一时爽, 重构代码火葬场
    我们使用 typescript 进行代码, typescript是一个编译时检查类型的语言, 然后转换成JavaScript代码
    这里我就不过多阐述了
  • 模块化可以继续扩展, 我们将一些重要的文件写进 config.json 中, 比如 port, dataPath 什么的
  • 写好了一个脚本, 我们如何知道他跑的如何呢? 我们需要单元测试, 正所谓如果所有过程正确就可以证明结论一点正确
    我们使用 jest 进行调试, 具体使用起来就是
    JAVASCRIPT
    // getTask 是一个 将你的 tasks 和 map 进行比对, 然后返回需要post的的函数
    it('should get task', function () {
      const res = getTask([
        [0, 0, 1],
        [0, 1, 1],
        [0, 2, 1]
      ], [
        '1122',
        '2222',
        '2222'
      ])
      expect(res.length).toEqual(1) // 我断定他只剩下一个任务
    })
    
    然后我们跑一下测试
    npm run jest
    运行正确

结束

详细的代码在 Github luogu-drawer
其中 Poster 在 poster.js

鸣谢

yyfcpp 给了我很多Cookie, 以便我不到半小时就画完了我的头像
abc1763613206 帮助我维护服务器上的 luogu-drawer

评论

49 条评论,欢迎与作者交流。

正在加载评论...