【Node.js】寫一個資料自動整理成表格的指令碼

凌覽發表於2023-01-14

前言

企業專案進行資料埋點後,埋點事件名需要整理成Excel表格便於統計,目標是將下圖左側資料轉化成下圖右側的Excel表格:

考慮到左側埋點資料是隨專案迭代增加的,埋點資料每增加一次我就要把資料一條一條的Ctrl+C/V複製貼上至Excel表格內。

懶,不想這樣玩,於是我寫了一個自動幫我整理成表格的指令碼。

指令碼實現

實現流程

  • Node.js生成Excel表格工具庫技術選型
  • 單獨複製一份埋點資料出來,保證它的變動不會影響業務相關埋點邏輯
  • 整理埋點資料成我們需要的資料結構

分成三步走

技術選型

Node.js操作Excel表格工具庫有:

僅羅列以上四個。

選擇的角度有以下幾點:

  • 學習成本低,文件API簡單易用,僅生成表格即可,其他功能並不需要,所以API越簡單越好
  • 生成Excel表格需要提供的資料結構簡單,便於實現
  • 能匯出xlsx表格,滿足最基本要求

node-xlsx最貼近以上要求,首選使用它。

node-xlsx官方生成Excel表格給出的程式碼塊:

import xlsx from 'node-xlsx';
// Or var xlsx = require('node-xlsx').default;

const data = [
  [1, 2, 3],
  [true, false, null, 'sheetjs'],
  ['foo', 'bar', new Date('2014-02-19T14:30Z'), '0.3'],
  ['baz', null, 'qux'],
];
var buffer = xlsx.build([{name: 'mySheetName', data: data}]); // Returns a buffer

生成表格資料data是二維陣列,對應表格的行列。data.length 為表格行數,data[0].length 為表格的列數;data[0][0]對應至表格的第一行第一列的值,data[0][1]對應至表格的第一行第二列的值。

所以將埋點資料整理為一個二維陣列即可,二維陣列資料結構整理容易實現。

複製埋點資料

埋點資料統一放置在buryData.js 檔案,但不能隨意改動它,所以將該檔案單獨再複製一份出來。

buryData.js

export default {
    version1: 'v1.5.3',
    bury1: 'ding提醒',
    bury2: '審批-篩選',
    bury3: '任務-點選任務標題開啟任務詳情',
    bury4: '任務詳情彈框-點選詳情tab',
    bury5: '任務詳情彈框-點選日誌記錄tab',
    bury6: '任務詳情彈框-點選工作總結tab',
    bury7: '任務詳情彈框-點選動態tab',
    //...
}

buryData.js複製出來檔案命名為bury.js,還有一個問題:bury.js需要執行它,拿到它匯出的資料物件,匯出資料是使用ES6模組化語法,這邊需要將ES6模組化轉化成CommonJs模組化,將export default {} 替換成module.exports ={}即可做到。

Node.js fs模組+正則替換是可以達成以上目的,但為了更快捷,我選擇使用工具庫magic-string

magic-string它是操作字串庫,它可以幫我去掉寫正則替換字串的步驟。

const path = require('path');
const magicString = require('magic-string')
const fs = require('fs');

//buryData.js 檔案路徑
const buryFile = path.join(__dirname, '../src/lib/buryData.js')

const getBuryContent = (filePath) => {
    const content = fs.readFileSync(filePath, 'utf8')
    //將export default 替換成module.exports =
    const s = new magicString(content)
    s.replace('export default', 'module.exports = ')
    return s.toString()
}

(async () => {
    const str = getBuryContent(buryFile)
    //將替換後的內容寫入至bury.js檔案
    const copyFilePath = path.join(__dirname, '/bury.js')
    fs.writeFileSync(copyFilePath, str)
    //動態匯入bury.js 獲取埋點資料
    const { default: data } = await import(copyFilePath)
})()

生成二維陣列

上文已提及,node-xlsx生成表格需要先將資料整理成二維陣列。

export default {
    version1: 'v1.5.3',
    bury1: 'ding提醒',
    /...
    version2: 'v1.5.4',
    bury21: '通訊錄人員列表',
    //..
}

以上資料整理成:

[
  ['v1.5.3','v1.5.4'],
  ['ding提醒','通訊錄人員列表'],
  //...
]

首先,將資料全部存放至一個Map物件中。因為埋點資料是一個物件,其中version1、version2 表示版本號,隨專案迭代版本號會增多version3、version4……以version進行劃分Map 值。

const _ = require('lodash');
//...

const getFormatDataMap = (data) => {
    let version
    const map = new Map();
    _.forIn(data, (value, key) => {
        if (key.includes('version')) {
            version = value
            !map.has(version) && map.set(version, [value])
            return
        }
        const mapValue = map.get(version)
        mapValue.push(value)
    })
    return map
}

(async () => {
    const str = getBuryContent(buryFile)
    const copyFilePath = path.join(__dirname, '/bury.js')
    fs.writeFileSync(copyFilePath, str)
    const { default: data } = await import(copyFilePath)
    
    //新增
    const map = getFormatDataMap(data)
})()

getFormatDataMap 函式執行後,返回的資料是:

{
'v1.5.3'=>['v1.5.3','ding提醒' //...]
'v1.5.4'=>['v1.5.4','通訊錄人員列表' //...]
}


然後,需要知道表格最大行數,表格列數即為map.size(),最大行數透過獲取Map.values()獲取所有的值values,遍歷values獲取values記憶體放的每一個陣列的長度,長度統一用另一個陣列lens臨時記錄,遍歷結束後比較lens中的數值得到最大的值,

MAX_LEN 即為表格最大的行數,也是values存放的所有陣列中長度最大的值。

const _ = require('lodash');
//...
const getMergeArr = (map) => {
    const values = _.toArray(map.values())
    const lens = []
    //獲取長度,長度值統一存放至lens陣列中
    values.forEach((value) => { lens.push(value.length) })
    //比較
    const MAX_LEN = _.max(lens)
    
    return getTargetItems({ mapValue: values, forNum: MAX_LEN })
}

(async () => {
    const str = getBuryContent(buryFile)
    const copyFilePath = path.join(__dirname, '/bury.js')
    fs.writeFileSync(copyFilePath, str)
    const { default: data } = await import(copyFilePath)
    const map = getFormatDataMap(data)
    //新增
    const table = getMergeArr(map)
})()

最後,以valuesMAX_LEN進行雙迴圈。表格列數map.size()可獲取,但為了方便直接mapValue.length,兩者是相等的。

有了表格列數即可建立二維陣列的第二層陣列,new Array(len).fill(' ')第二層陣列長度即為mapValue.length,建立時陣列內的值先統一填充為' '

const getTargetItems = ({ mapValue, forNum }) => {
    const len = mapValue.length
    const targetItems = []
    mapValue.forEach((v, i) => {
        for (let index = 0; index < forNum; index++) {
            const element = v[index];
            let targetItem = targetItems[index]
            if (!targetItem) {
            //建立陣列,值先統一填充為' '
                targetItem = new Array(len).fill(' ')
            }
            /**
            如果當前index大於陣列v的長度,這時獲取值v[index]為undefined。
            為undefined的話直接跳過,保持targetItem[i]為' '
            */
            targetItem[i] = element ? element : ' '
            targetItems[index] = targetItem
        }
    })
    return targetItems
}

完成二維陣列的轉化,資料結構為下圖:

生成表格

資料已完成,留下的就是寫入資料生成表格,直接複製node-xlsx演示的程式碼下來。

//...

(async () => {
    const str = getBuryContent(buryFile)
    const copyFilePath = path.join(__dirname, '/bury.js')
    fs.writeFileSync(copyFilePath, str)
    const { default: data } = await import(copyFilePath)
    const map = getFormatDataMap(data)
    const table = getMergeArr(map)

    //寫入資料,生成表格,返回buffer資料
    const buffer = xlsx.build([{ name: '埋點', data: table }])
    
    const outPath = path.join(__dirname, '/bury.xlsx')
    
    //bury.js檔案可以刪除,bury.xlsx如果已存在就先刪了
    fs.existsSync(outPath) && fs.unlinkSync(outPath)
    fs.existsSync(copyFilePath) && fs.unlinkSync(copyFilePath)
    
    //建立一個bury.xlsx檔案,將得到的buffer寫入
    fs.writeFileSync(outPath, buffer)
})()

指令碼完。

完整原始碼:

const path = require('path');
const fs = require('fs');
const xlsx = require('node-xlsx');
const magicString = require('magic-string')
const _ = require('lodash');
const buryFile = path.join(__dirname, '../src/lib/buryData.js')
const getBuryContent = (filePath) => {
    const content = fs.readFileSync(filePath, 'utf8')
    const s = new magicString(content)
    s.replace('export default', 'module.exports = ')
    return s.toString()
}
const getFormatDataMap = (data) => {
    let version
    const map = new Map();
    _.forIn(data, (value, key) => {
        if (key.includes('version')) {
            version = value
            !map.has(version) && map.set(version, [value])
            return
        }
        const mapValue = map.get(version)
        mapValue.push(value)
    })
    return map
}
const getTargetItems = ({ mapValue, forNum }) => {
    const len = mapValue.length
    const targetItems = []
    mapValue.forEach((v, i) => {
        for (let index = 0; index < forNum; index++) {
            const element = v[index];
            let targetItem = targetItems[index]
            if (!targetItem) {
                targetItem = new Array(len).fill(' ')
            }
            targetItem[i] = element ? element : ' '
            targetItems[index] = targetItem
        }
    })
    return targetItems
}
const getMergeArr = (map) => {
    const values = _.toArray(map.values())
    const lens = []
    values.forEach((value) => { lens.push(value.length) })
    const MAX_LEN = _.max(lens)
    return getTargetItems({ mapValue: values, forNum: MAX_LEN })
}
(async () => {
    const str = getBuryContent(buryFile)
    const copyFilePath = path.join(__dirname, '/bury.js')
    fs.writeFileSync(copyFilePath, str)
    const { default: data } = await import(copyFilePath)
    const map = getFormatDataMap(data)
    const table = getMergeArr(map)
    debugger
    const buffer = xlsx.build([{ name: '埋點', data: table }])
    const outPath = path.join(__dirname, '/bury.xlsx')
    fs.existsSync(outPath) && fs.unlinkSync(outPath)
    fs.existsSync(copyFilePath) && fs.unlinkSync(copyFilePath)
    fs.writeFileSync(outPath, buffer)
})()

去掉空行,一百行以內。

總結

Node.js可使用的場景非賞多,不單單是用於伺服器介面的開發,我們還能透過寫指令碼的形式解決生活中重複性的工作,憑藉js的語法簡單及強大的生態,前端不必學習shell、python等,僅使用js就可以搞定爬蟲、自動化指令碼等場景。

如果我的文章對你有幫助,你的?就是對我的最大支援^_^。

相關文章