temme:優雅地從 HTML 提取 JSON 數據
本文介紹了一個從 HTML 提取 JSON 數據的工具,並以豆瓣電影的例子展示了該工具的使用方法。本文中用到了大量的 CSS 選擇器,CSS 選擇器可以參考 MDN。
最近幾個月寫 Node 爬蟲比較多,解析 HTML 文檔用的工具是 cheerio(cheerio 可以認為是伺服器版的 jQuery)。cheerio 功能相當豐富,提供了一大堆 API 來查詢/修改/刪除/添加結點或文本。不過隨著爬取的頁面數量越來越多,大量使用 cheerio 還是顯得繁瑣了一點。爬蟲對於處理 HTML 的模式其實比較固定,但是 cheerio 處理某些模式時不夠簡潔明了,下面三點就是一些比較常見的情況:
下面的三點中,假設我們要從豆瓣電影首頁中爬取上圖這樣一個 「正在熱映」列表。注意該列表是實時更新的,所以本文中下面的選擇器的運行結果可能不同。
- 同一個元素會包含多個數據欄位。比如上圖中每個電影都有電影標題
title
,鏈接url
和評分rate
欄位; - 爬取目標是一個列表(甚至是列表的列表)。比如上圖中我們需要抓取一個電影信息列表;
- 頻繁但簡單的格式處理。例如:將電影的評分從字元串類型轉化為數字,去除電影鏈接中不需要的 url 參數。
temme 就是基於以上幾點觀察而開發出來的處理 HTML 的工具。temme 在 CSS 選擇器的基礎上,針對以上三點,加入了額外的語法來優雅地處理上述情況:
- 支持同時使用多個選擇器;支持多個欄位同時抓取;
- 支持列表抓取;
- 支持格式處理。
安裝與使用
# 全局安裝nyarn global add temme # npm install --save temmenn# 最基本的使用方式ntemme <selector> <html>nn# 省略html參數,使用來自stdin的輸入;--format 參數表示格式化輸出ntemme <selector> --formatnn# 使用文件中的選擇器ntemme <path-to-a-selector-file> <html>nn# 和 curl 配合使用ncurl -s <url> | temme <selector>n
temme 提供了一個 在線網頁版本,其中的編輯器提供了語法高亮功能。本文的剩下的部分也可以在該在線版本中進行,注意將對應的 HTML 複製過來即可。
例子一:從豆瓣電影首頁抓取電影信息
抓取第一個電影的標題,評分以及鏈接。temme 選擇器如下:
命令行運行步驟如下:
curl -s https://movie.douban.com | temme .ui-slide-item[data-title=$title data-rate=$rate]; .ui-slide-item a[href=$url]; --formatn# output:n# {n# "title": "煙花 打ち上げ花火、下から見るか?橫から見るか?",n# "rate": "5.7",n# "url": "https://movie.douban.com/subject/26930504/?from=showing"n# }n
例子中的選擇器和 CSS 選擇器非常相似,不一樣的地方在於 temme 選擇器包含了下面這樣的結構:[foo=$bar]
。該結構的含義是「將 foo 屬性放到結果的 bar 欄位」。上面的選擇器包含了三個這樣的結構,一次性選取了三個欄位。上面的選擇器也同時包含了兩個子選擇器(在圖中每行一個),每個子選擇器用分號作為結束符。
另一個常見的結構是 div{$buzz}
,該結構表示「將 div 元素的文本內容放到結果的 buzz 欄位」。如果熟悉 emmet 的話,可以看出來目前 temme 的行為就是 emmet 的逆過程。
例子二:格式變換
上面結果中 rate
是個字元串,我們可以用過濾器 Number
對其進行處理。我們這次不選取其他欄位。
curl -s https://movie.douban.com | temme .ui-slide-item[data-rate=$rate|Number];n# output: {"rate":5.7}n
可以看到結果中 rate
欄位類型為數字。目前結果中只有 rate
一個欄位,那麼將該欄位的值直接作為結果更為方便:
curl -s https://movie.douban.com | temme .ui-slide-item[data-rate=$|Number];n# output: 5.7n
省略 $xxx
中的 xxx
,那麼結果的格式會從 { xxx: yyy }
變為 yyy
。
例子三:「正在熱映」列表
「正在熱映」是一個列表,每一個電影信息對應一個滿足 CSS 選擇器 .ui-slide-item[data-title]
的 HTML 元素。上面的例子我們只選取了第一個電影的數據,這裡我們使用 @
符號來選取該列表。抓取「正在熱映」列表中所有電影的信息,選擇器如下:
運行效果如下:
curl -s https://movie.douban.com | temme .ui-slide-item[data-title] @recentMovies { &[data-title=$title data-rate=$rate|Number]; a[href=$url]; } --formatn# output:n# {n# "recentMovies": [n# {n# "title": "煙花 打ち上げ花火、下から見るか?橫から見るか?",n# "rate": 5.7,n# "url": "https://movie.douban.com/subject/26930504/?f rom=showing"n# },n# {n# "title": "相聲大電影之我要幸福",n# "rate": 0,n# "url": "https://movie.douban.com/subject/26811605/?f rom=showing"n# },n# ......n# ]n# ]n
選擇器含義:每一個滿足 CSS 選擇器 .ui-slide-item[data-title]
的 HTML 元素就是一個電影詳情的父元素,我們將 @
放在該選擇器之後,緊跟的 recentMovies
表示「最近熱映列表」在最終結果中的欄位名,然後我們在花括弧中放入例子一中的兩個選擇器,以選取單個電影的數據。
如果我們在這裡省略 @recentMovies
中的 recentMoviews
,僅保留一個 @
符號,那麼最終結果就會變為一個數組(JSON 的層級會減一層)。
列表的捕獲可以進行嵌套。例如在一個 stackoverflow 問題頁面中有多個回答,每個回答下有多個評論,下面的選擇器可以將這些評論以二維列表的格式抓取下來:
curl -s https://stackoverflow.com/questions/1014861/is-there-a-css-parent-selector | temme .answer@{ .comment@{ .comment-body{$|trim}; }; };n
例子四:電影詳情頁面
在首頁爬取到電影鏈接列表之後,我們可以進入每個電影的頁面爬取該電影的詳細數據。這裡我們以 煙花 這個電影為例子。電影介紹頁面中的數據非常詳細,包含了電影名稱、導演、編劇、主演、電影類型、官方網站等信息。這裡挑取了部分數據進行抓取,選擇器如下:
// 電影的名稱n[property="v:itemreviewed"]{$title};n// 電影上映年份n.year{$year|substring(1, 5)|Number};n// 電影導演n[rel="v:directedBy"]@directedBy { &{$} };n// 電影編劇(:contains是來自jQuery的選擇器 https://api.jquery.com/contains-selector/)n:contains(編劇) + span{$storyFrom|split(/)||trim};n// 電影主演(前三位)n[rel="v:starring"]@starring|slice(0, 3){ &{$} };nn// 平均評分n[property="v:average"]{$avgRating|Number};n// 具體的評分情況n.ratings-on-weight .item@ratingInfo{n span[title=$title];n .rating_per{$percentage};n};nn// 電影劇情簡介n[property="v:summary"]{$summary|trim};nn// 喜歡這部電影的人也喜歡...n.recommendations-bd dl@recommendations{n img[alt=$name src=$imgUrl];n a[href=$url];n};n
這裡選擇器較長,寫在終端中不太方便,我們將該選擇器保存到文件 douban-movie.temme,然後運行 temme:
curl -s https://movie.douban.com/subject/26930504/ | temme douban-movie.temme --formatn# output:n# {n# "title": "煙花 打ち上げ花火、下から見るか?橫から見るか?",n# "year": 2017,n# "directedBy": [ "新房昭之", "武內宣之" ],n# "storyFrom": [ "岩井俊二", "大根仁" ],n# "starring": [ "廣瀨鈴", "菅田將暉", "宮野真守" ],n# "avgRating": 5.4,n# "ratingInfo": [n# { "title": "力薦", "percentage": "7.2%" },n# { "title": "推薦", "percentage": "12.8%" },n# ......n# ],n# "summary": "川村元氣即將再度與《你的名字。》制......",n# "recommendations": [n# {n# "name": "想要傳達你的聲音",n# "imgUrl": "https://img3.doubanio.com/vie......",n# "url": "https://movie.douban.com/subject......"n# }n# ......n# ]n# }n
該選擇器雖然選取了很多內容,但是仍然保持了清晰的結構以及良好的可讀性。可以打開該例子的在線版本,對比其中選擇器和輸出的格式,應該可以明白該選擇器的含義。
寫爬蟲的時候,我們首先分析頁面結構,利用在線版本為每一種不同類型的頁面寫好對應的選擇器,然後將選擇器保存在本地文件中。爬蟲運行獲取到 HTML 之後,我們讀取相應的選擇器文件,運行並得到想要的輸出。
總結與其他
上面的介紹基本涉及到了 temme 的核心用法,可以看到 temme 實現了前面提到的改進思路。實踐中大部分網站的頁面結構都是比較清晰的,分析頁面元素的 CSS 選擇器也比較容易,此時使用 temme 可以大大提高數據選取的效率。temme 更完整的用法和文檔還請移步 Github,歡迎 fork 和 star。
下面列舉一些開發用到的主要技術:
- 開發語言 TypeScript
- 自定義語法解析 PEG.js
- HTML解析 cheerio
- 編譯工具 webpack
- 自動化測試 Jest
- 在線版本編輯器 ace
- 靈感來自 emmet
推薦閱讀: