黑米外掛:文章排行榜
2007 年 六月 27 日 (星期三) 10:04 pm分類:電腦
標籤: hemidemi, yahoo_pipes, ajax, blog, wordpress
楔子
對同一個網站,各人有各自的使用方式,也各有不同的預期。某些服務,原本應該是由網站主動提供的,但受限於目標、願景、人力、能力、優先順序等考量,官方可能不會提供;既然官方沒提供,改由熱血用戶實作出來,雖然很符合 Web 2.0 的 DIY 精神,但畢竟是繞了一圈,東拼西湊,總是沒那麼方便,也可能會影響官方網站的效能。像上次的 Qing 事件,大和解咖啡喝完之後,葛力的官方態度是:
歡迎其他使用者利用黑米提供的 RSS 或 API 開發對黑米長期發展有幫助的工具。需要的時候我們也會提供所需的 RSS/API。
我期待黑米官方能主動釋出更多有用的 API。在這日子來到之前,身為用戶的我們,還是得自力救濟。
像我最近看到一個黑米外掛〈我在黑米熱門書籤排行榜自動產生器 + Flash 搜尋器〉,目的是「把自己在黑米上的文章,依推收數的多寡,做一個排行榜」,再把結果秀在自己的部落格裡。這玩意兒很有意思,也是書籤網站原本應該主動提供的服務。這位 Billy Pan 還真是佛心來著。
不過我對它還有些不滿意。
首先是作者所說的第一步驟,它的原理是「利用黑米的搜尋功能(找自己的名字)」。這種方法很不精準,會有 false positive 及 false negative 問題:
- 當你的名字湊巧出現在完全不相干的書籤裡,就會出現 false positive。
- 當你的文章被別人以不涉及你的名字的方式收錄,就會出現 false negative。
萬一網站的全文檢索做得太差,效能更可能會被拖垮。除非你故意養一個專門自貼自推、不做其他正事的黑米帳號,就可以完全不靠全文檢索機制取得完整列表;但這種行為既不足取,又太費事 ── 如果有 N 家重量級的書籤網站,難不成你要一一去註冊分身帳號?
其次是這個程式利用 Dapper 的 screen scraping 功能,而不是透過正常的 API 管道。雖然 Dapper 也是很棒的網路服務,但這種做法畢竟不是王道,只能做為萬不得已的最後手段。
第三個缺點是如作者所說「不放在側邊列的原因是,太慢了,要來回跑 3 個網頁產生資料。」
第四個缺點是……我不喜歡 Flash。
基於以上的理由,我決定自己動手打造另一個版本,順便做為練功題。口說無憑,先用一段 demo 來展示我想做出來的效果吧。
目標
照例,在捲起袖子動手之前,先設下幾個目標:
- 盡可能使用黑米官方對外公開的 API。既然是 API,內部應該早就經過千錘百鍊的最佳化處理,效能不成問題,可放心使用,不會有「幫學弟測流量」的嫌疑。
- 盡可能降低自己主機的負載。我的電腦並不高檔,還有別的要事在排班,禁不起像 .CK 那樣的流量破表事情發生。最好是善用第三方的網路服務(像 Yahoo Pipes)、瀏覽器的 Ajax 能力,不要什麼事都攬在自己身上。
- 盡可能讓系統容易轉移到其他架站軟體或 BSP。
原理
黑米官方沒有提供「輸入:base URL,輸出:在黑米的排行榜」或「輸入:一堆網址,輸出:在黑米的排行榜」的 API,也沒有提供「輸入:一個網址,輸出:在黑米的收藏推薦數目」API(儘管 270 天以前我就在黑米提議過,但顯然只有我有這個需求而已,留言既沒被推也沒被採納),只有提供「輸入:一個網址,輸出:在黑米的收藏推薦名單」API。我們就以此為出發點。
「在黑米的收藏推薦名單」是個以 MD5 為基礎的 list/probe RSS API。譬如說,如果想知道〈xmliconv:解決 Yahoo Pipes 中文編碼問題〉這篇文章在黑米的收錄狀況,可依以下方式探測出:
- 此文章的網址是
http://william.cswiz.org/blog/archives/2007-06-13/xmliconv/ - 算出此網址的 MD5:
4b2555f1e3f86dd80de059b5d241863e - 套入黑米提供的「收藏推薦名單」RSS feed:
http://www.hemidemi.com/rss/bookmark/ (續)
4b2555f1e3f86dd80de059b5d241863e/users.xml - 從傳回的 RSS 內容,就可判斷此網頁是否已被收錄到黑米裡,也可看到已被哪些人推薦、收藏。
我把這玩意兒做成一個 Yahoo Pipes 模組,以後就可以直接引用,mashup 到其他可吃 RSS 及 JSON 的地方。不過可惜的是,Yahoo Pipes 並未提供 MD5 計算功能,必須先從別的地方產生出來,再餵給這個模組。
整體構想
整個流程很簡單,我先勾勒整個大局,稍後再細述個別步驟。
- 產生 blog 所有文章列表(只需含有文章標題、網址、網址 MD5 值這三項資訊),輸出成 RSS。
- 將這個 RSS 網址餵給 Yahoo Pipes。
- Yahoo Pipes 透過黑米 API,查詢上述每一篇文章的推薦清單。
- Yahoo Pipes 將結果輸出成 JSON 形式。
- Blog 系統取得上述的 JSON 結果,計算推薦指數,排序,取出前 N 名。
- 瀏覽器的 Ajax 以適當形式秀在網頁上。
整個流程如下圖所示:
![[整體流程示意圖]](/pic-blog/get-hemi-top10.png)
Blog 並不是每分每秒都有新文章誕生,所以第一步驟可做成靜態網頁,每逢 blog 文章有異動時才去自動或手動產生一次即可,或是納入系統定期備份程序的一環。
為什麼指明是 RSS 輸出形式?因為 Yahoo Pipes 比較喜歡它……
為什麼要在 RSS 裡面加入網址的 MD5 值?因為 Yahoo Pipes 自己做不到這一點,只好先算出來再餵給 Yahoo Pipes……
為什麼我一再指明是 Yahoo Pipes?因為我喜歡它,也拿它做了一點點應用,可趁此機會練練功;它也符合之前我列過的第 2、第 3 目標。但是它仍有許多不足之處,像是:內部資料流一律是以 RSS 為主,又沒有提供將其他資料型態轉換成 RSS 的內部模組,導致許多地方綁手綁腳。Yahoo Pipes 應該學習 Netvibes 的 UWA、iGoogle 的 Google Gadgets API 開放做法,才不會糟塌了這麼好的創意。
步驟一:文章列表 RSS
首先我們需要一個文章列表的 RSS 輸出,這個 RSS 只需含有文章標題 (title)、網址 (link)、網址 MD5 值 (guid) 三項資訊。
要如何取得所有文章列表呢?大體而言有三種方法:
- 利用 blog 系統的內建函數或外掛(像 WordPress 就有
get_posts()內建函數可用),再酌予加工處理。 - 利用 blog 系統的「匯出所有文章」功能。
- 直接去 blog 系統背後的資料庫撈資料。
第 1 種方法很正統,但需要熟悉 blog 系統架構及流程,才能繞過 blog 系統的層層包裝(關卡),直接輸出乾乾淨淨的 RSS。以 WordPress 為例:
<?php
if (empty($wp)) {
require_once('wp-config.php');
}
header('Content-Type: text/xml; charset="utf-8"');
echo <<<RSS_HEADER
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>部落格文章清單</title>
<description>部落格文章清單</description>
<link>部落格網址</link>
<language>zh-TW</language>
RSS_HEADER;
$all_posts = get_posts('numberposts=10000'); // a very big number!
foreach($all_posts as $post) :
?>
<item>
<title><?php the_title(); ?></title>
<link><?php the_permalink(); ?></link>
<guid><?php echo md5(get_permalink()); ?></guid>
</item>
<?php endforeach; ?>
</channel>
</rss>
這種做法的缺點是:每次系統都得去資料庫搜羅一遍,徒增系統負荷,必須加入一點簡單的 cache 機制。
至於第 2 種方法呢?Blog 系統通常都會有個「匯出所有文章」功能,且匯出格式通常都會包括 RSS(就連無名小站也都有人做出 RSS 2.0 匯出工具呢),這個 RSS 匯出檔就可做為我們的出發點。(至於另一種常見的匯出格式 Movable Type Import Format,因為裡面缺少網址資訊,不符合要求,所以不列入考慮。)
如果你產量驚人,總字數動輒以 MB 計,整個 RSS 檔案可能過於龐大,不適合直接拿來運用。有些 blog 系統產生的 RSS 檔只是權宜之計,只求 well-formed,根本沒有想過要符合嚴格的 XML 規範,難以進一步程式化處理(像 WordPress 匯出的 WXR 檔沒有針對 named entity 加以處理,無法通過 Firefox 及典型 RSS parser 的檢測)。這時你不妨考慮寫個小程式替它瘦身,只留下文章標題及網址;或者乾脆直接去背後的資料庫撈資料。
由於我原本就有寫 script 定期將資料庫的 schema 及資料全都 dump 出來備份,所以第 3 種方法「直接去背後的資料庫撈資料」算是舉手之勞。以 WordPress + MySQL 為例,可用以下命令得到所有文章的日期、標題、網址,輸出成 XML:
mysql -u 帳號 -p密碼 --database=資料庫 -X \
-e \
"select post_date pubDate, post_title title, guid link \
from wp_posts POST \
where POST.post_status='publish';"
資料庫、帳號、密碼三項設定,請見 WordPress 的 wp-config.php 檔。
然後再用個 Perl 小程式 fix-blog-rss.pl 稍加修正以上的 XML 輸出,讓它變成我要的 RSS 格式:
#!/usr/bin/perl -w
use Digest::MD5 qw(md5_hex);
while (my $line = <STDIN>) {
# insert hemidemi MD5
if ($line =~ /^\s*<link>([^<]+)<\/link>/) {
$link = $1;
$md5 = md5_hex($link);
print "\t<link>$link</link>\n";
print "\t<guid>$md5</guid>\n";
next;
}
# remaining stuff
if ($line =~ /^\s*<\?xml/) {
print <<RSS_HEADER;
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>部落格文章清單</title>
<description>部落格文章清單</description>
<link>部落格網址</link>
<language>zh-TW</language>
RSS_HEADER
next;
}
if ($line =~ /^\s*<\/resultset>/i) {
print "</channel>\n</rss>\n";
exit;
}
next if ($line =~ /^\s*<resultset/i);
next if ($line =~ /^\s*from\s/i);
next if ($line =~ /^\s*where\s/i);
$line =~ s/^\s*<row>/<item>/i;
$line =~ s/^\s*<\/row>/<\/item>/i;
print $line;
}
用簡單的 script 將上述兩個動作串連起來:
mysql -u 帳號 -p密碼 --database=資料庫 -X \
-e \
"select post_date pubDate, post_title title, guid link \
from wp_posts POST \
where POST.post_status='publish';" \
| fix-blog-rss.pl > 輸出檔
得到的 RSS 輸出檔,就可以餵給 Yahoo Pipes 去進一步處理。
以上總共介紹了三種產生文章列表 RSS 的做法,請根據自己的環境限制及偏好,從中任選一種。
步驟二:餵給 Yahoo Pipes
把上述 RSS 網址串接在本系統進入點的網址後面:
- http://pipes.yahoo.com/pipes/pipe.run?_render=json&_run=1 (續)
&_id=2Lk_zSMj3BG_J5kcqGIyXQ&url=網址
當然啦,為了保險起見,我在裡面也串接了日前設計的 xmliconv。
步驟三:取得推薦列表
利用稍早提過的黑米收藏推薦名單模組,求得每一篇文章的收藏推薦名單。
有了「名單」,接下來只要數個數就可算出每一篇文章的「熱門程度」。Yahoo Pipes 有提供 count 運算,但輸出形式是數字,不是 RSS 資料流,難以串接到 Yahoo Pipes 的後續流程;Yahoo Pipes 提供的 DOM 操作也少得可憐,難以將這個數字寫回原本的 RSS 資料流中,遑論據以排序。我只好將這串「名單」原原本本塞入 RSS 資料流的 item.rank 裡,留到第五步驟才去計算分數、排序。
步驟四:輸出成 JSON
Yahoo Pipes 對外輸出的 RSS 會吃掉一些欄位,以求符合 RSS 規格;但這對我們而言很不利。所以我在第二步驟就以「_render=json」參數指定輸出成 JSON,以保留 item.rank 這項重要資訊不被裁切。JSON 這種格式雖然不易肉眼辨識,但很容易程式化處理,容易串接到前端的 Ajax 或後端的 PHP、Java、.Net 平台。
步驟五:計算、排序
這是很典型的 blog 加工做法,以 WordPress 為例:
<?php
$url = '步驟二所提的整串網址';
$counter = 10; // 想輸出前幾名的資料?
header('Content-Type: text/plain; charset="utf-8"');
$response = file_get_contents($url); // 步驟二:連線到 Yahoo Pipes...
$obj = json_decode($response);
$ar_link = array();
$ar_counter = array();
foreach ($obj->value->items as $item) {
array_push($ar_link, $item->link);
$num = count($item->rank); // 計算推薦次數
array_push($ar_counter, $num);
$item->counter = $num;
}
array_multisort($ar_counter, SORT_DESC,
$ar_link, SORT_DESC,
$obj->value->items);
for ($i = 0; $i < $counter; ++$i) {
echo '<li>';
echo '<a href="';
echo $obj->value->items[$i]->link;
echo '">';
echo $obj->value->items[$i]->title;
echo '</a> ';
echo '(';
echo $obj->value->items[$i]->counter;
echo ")</li>\n";
}
理論上來說,這段程式的輸出應該也要編碼成 JSON 形式,顯示細節 (HTML/XHTML) 應該交給瀏覽器端的 Ajax 去負責,才比較符合 MVC 精神。不過我懶得如此嚴格,覺得有點殺雞用牛刀。
步驟六:以 Ajax 方式取得結果
前面的步驟二~步驟五,可能會有網路存取時間需要等待,所以最好是以 Ajax 方式拖延局部畫面的輸出,如此一來,即使把它放在 blog 的側邊列,也不會拖慢整個 blog 頁面(口說無憑,請再看一次 demo 吧)。
我以 Prototype 程式庫為例,示範如何以非同步方式取得前面辛辛苦苦拿到的文章排行榜,以 HTML 形式秀在瀏覽器:
<html>
<head>
<script type="text/javascript"
src="http://www.prototypejs.org/javascripts/prototype.js">
</script>
<script>
function get_hemi_top10(ajax_prog, result_panel)
{
var myAjax = new Ajax.Request(ajax_prog,
{ method: 'get',
onSuccess: function(http_obj) {
var response = http_obj.responseText || "";
$(result_panel).innerHTML = response; // 組裝成 XHTML 輸出形式
},
onFailure: function(http_obj) {
var response = http_obj.responseText;
alert("Fail!\n" + response);
}
});
}
</script>
</head>
<ol id="hemidemi_top10_panel"> <!-- 最終結果輸出處 -->
<!-- Ajax 常見風格:放一個「等待中」的動畫 -->
<img alt=""
src="http://www.ajaxload.info/cache/ff/ff/ff/00/80/ff/24-1.gif"
/>
</ol>
<script>
get_hemi_top10(
'請填入步驟五的程式網址',
'hemidemi_top10_panel' // 填入「最終結果輸出處」的 id
);
</script>
</body>
</html>
當然啦,如果你不喜歡 Ajax,或是你的 BSP 不支援 Ajax,就只好把第六步驟省略掉,直接把顯示邏輯寫死在第五步驟裡。
為了加深印象,我把示意圖再秀一遍:
![[整體流程示意圖]](/pic-blog/get-hemi-top10.png)
這篇文章以一個簡單的應用需求出發,談到 WordPress、Yahoo Pipes、JSON、Ajax 的綜合運用,希望對您有幫助,觸發更多活用的靈感。


追蹤留言回應:以
引用通告 (trackback):![[add to funP]](http://william.cswiz.org/blog/wp-content/themes/william/images/add-funp.png)
![[add to HEMiDEMi]](http://www.hemidemi.com/sticker/user/roxytom.bluecircus.net.gif)
![[add to udn bookmark]](http://bookmark.udn.com/html/help/80_20_02.gif)

2007 年 六月 28日 於 9:39 am
請教一下,您這邊呈現 Code 的 Plugin 是用哪個?謝謝。 看起來連 mysql dump 的指令都處理的蠻好的。
2007 年 六月 28日 於 10:59 am
我用的是 Google Code 裡面的 SyntaxHighlighter,不過還沒時間仔細調整 CSS。
2007 年 八月 21日 於 10:09 pm
黑米今天新推出的「部落格工具包」,的確提供了不錯的文章排行榜列表,是正確的一步,也算是初步回應了 BillPan 在〈我在黑米熱門書籤排行榜自動產生器 + Flash 搜尋器〉一文所提的訴求。
只可惜還是沒有提供我這篇文章一開始的訴求:API。
雖然現在可以用 Dapper 的 screen scraping 功能來生出一個 RSS,但畢竟不是王道,也容易因黑米改變排版方式而失效。黑米官方提供的〈在部落格顯示您最受歡迎的文章〉JavaScript 功能則是和以往類似服務有著相同的老問題:太 coarse-grained 了,客製化能力不足;譬如說,如果想做出我的部落格側邊欄「熱門文章(黑米榜)」的樣子,就只能自己動手,官方版的 JavaScript 完全使不上力。
長久之計還是應該請黑米官方釋出這方面的 API 呀。