2012年12月27日

使用 Groovy + NekoHTML 快速開發網頁原始碼資料分析程式

Java 有很強悍的 XML 與 HTTP Client 函式庫,所以可以看到不少爬蟲引擎都是 Java-based 設計;但是如果只想要寫一個非常簡單的網頁原始碼資料分析,使用 Java 開發相當麻煩,通常許多開發者會選擇比較簡易的作法,例如市售書籍有介紹 PHP + CURL 的作法。

不過有了 Groovy 這個 Java 的 Scripting 語言,只要簡單地搭配 NekoHTML Library 就能快速做出簡易源碼分析程式。前幾天曾介紹過「Groovy + XmlParser」用來擷取 Google 新聞 RSS 的內容,但是這個作法並不適用於一般網頁,因為一般的 HTML 很少能通過標準 XML 的解析,所以我們只另找對 HTML/XHTML 有更強悍解析能力的函式庫。

這個範例使用 CyberNeko 的 NekoHTML,這是個相當管用的函式庫,搭配 Groovy 服用只需要不到十行程式碼就能達成目的。

此範例的目標是擷取 Google 新聞首頁側邊欄的「發燒新聞」列表標題,雖然用 RSS 或 API 存取也能達成相同目的;但本文的範例對某些用途來說比較敏感,所以只好挑個比較沒爭議的資料來源 : )

view-source:https://news.google.com/nwshp?hl=zh-TW&tab=wn
本範例擷取右邊側欄「發燒新聞」的文字內容

第一個要解決的問題,是傳統 Java 需要先加載 .jar 函式庫檔案到 CLASSPATH 的麻煩,這對 Groovy 來說,只需要一行程式碼就解決了,使用 Grape / Grab 自動從 Maven Repositories 取得相依的套件庫:

@Grab('net.sourceforge.nekohtml:nekohtml:1.9.16')

接下來開始建立 NekoHTML 的 SAXParser(),它用來解析 HTML/XHTML 文件。


def parser = new org.cyberneko.html.parsers.SAXParser()
parser.setFeature('http://xml.org/sax/features/namespaces', false)

從某個 URL 載入原始碼並完成解析只要一行:

def page = new XmlParser(parser).parse('https://news.google.com/nwshp?hl=zh-TW&tab=wn')

接著利用網頁除錯工具或打開原始碼,找到發燒新聞的 DOM 標籤。

使用 Google Chrome 內建的網頁除錯工具

以這個範例來說,我們的目標就是這個 <DIV> 標籤:

<div class="small-section section-POPULAR" ...

所以利用深度優先(depthFirst)搜尋將節點找到,這裡搭配 grep 進行資料篩選,只有 class 屬性包含 small-section 與 section-POPULAR 才會被取出:

page.depthFirst().DIV.grep{ it.'@class'?.contains('small-section') }

上面的傳回結果是 List 型態,所以可以用 each 做迴圈:

    page.depthFirst().DIV.grep{ it.'@class'?.contains('small-section section-POPULAR') }.each {
        div->
        println div
    }

下一步開始修改迴圈內容,將裡面包含的標題找出來,標題清單位於這個 <DIV> 標籤內:

<div class="contents" s="t">

同樣使用 depthFirst 方式搜尋:

    def contents = div.depthFirst().DIV.grep{ it.'@class'=='contents'&&it.'@s'=='t' }

這個結果也是 LIST,我們只需要取出一個:

    if (contents.size() > 0) {
        contents.first().depthFirst().DIV.grep{ it.'@class'=='title' }.each {
            div_title->
            println div_title
        }
    }

其中 div_title 的資料結構如下:

DIV[attributes={class=title}; value=[A[attributes={target=_blank, class=article usg-AFQjCNGshI48ZItUXdT9SuCtCCz76f-tGA did--2335265617202207122, href=http://www.espnstar.com.tw/news/basketball/2012/1227/256409.htm, url=http://www.espnstar.com.tw/news/basketball/2012/1227/256409.htm, id=-2335265617202207122}; value=[SPAN[attributes={class=titletext}; value=[險勝灰狼豪自評打得難看]]]]]]

到這邊已經很容易直接將文字內容取出:

        println div_title.A.SPAN.text()

目前完成的程式碼如下,已經可以解析出標題文字:

    @Grab('net.sourceforge.nekohtml:nekohtml:1.9.16')
 
    def parser = new org.cyberneko.html.parsers.SAXParser()
 
    parser.setFeature('http://xml.org/sax/features/namespaces', false)
 
    def page = new XmlParser(parser).parse('https://news.google.com/nwshp?hl=zh-TW&tab=wn')
 
    page.depthFirst().DIV.grep{ it.'@class'?.contains('small-section section-POPULAR') }.each {
        div->
     
        def contents = div.depthFirst().DIV.grep{ it.'@class'=='contents'&&it.'@s'=='t' }
     
        if (contents.size() > 0) {
            contents.first().depthFirst().DIV.grep{ it.'@class'=='title' }.each {
                div_title->
                println div_title.A.SPAN.text()
            }
        }
    }

輸出結果參考如下:

印度政府把巴士強暴案女大學生送新加坡治療
險勝灰狼豪自評打得難看
金溥聰:總統堅持對的事氛圍會改善
黃淑琦不熟新家墜樓賈永婕哭喊:妳趕快回來!
創舉! 飢童可免費到4大超商吃到飽
京廣高鐵通車陸媒瘋狂連線
宏達電蝴蝶機Butterfly(2498-TW)熱銷
披肩殺人! 捲入機器勒斃女子
Kobe狂飆40分湖人兵敗丹佛
總統:希望每年新生兒不少於18萬


上面的程式碼落落長,其實只是為了說明 NekoHTML 的用法,實際上程式碼可以化簡成不到十行:

    @Grab('net.sourceforge.nekohtml:nekohtml:1.9.16')
    def parser = new org.cyberneko.html.parsers.SAXParser()
    def page = new XmlParser(parser).parse('https://news.google.com/nwshp?hl=zh-TW&tab=wn')
    page.depthFirst().DIV.grep{ it.'@class'=='title' }.each {
        println it.A.SPAN.text()
    }

Groovy 很值得 Java 開發者學習,即使沒機會在專案派上用場,它平時也能化簡很多工作上繁雜的電腦操作程序啊 : ) 歡迎加入 Groovy Taiwan 的學習社群~

原始碼分享於 Gist:
https://gist.github.com/4387321



1 則留言:

  1. 謝謝分享,我現在正在想一個開發的程式。
    這一篇文章對我有很大的幫助,謝謝

    回覆刪除

lyhcode by lyhcode
歡迎轉載,請務必註明出處!