黑米外掛:文章排行榜

2007 年 六月 27 日 (星期三) 10:04 pm
分類:電腦
標籤:, , , ,

楔子

對同一個網站,各人有各自的使用方式,也各有不同的預期。某些服務,原本應該是由網站主動提供的,但受限於目標、願景、人力、能力、優先順序等考量,官方可能不會提供;既然官方沒提供,改由熱血用戶實作出來,雖然很符合 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 來展示我想做出來的效果吧。

目標

照例,在捲起袖子動手之前,先設下幾個目標:

  1. 盡可能使用黑米官方對外公開的 API。既然是 API,內部應該早就經過千錘百鍊的最佳化處理,效能不成問題,可放心使用,不會有「幫學弟測流量」的嫌疑。
  2. 盡可能降低自己主機的負載。我的電腦並不高檔,還有別的要事在排班,禁不起像 .CK 那樣的流量破表事情發生。最好是善用第三方的網路服務(像 Yahoo Pipes)、瀏覽器的 Ajax 能力,不要什麼事都攬在自己身上。
  3. 盡可能讓系統容易轉移到其他架站軟體或 BSP

原理

黑米官方沒有提供「輸入:base URL,輸出:在黑米的排行榜」或「輸入:一堆網址,輸出:在黑米的排行榜」的 API,也沒有提供「輸入:一個網址,輸出:在黑米的收藏推薦數目」API(儘管 270 天以前我就在黑米提議過,但顯然只有我有這個需求而已,留言既沒被推也沒被採納),只有提供「輸入:一個網址,輸出:在黑米的收藏推薦名單」API。我們就以此為出發點。

「在黑米的收藏推薦名單」是個以 MD5 為基礎的 list/probe RSS API。譬如說,如果想知道〈xmliconv:解決 Yahoo Pipes 中文編碼問題〉這篇文章在黑米的收錄狀況,可依以下方式探測出:

  1. 此文章的網址是 http://william.cswiz.org/blog/archives/2007-06-13/xmliconv/
  2. 算出此網址的 MD5:4b2555f1e3f86dd80de059b5d241863e
  3. 套入黑米提供的「收藏推薦名單」RSS feed:
    http://www.hemidemi.com/rss/bookmark/  (續)
    4b2555f1e3f86dd80de059b5d241863e/users.xml
  4. 從傳回的 RSS 內容,就可判斷此網頁是否已被收錄到黑米裡,也可看到已被哪些人推薦、收藏。

我把這玩意兒做成一個 Yahoo Pipes 模組,以後就可以直接引用,mashup 到其他可吃 RSS 及 JSON 的地方。不過可惜的是,Yahoo Pipes 並未提供 MD5 計算功能,必須先從別的地方產生出來,再餵給這個模組。

整體構想

整個流程很簡單,我先勾勒整個大局,稍後再細述個別步驟。

  1. 產生 blog 所有文章列表(只需含有文章標題、網址、網址 MD5 值這三項資訊),輸出成 RSS
  2. 將這個 RSS 網址餵給 Yahoo Pipes。
  3. Yahoo Pipes 透過黑米 API,查詢上述每一篇文章的推薦清單。
  4. Yahoo Pipes 將結果輸出成 JSON 形式。
  5. Blog 系統取得上述的 JSON 結果,計算推薦指數,排序,取出前 N 名。
  6. 瀏覽器的 Ajax 以適當形式秀在網頁上。

整個流程如下圖所示:

[整體流程示意圖]

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) 三項資訊。

要如何取得所有文章列表呢?大體而言有三種方法:

  1. 利用 blog 系統的內建函數或外掛(像 WordPress 就有 get_posts() 內建函數可用),再酌予加工處理。
  2. 利用 blog 系統的「匯出所有文章」功能。
  3. 直接去 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,就只好把第六步驟省略掉,直接把顯示邏輯寫死在第五步驟裡。

為了加深印象,我把示意圖再秀一遍:

[整體流程示意圖]

這篇文章以一個簡單的應用需求出發,談到 WordPress、Yahoo Pipes、JSON、Ajax 的綜合運用,希望對您有幫助,觸發更多活用的靈感。


◤建議您一併閱讀以下文章:

3 項留言回應 給 “黑米外掛:文章排行榜”

  1. 1 derjohng 留言:

    請教一下,您這邊呈現 Code 的 Plugin 是用哪個?謝謝。 看起來連 mysql dump 的指令都處理的蠻好的。

  2. 2 william 留言:

    我用的是 Google Code 裡面的 SyntaxHighlighter,不過還沒時間仔細調整 CSS。

  3. 3 william 留言:

    黑米今天新推出的「部落格工具包」,的確提供了不錯的文章排行榜列表,是正確的一步,也算是初步回應了 BillPan 在〈我在黑米熱門書籤排行榜自動產生器 + Flash 搜尋器〉一文所提的訴求。

    只可惜還是沒有提供我這篇文章一開始的訴求:API。

    雖然現在可以用 Dapper 的 screen scraping 功能來生出一個 RSS,但畢竟不是王道,也容易因黑米改變排版方式而失效。黑米官方提供的〈在部落格顯示您最受歡迎的文章〉JavaScript 功能則是和以往類似服務有著相同的老問題:太 coarse-grained 了,客製化能力不足;譬如說,如果想做出我的部落格側邊欄「熱門文章(黑米榜)」的樣子,就只能自己動手,官方版的 JavaScript 完全使不上力。

    長久之計還是應該請黑米官方釋出這方面的 API 呀。

留言回應

[檢核碼]  


Allowed XHTML tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

本站已啟用 spam 防護機制。為避免系統誤判,請在按下按鈕之前,先備份您的留言,以防不測。如果您一直無法順利留言,請改用 email 方式。
此外,如果您想留的言與本篇文章及討論串無關,也請轉而點選這裡。謝謝您!