[Node.js] 練習題:將部落格文章依人氣數排序
最近 PT 問我能不能將網誌文章,依人氣數排序列出來,
這樣如果有破圖的話,可以優先對高人氣的文章處理~
正好最近 Node.js 學到 使用 http 模組取得網頁內容,就順便拿來練習吧~
要取得某個痞客邦網誌的所有文章,
可以從 http://<name>.pixnet.net/blog/listall/<page> 得到~
這邊的 <page> 是從 1 開始的數字,可是要到多少呢?
觀察一下第一頁,它會有個指向下一頁的 next 連結,
而最後一頁就沒有 next 了,因此可以用這個來做為要不要取得下一頁的依據~
因此,我們可以先寫一個簡單的 get_page() 函式,用來取得某一頁的 HTML 資料~
這邊我們會先在 response 物件上註冊 “data” callback,收集伺服器回傳的片段資料,
而另外註冊的 “end” callback 則可以讓我們在收集到這個頁面完整的 HTML 後,
開始作處理,這邊的處理是呼叫另一個函式 handle_response():
// Load Node.js modules var fs = require("fs"); var http = require("http"); // Global variables var g_blog_name = "ephrain"; var g_base_url = "http://" + g_blog_name + ".pixnet.net/blog/listall/"; var g_all_articles = []; function get_page(page) { // Fetch URL var target_url = g_base_url + page; console.log("Fetching " + target_url + " ..."); http.get(target_url, function(response) { var full_response = ""; // Collect partial response response.on("data", function(data) { full_response += data.toString(); }); // All response from this URL is collected. // Start analysis. response.on("end", function() { handle_response(page, full_response); }); }).on("error", function(error) { console.log("Error fetching " + target_url + ": " + error); }); }
handle_response() 函式裡,
首先是呼叫 parse_articles() 來分析 HTML 中的文章,這個待會再提~
接下來就是檢查是不是有下一頁 next 的連結,有的話就繼續拜訪下一頁,
沒有的話,就可以排序所有的文章,顯示結果了:
function handle_response(page, full_response) { // Parse articles from the HTML response parse_articles(full_response); // Check if there is next page if (full_response.indexOf('class="next"') >= 0) { // Visit next page get_page(page+1); } else { // This is the final page. // Sort all articles sort_articles(); // Generate sorted html generate_html_output(); } }
parse_articles() 這個函式裡,主要就是要取出 HTML 裡面 <table> 到 </table> 內的資料,
而表格裡的每一個 <tr>…</tr> 都代表了一篇文章,
因此用 match() 加上 regular expression 來比對比較方便~
這邊的 regular expression 在使用時學到了幾個新的用法:
– 要 match 任意字元跨越多行的狀況時,可以用 [\s\S]*。
\s 代表空白類的字元,\S 是非空白類的字元,合起來就是所有的字元了。
這邊不能用 “.” 代表任意字元,因為 “.” 是不包含換行字元的~
(參考資料:stackoverflow: How to use JavaScript regex over multiple lines?)
– 要 match 特殊字元時,用 \xnn 的語法。
這邊的 nn 代表那個字元的十六進位碼,像我想找 / 這個字元的話,
跟 regular expression 的 / 衝到了,因此得用 \x2F 來代表 /~
(參考資料:w3schools: JavaScript RegExp Reference)
– 要做 non-greedy 的 match 時,加上 ? 字元。
預設狀態下,JavaScript 會做 greedy match,因此 <tr>[\s\S]+\x2Ftr> 這個寫法,
可能會把第一個 <tr> 和最後一個 </tr> 中間的東西,都算在一次 match 上,
因此要用 non-greedy match,變成 <tr>[\s\S]+?\x2Ftr>,
讓它找到最近的一個 </tr> 就停止~
當成功抓出文章的資訊時,就把它加到 g_all_articles 這個 global array 去:
function parse_articles(full_response) { var i = 0; var matched_trs, matched_tokens; // Get articles inside the table matched_trs = full_response.match(/<table>[\s\S]*<\x2Ftable/); if (matched_trs) { matched_trs = matched_trs[0].match(/<tr>[\s\S]+?\x2Ftr>/g); if (matched_trs) { for (i = 1; i < matched_trs.length; i++) { matched_tokens = matched_trs[i].match(/<tr>[\s\S]+?href="(.+?)">(.+?)<\x2Fa>[\s\S]+?article_hit[^>]+?>(\d+)[\s\S]+?<\x2Ftr>/); if (matched_tokens) { g_all_articles.push({ url: matched_tokens[1], title: matched_tokens[2], visit_count: parseInt(matched_tokens[3]) }); } } } } }
當所有的頁面都處理完畢後,在 g_all_articles 裡面就會有所有的文章,
下一步的 sort_articles() 就是依照人氣數來排序,
這動作很簡單,呼叫 sort() 並給它一個比較用的函式就可以了~
因為我們想要高人氣的文章排在前面,因此比較的時候,是用 b-a 而不是 a-b 的方式:
(參考資料:w3schools: JavaScript Array sort() Method)
function sort_articles() { // Sort articles by visit count g_all_articles = g_all_articles.sort(function(a, b) { return (b.visit_count-a.visit_count); }); }
最後,就是要呼叫 generate_html_output() 將排序後的結果輸出到檔案~
這邊因為我們需要寫出多篇文章的資料,因此不適合呼叫 fs.write(),
因為如果多次呼叫 fs.write() 的話,
並沒有辦法保證比較早呼叫的 fs.write() 一定會比較早執行,
這樣資料就會亂掉了…
根據 Node.js: fs.write() 這邊的建議,是用 fs.createWriteStream() 來作:
function generate_html_output() { var ws = fs.createWriteStream("result.html"); var encoding = "utf-8"; ws.write('<!DOCTYPE html>\n' + '<meta charset="utf-8">\n' + '<html>\n' + '<body>\n' + '<table>\n' + '<tr><td>Count</td><td>Title</td></tr>\n', encoding); for (var i = 0; i < g_all_articles.length; i++) { var splited = g_all_articles[i].url.split("/"); ws.write('<tr>' + '<td>' + g_all_articles[i].visit_count + '</td>' + '<td><a href="' + g_all_articles[i].url + '">' + g_all_articles[i].title + "</a></td>" + '</tr>\n', encoding); } ws.write('</body>\n' + '</html>\n', encoding);
}
}
最後只要記得呼叫 get_page(1) 開始從第一頁讀取,
程式就會把所有的文章都讀取並排序囉~
完成後把 result.html 用瀏覽器打開,就能看到成果啦:
初學 Node.js 的應用,算是成功囉~^^/