[node 工具] 用Node.js 将bugzilla上的bug列表导入到excel表格里
公司用bugzilla管理产品bug,之前用Node.js做了个东西,方便能够把bug的相关信息导入到excel表格里,好做后续的管理。
本来很早是写过一篇的,但只是添加代码和注释了事。但是最近觉得是得好好整理一些东西的,所以又改了下博客,心中也有一些以后博文的规范。所以把之前的删掉重新写。
效果是这样的,比如这个链接( https://bugzilla.mozilla.org/buglist.cgi?order=Importance&resolution=---&query_format=advanced&product=Add-on%20SDK )里的bug,把它们的详细信息导入到excel表格里
下面就是撸代码了。只是一个单文件的小工具,尽量把所有代码都贴上。安装node和写package.json那种事情就不说了。
1 var request = require("request") //发送http请求 2 var cheerio = require("cheerio"); //解析html页面 3 var Excel = require('exceljs'); //读写excel 4 var colors = require("colors"); //串口颜色显示 5 var program = require("commander"); //串口命令行解析 6 var readlineSync = require('readline-sync'); //命令行交互 7 var Agent = require('agentkeepalive'); //http地址池,keep-alive长连接使用 8 var ProgressBar = require('progress'); //显示进度条 9 var fs = require('fs');
项目需要这些模块
1 var putError = console.log; 2 global.console.error = function(error) { 3 putError(colors.red("[Error]: " + error)); 4 }
只是为了打印错误信息用红色字体比较显眼一点而已
1 program.on('--help', function() { 2 console.log(' \nExamples:\n'); 3 console.log(' node app.js -u "http://..." -s Name'); 4 }) 5 6 program.option('-u, --url <url>', 'Url of bug list to generate.') 7 .option('-s, --specifyName ', 'Specify string of file name.') 8 .parse(process.argv);
解析命令行,parse需要放在最后。commander模块自己会有-h参数打印帮助信息,觉得加个例子会比较清楚。并且也是为了提醒 -u 参数的值添加双引号。
1 var fileName = "BugList-" + (new Date()).toLocaleDateString() + (program.specifyName? "-"+program.specifyName : "");
将会保存的 excel 的名字
1 var url = ""; 2 if (!program.url) { 3 var count = 0; 4 while (url == "") { 5 if (++count > 3) { 6 program.outputHelp(); 7 process.exit(1); 8 } 9 url = readlineSync.question('Please input the url of bug list: ').trim().replace(/^"(.*)"$/g, "$1"); 10 } 11 } 12 else { 13 url = program.url; 14 }
因为 url 参数是必需的,所以如果使用者没有输入的话,那么就用 readlineSync 模块询问使用者。readlineSync 模块会阻塞流程,一直等到用户输入。如果三次还是没有输入的话,调用 program.outputHelp() 函数会打印帮助信息,效果就跟 node app -h 一样
1 url = decodeURIComponent(url); 2 url = encodeURI(url); 3 //url地址的转换,比如从浏览器直接复制带中文地址会出错 4 var urlIndex = url.indexOf("/bugzilla3/"); 5 if (urlIndex != -1) { 6 var root = url.slice(0, urlIndex + 11); //公司bugzilla放在这个目录下,额外做个判断 7 } 8 else { 9 var root = url.replace(/^((https?:\/\/)?[^\/]*\/).*/ig, "$1"); //取域名 10 root = (/^https?:\/\//ig.test(root) ? root : "http://" + root); //如果没有http://,就加上它 11 } 12 var bugUrl = root + "show_bug.cgi?ctype=xml&id=";
这里是处理 url 地址以及设置每个bug的的 url 地址,后面通过用户输入的链接,将bug id取出来,分别添加到 bugUrl 的 id 之后,就构成了每个特定 bug 的地址
对于用户输入的处理到这里就结束了,接下来要根据用户输入的 url 地址,去取每个 bug 的信息
1 function getFunc(getOption, parseFunc) { 2 return new Promise(function(resolve, reject) { 3 request.get(getOption, function(error, response, body) { 4 if(!error && response.statusCode == 200) { 5 var $ = cheerio.load(body); 6 var result = parseFunc(getOption.url, $); 7 resolve(result); 8 } 9 else { 10 reject(error); 11 } 12 }) 13 }) 14 }
先写一个公用的 get 函数,用来发送 get 请求到参数地址,成功之后用 cheerio 解析,然后调用回掉函数,将解析结果传给回掉函数。因为发送请求是异步的,所以这个 get 函数用 Promise 包装,返回一个 Promise 对象。在调用回掉函数之后调用 resolve() 就是一次请求结束。
1 Agent = (root.toLowerCase().indexOf("https://") != -1)? Agent.HttpsAgent: Agent; //https就要用支持https的地址池,因为bugzilla用的长连接,不用地址池会出错 2 var keepaliveAgent = new Agent({ 3 maxSockets: 100, 4 maxFreeSockets: 10, 5 timeout: 60000, 6 freeSocketKeepAliveTimeout: 30000 7 });//地址池配置 8 9 var option = { 10 agent: keepaliveAgent, 11 headers: {"User-Agent": "NodeJS", Host: url.replace(/^((https?:\/\/)?([^\/]*)\/).*/g, "$3")}, 12 url: url 13 };//get 请求的配置
因为 bugzilla 使用的是长连接,因此使用地址池来发送请求。根据服务器情况自己配置参数。
1 getFunc(option, function (url, $) { 2 var bugs = new Array(); 3 var td = $("table.bz_buglist tr td.bz_id_column a"); 4 td.each(function (key) { 5 bugs.push(td.eq(key).text()); 6 }) 7 if (bugs.length > 0) { //获取bug的ID 列表并初始化进度条 8 console.log(""); 9 global.bar = new ProgressBar('Getting Bugs [:bar] :percent | ETA: :etas | :current/:total', { 10 complete: "-", 11 incomplete: " ", 12 width: 25, 13 clear: false, 14 total: bugs.length, 15 }); 16 } 17 else { 18 console.error("No bugs can be found."); 19 process.exit(1); 20 } 21 return bugs; //bugs 这个值通过 getFunc 里的 resolve 函数传递给 then 里面的函数 22 })
开始解析首页获得每个 bug 的 id
1 .then(function (bugs) { 2 var done = 0; 3 //用map对ID数组做每个bug 信息的取回,每个请求返回的都是一个promise对象,这些promise对象组成map返回数组的项当作Promise.all的参数,当里面所有的promise对象都成功之后,Promise.all返回的promise对象就算是都resolve了 4 return Promise.all(bugs.map(function (eachBug, index) { 5 option.url = bugUrl + eachBug; 6 var promiseGetOne = getFunc(option, function (url, $) { 7 var oneInfo = new Object(); //用cheerio取需要的信息 8 oneInfo.url = url.replace(/ctype=xml&/ig, ""); 9 oneInfo.id = $("bug_id").text(); 10 oneInfo.summary = $("short_desc").text(); 11 oneInfo.reporter = $("reporter").text(); 12 oneInfo.product = $("product").text(); 13 oneInfo.component = $("component").text(); 14 oneInfo.version = $("version").text(); 15 oneInfo.status = $("bug_status").text(); 16 oneInfo.priority = $("priority").text(); 17 oneInfo.security = $("bug_security").text(); 18 oneInfo.assign = $("assigned_to").text(); 19 oneInfo.comment = new Array(); 20 var comments = $("long_desc"); //第一条评论当作bug描述 21 comments.each(function (key) { 22 var who = comments.eq(key).find("who").text(); 23 var when = comments.eq(key).find("bug_when").text(); 24 when = when.replace(/([^\s]+)\s.*$/g, "$1"); 25 var desc = comments.eq(key).find("thetext").text(); 26 if (key == 0 && who == oneInfo.reporter) { 27 oneInfo.detail = desc; 28 return true; 29 } 30 oneInfo.comment.push({ 'who': who, 'when': when, 'desc': desc }); 31 }) 32 33 return oneInfo; 34 }) 35 36 promiseGetOne.then(function () { 37 done++; 38 bar.tick(); //更新进度条 39 if (done == bugs.length) { 40 console.log("\n"); 41 } 42 }) 43 44 return promiseGetOne; 45 })) 46 })
在获得所有 bug id 之后,用 id 组成特定 bug 的 url 地址,分别去取得每个 bug 信息。用 cheerio 解析里面的 DOM 节点,取出需要的信息,用 resolve 函数返回。整个函数的返回值将是由每个 bug 的 resovle 返回值组成的数组
1 .then(function (bugLists) { 2 var workbook = new Excel.Workbook(); //新建excel文档 3 var productNum = 0; 4 5 for (var i in bugLists) { 6 bugInfo = bugLists[i]; 7 8 var sheet = workbook.getWorksheet(bugInfo.product); //根据项目,如果没有工作表,就新建一个 9 if (sheet === undefined) { 10 sheet = workbook.addWorksheet(bugInfo.product); 11 productNum++; 12 } 13 14 try { 15 sheet.getColumn("id"); //如果没有标题行,就添加标题行 16 } 17 catch (error) { 18 sheet.columns = [ 19 { header: 'Bug ID', key: 'id' }, 20 { header: 'Summary', key: 'summary', width: 35 }, 21 { header: 'Bug Detail', key: 'detail', width: 75 }, 22 { header: 'Priority', key: 'priority', width: 8 }, 23 { header: 'Version', key: 'version', width: 15 }, 24 { header: 'Status', key: 'status', width: 15 }, 25 { header: 'Component', key: 'component', width: 15 }, 26 { header: 'Comments', key: 'comment', width: 60 }, 27 { header: 'Assign To', key: 'assign', width: 20 }, 28 { header: 'Reporter', key: 'reporter', width: 20 }, 29 ]; 30 } 31 32 var comment = ""; 33 for (var j in bugInfo.comment) { 34 comment += bugInfo.comment[j].who + " (" + bugInfo.comment[j].when + " ):\r\n"; 35 comment += bugInfo.comment[j].desc.replace(/\n/gm, "\r\n") + "\r\n"; 36 comment += "-------------------------------------------------------\r\n" 37 } 38 sheet.addRow({ //每个bug添加一行 39 id: { text: bugInfo.id, hyperlink: bugInfo.url }, 40 summary: bugInfo.summary, 41 detail: bugInfo.detail ? bugInfo.detail.replace(/\n/gm, "\r\n") : "", 42 priority: bugInfo.priority, 43 version: bugInfo.version, 44 status: bugInfo.status, 45 component: bugInfo.component, 46 comment: comment, 47 assign: bugInfo.assign, 48 reporter: bugInfo.reporter, 49 }); 50 51 sheet.eachRow(function (Row, rowNum) { //设置对齐方式等 52 Row.eachCell(function (Cell, cellNum) { 53 if (rowNum == 1) 54 Cell.alignment = { vertical: 'middle', horizontal: 'center', size: 25, wrapText: true } 55 else 56 Cell.alignment = { vertical: 'top', horizontal: 'left', wrapText: true } 57 }) 58 }) 59 } 60 61 fileName = ((productNum > 1) ? "" : bugInfo.product + "-") + fileName + ".xlsx"; 62 var files = fs.readdirSync("./"); 63 var postfix = 1; 64 while (files.indexOf(fileName) != -1) { //如果文件重名,就在后面添加(1)等数字,直至没有重名,否则会直接覆盖掉重名文件 65 fileName = fileName.replace(/(\(\d+\))?\.xlsx$/g, "(" + (postfix++) + ").xlsx"); 66 if (postfix > 99) { 67 console.warn("It may occur somethins wrong."); 68 break; 69 } 70 } 71 72 return workbook.xlsx.writeFile(fileName); 73 })
在上面一步已经将所有信息都取到了,因此这里就是把这些信息写入到 excel 表格里。
1 .then(function() { 2 console.log("Generate xlsx file successfully. File name is " + colors.cyan(fileName)); //结束,告诉使用者生成的文件名 3 }).catch(function(err) { 4 console.error(err); 5 process.exit(1); 6 })
最后一个 catch 不仅能够捕获写入 excel 表格的错误信息,也能捕获从一开始请求所有 id 和取得每个 bug 信息的错误。
生成出来的 excel 表格就是下面这样的。其实这个工具我迭代更新了几次了,后来索性将它做成了网页版,现在不就是什么都讲究云嘛。但是这是很早的一篇,以后再另写一篇改进了什么,就能有一点软件迭代开发的样子了。