[node 工具] 用Node.js 將bugzilla上的bug列表導入到excel表格裏

公司用bugzilla管理產品bug,以前用Node.js作了個東西,方便可以把bug的相關信息導入到excel表格裏,好作後續的管理。html

原本很早是寫過一篇的,但只是添加代碼和註釋了事。可是最近以爲是得好好整理一些東西的,因此又改了下博客,心中也有一些之後博文的規範。因此把以前的刪掉從新寫。node

 

效果是這樣的,好比這個連接( https://bugzilla.mozilla.org/buglist.cgi?order=Importance&resolution=---&query_format=advanced&product=Add-on%20SDK )裏的bug,把它們的詳細信息導入到excel表格裏json

 

下面就是擼代碼了。只是一個單文件的小工具,儘可能把全部代碼都貼上。安裝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');

項目須要這些模塊promise

 

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 的名字app

 

 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 表格就是下面這樣的。其實這個工具我迭代更新了幾回了,後來索性將它作成了網頁版,如今不就是什麼都講究雲嘛。可是這是很早的一篇,之後再另寫一篇改進了什麼,就能有一點軟件迭代開發的樣子了。

相關文章
相關標籤/搜索