走进 Node.js 后端:随机生成文章

469 阅读3分钟

前言

想要成为一个优秀的全栈工程师,就必须得前后端都比较精通才行,通过一个实战项目加深node.js的后端开发的了解。这个项目可以在语料库的基础上随机生成一篇文章。看看这个项目是怎么实现的吧。

项目实现

准备操作

  1. 创建一个后端项目

    npm init -y
    
  2. 在执行这段指令后会创建一个package.json配置文件,在package.json配置文件内添加"type": "module",将node的模块化从CommonJS改成ES Module。

    在node里存在两种模块化方式,分别是CommonJS和ES Module。CommonJS是node为JavaScript打造的模块化机制,那时JavaScript并没有模块化能力,然而在JavaScript的后续更新中添加了模块化机制,也就是ES Module,因此node引入了ES Module。

  3. 创建一个corpus文件夹用于存放数据,在里面创建一个data.json文件作为语料库。里面的数据是6个数组,数组里面的元素都是一个句子,如:"爱迪生{{said}},天才是百分之一的勤奋加百分之九十九的汗水。{{conclude}}"

  4. 创建一个lib文件夹存放模块。

  5. 创建一个output文件用于存放输出生成的文档。

  6. 创建一个index.js文件。

Snipaste_2024-07-04_22-00-58.jpg

## 创建模块

随机生成数字

首先定义一个可以输出给定范围随机数的函数。

export function randomInt(min, max) {
    const n = Math.random()
    return Math.floor(min * (1 - n) + max * n)
}

可以生成minmax范围内的随机数。

随机选择一个句子

在传输过来给的数组中靠randomInt函数随机生成一个下标,实现随机选择一句话。但是,因为是随机选择的句子,可能会出现前后生成的句子是一样的,也就是随机生成的下标是一样的。为了避免这样的事情发生,我们想要定义一个randomPick函数。

方法一:先用全局变量保存上一次随机选择的句子,然后再用do while循环进行判断,如果和上次选择的不一样则让全局变量记录随机选择的句子,如果和上次选择的一样则循环继续,再随机选择一个句子,直到和生成选择的不一样为止。

let lastPiced = null
export function randomPick(arr) {
    let picked = null
    do {
        const index = randomInt(0, arr.length)
        picked = arr[index]
    } while (picked === lastPiced)
    lastPiced = picked
    return picked
}

这样的方法不太优雅,因为设置了一个全局变量,全局变量会污染全局,需要用别的方法实现。

方法二:

  1. 首先在数组的下标为0到arr.length - 2随机选择一个句子,然后将选中的句子和数组最后一个句子进行交换,下一次选择就不会选择到数组的最后一个句子,这样就可以避免随机选择到一样的句子。

    function randomPick(arr) {
        const len = arr.length - 1
        const index = randomInt(0, len)
        [arr[index], arr[len]] = [arr[len], arr[index]]
        return arr[index]
    }
    
  2. 利用闭包,即使 createRsndomPicker 函数执行结束,randomPick 函数仍然能够保留对 arr 的访问权限,并根据其进行后续的随机选择操作。

    export function createRsndomPicker(arr) {
        arr = [...arr]
        function randomPick() {
            const len = arr.length - 1
            const index = randomInt(0, len);
            [arr[index], arr[len]] = [arr[len], arr[index]]
            return arr[index]
        }
        randomPick()
        return randomPick
    }
    

整合成文章

引入上面两个模块。通过上面的模块,我们获取到随机的句子,但是句子并不是可以直接用的,如:"爱迪生{{said}},天才是百分之一的勤奋加百分之九十九的汗水。{{conclude}}",这样的句子需要用随机选择到的内容进行替换。所以需要封装成一个函数sentence

function sentence(pick, replacer) {
    let ret = pick()
    for (const key in replacer) {
        ret = ret.replace(
            new RegExp(`{{${key}}}`, 'g'),
            typeof replacer[key] === 'function' ? replacer[key]() : replacer[key]
        )
    }
    return ret
}

其中pick就是通过createRsndomPicker函数返回的闭包函数,需要调用才能返回一个句子。replacer是一个对象,其键作为模板字符串中的占位符,值为用于替换的字符串或函数。

然后使用for...in循环遍历replacer对象的所有键,对replacer中的每个键创建一个正则表达式,用于在ret中查找匹配的占位符。每找到一个占位符,就使用replace()方法进行替换,如果replacer[key]是一个函数,就调用该函数用于获取实际要插入的字符串,否则直接使用replacer[key]的值进行替换。

在定义了替换文本的函数后,可以开始实现文本的整合了。

export function generate(title, { corpus, min = 500, max = 800 }) {
    const articleLenth = randomInt(min, max)
    const { famous, bosh_before, bosh, conclude, said } = corpus
    const [PickFamous, PickBosh_before, PickBosh, PickConclude, PickSaid] = [famous, bosh_before, bosh, conclude, said].map(createRsndomPicker)

    const article = []
    let totalLength = 0
    while (totalLength < articleLenth) {//生成文章
        let section = ''
        const sectionLength = randomInt(100, 300)
        while (section.length < sectionLength) {//生成段落
            const n = randomInt(0, 100)//代表百分比
            if (n < 20) {
                section += sentence(PickFamous, {
                    said: PickSaid,
                    conclude: PickConclude,
                })
            } else if (n < 50) {
                section += sentence(PickBosh_before, { title }) + sentence(PickBosh, { title })
            } else {
                section += sentence(PickBosh, { title })
            }
        }
        totalLength += section.length
        article.push(section)
    }
    return article
}

title是随机生成的标题,{ corpus, min = 500, max = 800 }是一个对象,里面包括语料库和文章的默认长度范围。

  1. 在文章的长度范围内随机生成一个文章的长度articleLenth
  2. 用对象的解构获取语料库里的不同数组然后组成一个大数组再使用map遍历并且以createRsndomPicker函数作为回调函数,最终返回的结果再用数组的解构分别获取,这样就获取到了随机选择句子的函数数组了。
  3. 初始化文章的总长度和文章数组。在文章的总长度小于随机生成的文章长度使持续生成段落。
    1. 初始化一个段落,并且随机生成段落的长度,在段落总长度小于随机生成的段落长度时,生成一个随机数用于随机绝对段落的组成,然后将sentence替换的句子拼接起来整合成一段话。重复拼接段落,直到段落总长度不小于随机生成的段落长度时结束。
  4. 段落生成完后添加到文章数组里,并且将段落总长度增加到文章的总长度上。直到文章的总长度不小于随机生成的文章长度才结束。
  5. 最终返回一个文章数组,数组的每一个元素代表一段文字。

index.js

引入模块

import fs from 'fs';
import { fileURLToPath } from 'url'
import { dirname, resolve } from 'path'
import { generate } from './lib/generator.js'
import { createRsndomPicker } from './lib/random.js'

其中,fs 模块用于文件系统的操作;fileURLToPath 用于将文件的 URL 转换为路径;dirnameresolve 用于处理文件和目录的路径;其余的是生成文字的模块和随机选择句子的模块。

封装一个loadCorpus函数用于加载指定路径的语料库数据。

function loadCorpus(src) {
    const url = import.meta.url;//读取当前脚本的绝对路径
    const path = resolve(dirname(fileURLToPath(url)), src)
    const data = fs.readFileSync(path, { encoding: 'utf-8' })
    return JSON.parse(data)
}

首先读取当前脚本的绝对路径,然后解析出当前脚本所在的文件夹并基于此文件夹定位到指定文件的位置。文件内容以UTF-8编码同步读取,并且将文件内容解析成JavaScript对象返回。

const corpus = loadCorpus('corpus/data.json')
const pickTitle = createRsndomPicker(corpus.title)
const title = pickTitle()
const article = generate(title, { corpus })
fs.writeFileSync(`./output/${title}.md`, article.join('\n'))

获取到语料库文件内容后在随机选择一个标题,通过语料库和标题生成文章,最后将文章文件写入到output文件夹内。

效果展示

动画12.gif