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

2018-06-21 16:55:49


F45cCD.md.jpg

前言

Node.js 本身使用事件驱动、非阻塞和异步输入输出模型等技术,非常方便, 我们也可以将其利用成为冬日绘版中的佼佼者

本篇将简单介绍Node.js的使用,完成冬日绘版的自动提交

您可以需要了解:

  • Javascript

  • Chrome开发者工具(元素审查大师

什么是Node.js

白话版:

Node.js 可以让JavaScript代码跑在服务器上

专业版:

Node.js 通过 v8 (Chrome内核), 实现高效率的Web服务器, 最后实现出了单线程/单进程系统

实现思路

利用 Node.js 事件驱动的优势, 根据每次事件间隔进行 post

主体代码如下:

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, 读取图片每个像素的颜色, 和洛谷提供的数据进行一一比对, 然后选择最适合的值

伪代码如下:

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()

注意, 最小颜色差值也并不是我们直观上的欧几里得距离

有几篇文章阐述了原理, 有关详细原理超出编者的知识水平, 我这里不过多阐述, 供上链接

COLOR SPACE CONVERSION

Colour metric

我们直接使用Python自带库 colorsys

部分代码如下, 具体可以到我的项目中查看

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表示

我们将返回的字符串序列进行简单处理

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 实现其他功能

伪代码如下:

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 进行调试, 具体使用起来就是

    // 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