⚠この記事は個人による適当な調査結果をまとめたものです。間違いなどありましたらご指摘いただきますと幸いです。⚠
みなさん、スクレイピングしていますか?スクレイピング時のパーサーライブラリは、何を使っていますか?
私はRubyが好きなので、大体Nokogiriを使っています。
しかし先日、あるスクレイピングの作業中に、NokogiriのHTMLパーサーは少し遅いのでは…?と感じました。約10万件のデータをパースするのに、約10分ほど待たなければいけないのです。何度も回して試行錯誤しなければならなかったので、時間がかかって仕方ありませんでした。
そもそもRubyが遅いので、言語問わず他のパーサーを使えばもっと快適に作業できのでは?という考えから、今回は、いくつかの言語のHTMLパーサーのパフォーマンスを比較してみました。
比較結果やコードは GitHub(snakazawa/html-parser-comparison) にも上げています。
対象の言語とライブラリ
言語は、私がある程度使い慣れているRuby, Python, JavaScript(Node.js), C++の4つとします。PHPとJavaも候補に入れようと思いましたが、使いたくないので止めました。
各言語でHTMLパーサーのライブラリは簡単に調べて、ある程度有名そうなものを選びました。
- Ruby: Nokogiri, Oga
- Python: BeautifulSoup4 (3つのパース方法を試す)
- JavaScript: cheerio, jsdom, dom-parser, fast-html-parser
- C++: gumbo-query
比較方法
以下の項目を比較します。
- パフォーマンス: 1秒間にパースできるファイル数
- エラー数: パースできないファイル数
- 要素検索方法: XPathやCSSセレクタなどの、要素を検索するインターフェース
- その他: Cなどで実装された外部依存ライブラリや、WHATWG準拠かどうか
計測方法
Qiita Advent Calendar2017 の記事のうち、2017年12月24日までに投稿された文字コードがUTF-8のHTMLファイル 10599 個をパース対象とします。記事はShellでクロールしました(詳しくは (https://github.com/snakazawa/html-parser-comparison#crawling)[https://github.com/snakazawa/html-parser-comparison#crawling])。
各パーサーで下の疑似コードのように、(1)ファイルを読み込む、(2)読み込んだHTMLテキストをパースする、(3)最初のh1タグを探す、という処理をすべてのファイルに対して行い、このときのパフォーマンスを計測します。
for file in files:
html = read(file) // (1)
doc = parser.parse(html) // (2)
h1 = parser.findOne('h1') // (3)
これを各パーサーで10回繰り返し、それぞれのパフォーマンスの平均値を最終的な結果とします。
結果
ここで示すコードは、あくまでイメージをしやすくするためのサンプルコードです。実際に計測に使ったコードはGitHubに上げてあります。
(Ruby) Nokogiri
require 'nokogiri'
Doc = File.open(html_path) { |f| Nokogiri::HTML(f) }
node = doc.at_xpath('//h1')
パフォーマンス: 225.72 files/s, no error
Cで実装された外部ライブラリ(libxml2)を使います。そのため、(最近はあまり感じませんが)インストールが少し面倒かつ遅いです。
要素検索はXPathとCSSクエリに対応しています。
これを基準とします。
(Ruby) Oga
require 'oga'
Doc = File.open(html_path) { |f| Oga::parse_html(f) }
node = doc.at_xpath('//h1')
パフォーマンス: 61.16 files/s, 19 errors
全てRubyで書かれています(astとruby-llを利用)。そのため、インストールが非常に楽です。多くのプラットフォームで動くようです。
しかし、Nokogiriに比べると1/3未満の速度しか出ず、また、いくつかのファイルがパースできていません。
インターフェースはNokogiri likeで、同じくXPathとCSSクエリに対応しています。
Rubyを使いたくて、かつ、パース対象の量が少なく場合に良さそうです。
(Python) BeautifulSoup4
from bs4 import BeautifulSoup
parser_type = 'html.parser'
soup = BeautifulSoup(open(html_path), parser_type)
node = soup.find('h1')
parser_typeは html.parser
, html5lib
, lxml
の3つのライブラリから選択できます。
要素検索はCSSクエリと、独自のfind関数を提供しています。
html.parser
パフォーマンス: 39.65 files/s, no error
標準ライブラリのパーサーを使っているので、安定感がありそうです。
しかし、速度はNokogiriの1/5未満と、なかなか厳しいです。
html5lib
パフォーマンス: 16.43 files/s, no error
WHATWG仕様に準拠したパーサーです。
速度は遅いですが、正しいHTML5を生成するようです(公式ドキュメントより)。
lxml
パフォーマンス: 54.56 files/s, no error
パースにはCで実装された外部ライブラリを使います(libxml2とlibxslt)。
公式ドキュメントによると “爆速(Very fast)” らしいのですが、Ogaよりも速度が出ていません。
(JavaScript) cheerio
const fs = require('fs');
const cheerio = require('cheerio');
const html = fs.readFileSync(htmlPath, 'utf-8');
const $ = cheerio.load(html);
const nodes = $('h1');
パフォーマンス: 109 files/s, no error
パースにはJavaScriptで書かれた htmlparser2 を使います。
jQueryライクなインターフェースを提供しています。
速度はNokogiriの半分程度ですが、それなりに早いです。
jQueryが好きな人には良さそうです。
(JavaScript) jsdom
const fs = require('fs');
const {JSDOM} = require('jsdom');
JSON.fromFile(htmlPath).then(({window: {document: doc}}) => {
const nodes = doc.getElementsByTagName('h1');
});
パフォーマンス: 30.33 files/s, no error
パースにはJavaScriptで書かれた parse5 を使います。
要素検索はDOM関数(getElementsなんちゃらやquerySelector)に対応しています。
速度は遅いですが、このライブラリは仮想DOM環境を構築しているようです。また、html5libと同じくparse5はWHATWG準拠です。
(JavaScript) libxmljs
const fs = require('fs');
const libxmljs = require('libxmljs');
const html = fs.readFileSync(htmlPath, 'utf-8');
const doc = libxmljs.parseHTLM(html);
const node = doc.get('//h1');
パフォーマンス: 314.33 files/s, no error
名前の通り、Cで実装されたlibxml2を使ってパースします。
要素検索はXPathのみの対応ですが、Nokogiriの約1.5倍早いです。なかなか良いですね。
(JavaScript) dom-parser
const fs = require('fs');
const DomParser = require('dom-parser');
const html = fs.readFileSync(htmlPath, 'utf-8');
const node = doc.getElementsByTagName('h1');
パフォーマンス: 1405 files/s, 1 error
Nokogiriの約7倍の速さ!?と思ったら、検索時に正規表現を走らせるライブラリのようです。木の構築はしないので、何度も検索を行う場合は遅くなるかもしれません。
要素検索は、getElement系により行えますが、CSSクエリやXPathには対応していません。
しかし、速度はピカイチなので、使い道はありそうです。
(JavaScript) fast-html-parser
const fs = require('fs');
const HTMLParser = require('fast-html-parser');
const html = fs.readFileSync(htmlPath, 'utf-8');
const doc = HTMLParser.parse(html);
const node = doc.querySelector('h1');
パフォーマンス: 972 files/s, no error
外部ライブラリのパーサーを使っていません。
要素検索はCSSクエリのみ対応しています。
HTML 4 styleのmalformatted HTMLには対応していないとのことですが、Nokogiriの4倍以上と、非常に高速です。
(C++) gumbo-query
#include <bits/stdc++>
#include <iostream>
#include <fstream>
#include <string>
#include <iterator>
#include <vector>
#include <gq/Document.h>
#include <gq/Node.h>
int main(void) {
ifstream ifs(path);
istreambuf_iterator<char> it(ifs);
istreambuf_iterator<char> last;
string text(it, last);
CDocument doc;
doc.parse(text.c_str());
CSelection c = doc.find('h1');
return 0;
}
パフォーマンス: 439 files/s, no error
Googleが作ったC実装の gumbo-parser を使っています。WHATWGに準拠しています。
要素検索はCSSクエリのみ対応しています。
速度はNokogiriの約2倍と、なかなか良いですが、やはりC++なので、インストールや実装が少々面倒です。
まとめ
表が見にくいので画像のリンク -> 表(画像)
言語 | ライブラリ名 | 利用パーサー | 要素検索方法 | パフォーマンス(file/sec) | エラー数 | 備考 |
---|---|---|---|---|---|---|
Ruby | Nokogiri | libxml2 (C) | XPath, CSS | 245 | 0 | |
Ruby | Oga | ast, ruby-ll (Ruby) | XPath, CSS | 66 | 31 | |
Python | BeautifulSoup4 | html.parser (Python, standard library) | CSS, find関数 | 42 | 5 | |
Python | BeautifulSoup4 | libxml2, libxslt (C) | CSS, find関数 | 60 | 5 | |
Python | BeautifulSoup4 | html5lib (Python) | CSS, find関数 | 17 | 5 | WHATWG準拠 |
JavaScript | cheerio | htmlparser2 (JavaScript) | CSS ( jQuery like) | 125 | 0 | |
JavaScript | libxmljs | libxml2 (C) | XPath | 383 | 0 | |
JavaScript | jsdom | parse5 (JavaScript) | CSS, DOM関数 | 36 | 0 | WHATWG準拠 |
JavaScript | dom-parser | 自前 (regex) | DOM関数(CSS除く) | 1562 | 1 | 木の生成なし |
JavaScript | fast-html-parser | 自前 | CSS | 1400 | 0 | |
C++ | gumbo-query | gumbo-parser (C) | CSS | 459 | 0 | WHATWG準拠 |
Nokogiriは悪くない選択肢でした。ごめんなさい。
Pythonは少し期待していたのですが、BeautifulSoup4はあまり良い結果となりませんでした。
JavaScriptはいろいろと選択肢があって良いですね。fast-html-parserの速さには驚きです(V8が強いのでしょうか)。この分だとWHATWG準拠のJavaScriptの早いパーサーもどこかにありそうです。
C++のgumbo-queryは導入の面倒くささを除けば、WHATWG準拠で速度もあり、流石です。
ということで、普段のパース案件であれば、JavaScriptのdom-parserかfast-html-parserを使っておけば幸せになれそうですね。やっぱりJavaScriptは最強でした。