⚠この記事は個人による適当な調査結果をまとめたものです。間違いなどありましたらご指摘いただきますと幸いです。⚠

みなさん、スクレイピングしていますか?スクレイピング時のパーサーライブラリは、何を使っていますか?
私は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で実装された外部ライブラリを使います(libxml2libxslt)。
公式ドキュメントによると “爆速(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は最強でした。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください