lxmlをレビュー:いまなおPythonのどのパーサーよりもXPathで強い理由

最終更新日 August 12, 2026
lxmlをレビュー:いまなおPythonのどのパーサーよりもXPathで強い理由
AI要約
この lxml レビューでは、libxml2 と libxslt を支える長寿の Python バインディングとして lxml を位置づけ、最新パーサーでもなかなか代替できない最大の強みである本物の XPath エンジンに注目しています。記事では、XPath の対応範囲、パーサーの厳格モード、ストリーミング時のメモリ挙動、CSS と XPath の表現力の差、libxml2 の深さ制限を検証しています。その結果、lxml は高速かつ省メモリで、軸・述語・関数・ストリーミング・堅牢な復旧モードが必要な XML / HTML 処理に非常に強いことが分かります。また、セキュリティを意識した木の深さ上限と、huge_tree を使うことで何が変わるのかも説明しています。

数か月おきに、より速いHTML parserが登場し、benchmarkが話題になって、もう古いものは不要だと誰かが言い出します。けれど、特定の単語を含む段落を全部選びたい、あるいは一致したノードの親を取りたい、となった瞬間、なぜ lxml が今も別タブで開きっぱなしなのかを思い出すはずです。

lxmlは、20年以上前からある libxml2 のバインディングです。派手さはありません。新しくもありません。ですが、ひとつだけはっきりした役割があります。つまり、本物の XPath が必要な処理です。主流のPython環境で、ここに真正面から競争できるものは他にありません。これは、lxmlが何をして、どこで静かに勝ち、そしてどこでデフォルト設定に気づかないと痛い目を見るのかを、実際に触って確かめたレビューです。

lxmlを一言でいうと何か

lxmlは、Cライブラリ libxml2 と libxslt へのPythonバインディングです。scraperでもbrowserでもなく、parser兼serializerです。マークアップを木構造に変換して検索・編集できるようにし、最後はその木をバイト列に戻します。ElementTree互換のAPI、完全な XPath 1.0 エンジン、XSLT 1.0、スキーマ検証を備え、Stefan Behnel によって "the most feature-rich and easy-to-use library for processing XML and HTML in the Python language" という標語のもとで保守されています。

2026-07-14時点で、GitHubとPyPIのスナップショットから見ると位置づけは次のとおりです。

項目
リポジトリlxml/lxml
Stars3,043
Forks620
Open issues16
LicenseBSD-3-Clause
Created2011-02-11
Last push2026-07-02
PyPI stable6.1.1 (2026-05-18)
Bundled enginelibxml2 2.14.6 + libxslt 1.1.43

先にひとつだけ釘を刺しておきます。このレビューに、隠しネタはありません。lxmlは十分に古いので、ここで触れる挙動はすべて lxml docs や libxml2 の変更履歴、あるいは launchpad のスレッドのどこかに書かれています。未公開の裏技は見つかりませんでしたし、勝手に作るつもりもありません。以下の価値は、新情報だからではなく、体系化され、数値化され、lxmlを主役として整理されていることにあります。

テスト環境(そしてタイミング数値を借りている理由)

このレビューには2種類のデータがあり、それぞれ別の場所から来ています。そこは先に明確にしておきます。

機能テスト──XPathの挙動、2つのparser API、namespaces、encoding、nodeのライフサイクル──は、1台のマシンで新規に実行しました。macOS arm64、Python 3.14.2、lxml 6.1.1、libxml2 2.14.6 です。artifacts/raw/*.json の数値はすべてスクリプト実行で算出したもので、手入力ではありません。機能テストは決定的な真偽値や列挙型なので、1回の実行で十分安定しています。マシンの負荷が変わっても、//a/@href が属性文字列を返すかどうかは変わりません。

実行時間とメモリ使用量の数値は、このパックで新たに計測したものではありません。 以前の selectolax benchmarkパックからそのまま再利用しています。条件は同じマシン、同じ仮想環境、同じ lxml と libxml2 のビルド、benchmark時点は 2026-07-13 です。ここでは再実行していません。これは意図的です。機能スクリプトと並べて timing benchmark を再実行するとCPU競合が起き、再利用した数値が汚れるおそれがありますし、重複作業にもなります。lxml はそのパックで既に十分に計測済みの比較対象でした。同じbenchmarkを使い回すことで、比較条件を揃えたままにできます。なので、下のミリ秒の数値は「同じ試験環境で、2026-07-13時点のもの」と読んでください。「今日、私が再計測した」という意味ではありません。

評価には信頼度タグを付けています。single-observation は決定的な機能テスト、triple-run は再利用した timing 分布、hypothesis は、仕組みとしてはありそうだが切り分けていないものです。

XPath:selectolax と BeautifulSoup にない唯一の強み

これが本題なので、まずここから入ります。

lxml XPath coverage moat with axes predicates and functions

lxml の xpath() に、事前登録した37項目のマトリクスを通しました。各ケースの期待値はテスト実行前にソースへ書き込んであるので、後から甘く採点する余地はありません。10のaxis、9種類のpredicate、10の組み込みfunction、3つのスカラー戻り値型、そして lxml の 1.0エンジンなら拒否すべき XPath 2.0 専用構文のトラップケース5つです。

カテゴリ対象結果
child / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding10/10 pass
述語[1] / last() / position()<n / 属性一致 / 属性の存在 / and / or / ネストした [.//a] / not()9/9 pass
関数text() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string()10/10 pass
戻り値型boolean / number スカラー3/3 pass
トラップケースmatches() / sequences / if-then-else / except / syntax error5/5 正しく拒否

スコアは37/37です。そして重要なのはトラップ列です。matches()、sequence式、if/then/elseexcept はすべて XPath 2.0 の構文ですが、libxml2 の 1.0エンジンはそれらを中途半端に受け付けたりはしません。XPathEvalError を出してきちんと拒否します。つまり、曖昧な穴埋め問題ではなく、壊すつもりで試したうえでの満点です。ここでの挙動はすべて lxml XPath docs に書かれている通りで、それがまさにポイントです。

ひとつだけ、ハーネスの側が間違っていたことも認めておきます。信頼できる「37/37」の方です。//div[.//a[@href]] の期待値を最初は2件と見積もっていましたが、実行結果は1件でした。最初は約30秒ほど lxml を疑ったのですが、fixture を確認すると2つ目の要素は <footer> であって <div> ではありませんでした。間違っていたのは期待値のほうです。修正して、コメントに残しました。責任の向きとしては正しい順番です。まず自分のテストを疑い、20年物のCライブラリをその次に疑うべきです。

XPathとCSS:CSSではそもそも表現できないこと

「XPath のほうが強い」という抽象論だけでは弱いので、差を数値化しました。lxml には .xpath().cssselect() の両方があります(後者は内部でCSSをXPathに変換します)。10個の選択対象を用意して、CSSで本当に表現できるかを確認しました。

XPath expresses seven of ten tasks CSS cannot express

対象XPathCSS (cssselect)
テキスト内容で絞り込む (contains(text(),"bargain"))YesNo text predicate
子要素から親を選ぶ (//b/parent::p)YesNo parent selector
属性値を返す (//a/@href)YesElements only
テキストノードを返す (//p/text())YesNo text nodes
ancestor 軸 (//td/ancestor::div)YesNo upward navigation
子要素数で親を絞る (//ul[count(li)=4])YesNo count predicate
テキスト長で絞る (string-length(text())>5)YesNo length predicate
nth-child / last-child / adjacent siblingYesYes (3 baseline)

10個中7個は、CSSではそもそも表現できません。テキスト内容でのfilter、親や祖先へ戻るnavigation、属性値や素のtext nodeを結果として取り出すこと、数を基準にしたpredicate──CSSはこれらを語れません。両方でいけるのは3つ(nth-childlast-child、隣接兄弟)だけです。これが、「lxml を使うと何が増えるのか」という問いへの定量的な答えです。selectolax はCSS専用で、xpath() メソッド自体がありません。なので、その7種類のクエリは、あちらではPythonの多段ループに分解するか、あるいはやらないかのどちらかです。scrapingロジックがそこに依存しているなら、答えはもう出ています。

(ちなみに、ハーネスはここでも私を捕まえました。string-length(text())>5 は空集合だと予想したのですが、6文字の文字列が2つ一致しました。期待値を直したのであって、ツールを直したわけではありません。)

厳格さの3段階:etree、recover、lxml.html

XPath が lxml を選ぶ理由だとしたら、この3段階の厳格さが、使い続ける理由です。

lxml strictness gears: etree, recover, and lxml.html

多くのparserは、壊れた入力に対して1種類の挙動しか持ちません。lxml は3種類あります。そして十分に予測可能なので、6種類の不正な markupをそれぞれに流し込み、各経路の動作を事前登録しておきました。

不正な入力lxml.etree (strict)etree + recover=Truelxml.html (lenient)
タグ閉じ忘れ <root><a>x</root>raisesrecoversaccepts
入れ子崩れ <b><i></b></i>raisesrecoversaccepts
未定義エンティティ &nbsp;raisesrecoversaccepts
生の & (Tom & Jerry)raisesrecoversaccepts
複数ルート <a>1</a><b>2</b>raisesrecoversaccepts
正常なXMLacceptsaccepts (0 errors)accepts
ブール属性 <input disabled>raisesrecoversaccepts

7件中7件が事前登録の期待どおりでした。lxml.etree は、6種類すべての不正ケースで XMLSyntaxError を投げます。同じparserに recover=True を付けると、エラーを飲み込んで使える木構造を再構築します。そして見落とされがちな点ですが、そのとき parser.error_log が飲み込んだエラーをすべて列挙してくれます。lxml.html は、何事もなかったように全部受け入れます。

「raises / recovers / accepts」を判定する分類器は、ハードコードではなく実行時の error_log の長さで動いています。そのため、recover=True でも正常な文書は空ログなら正しく「accepts」と判定され、「recovers」にはなりません。最初の版では recover=True なら無条件に「recovers」としてしまい、きれいな入力を誤分類していました。実際の error_log を見るようにしたら直りました。

実務上の意味はこうです。壊れたフィードを厳格に落としたいなら lxml.etree。汚れた現実のHTMLを、とにかく通したいなら lxml.html。そして、多くのtoolが苦手な「寛容に読みたい。でも何が壊れていたかは正確に知りたい」という中間解が欲しいなら、recover=True を使って error log を読めばいいのです。selectolax にあるのはこの“寛容”側だけで、厳格モードも error log もありません。

iterparse:selectolax にまったくないストリーミング機能

これは速度の話ではなく、機能の話です。selectolax は文字列全体を一括で読み込むだけで、段階的なインターフェースはありません。lxml の iterparse は、要素が閉じるたびに順に返してくれます。おなじみの fast_iter パターン(elem.clear() と前の兄弟要素の削除を都度行うやり方)と組み合わせれば、文書がどれだけ大きくてもメモリ使用量をほぼ一定に保てます。

lxml iterparse streams 300K records with about 1-2 MB RSS

メモリ特性は直接測りました。ru_maxrss によるピークRSS、各対象を新しいprocessで実行、対象は 300,000 個の <record> 要素で総サイズは約 26.7 MB(26,744,801 bytes)です。

モードピークRSS差分備考
iterparse + clear (fast_iter)約1-2 MB進みながら解放。件数に関係なく平坦
iterparse without clear約386 MB参照を保持するのでフルロード並み
etree.parse (full load, anchor)約386 MB大きさの基準を示すアンカー

bounded モードでは、フルロード時の約386 MBに対して、ピークRSS差分はおよそ1〜2 MBに収まりました。これは0.3〜0.4%程度の差です。しかも最初の record event はファイルの読み込みが終わる前に発火するので、偽の“擬似streaming”ではなく、本当に段階的です。示唆的なのは中央の行です。同じ iterparse ループでも clear() を省くと、保持している参照のせいでメモリはまた約386 MBまで伸びます。勝ち筋は iterparse 単体ではなく、clear() にあります。フルロードのanchorがboundedモードよりはるかに高いことも、RSSメーターがちゃんと差を見分けている証拠です。(このメモリテストは、このパックで新たに実施したものです。footprint測定なので、借用した timing 数値とは別です。)

現場での意味は明快です。RAMに収まらない数ギガバイト級のXML exportを扱うなら、selectolax には逃げ道がありません。lxml の streaming parserを使うか、別の言語に行くかです。

名前空間:RSS、SVG、そしてデフォルト名前空間の罠

3つのnamespaceにまたがるRSS、デフォルトnamespaceと xlink を含む SVG、そしてデフォルトnamespaceのXMLを含めた12ケースを確認しました。12件すべて通過です。

lxml は RSS feed から //dc:creator/text() を正確に ["Alice", "Bob"] として取り出し、同一文書内の3つのnamespaceをまたいで //atom:link/@href//content:encoded を解決し、SVG の2つ目のnamespaceで //s:rect//s:use/@xlink:href を処理し、QName を使って Clark表記の {uri}local 名を分解し、nsmap で内省できます。これは保守され、文書化された挙動であり、selectolax が触れない次元です。selectolax はHTML5専用で、任意のXML namespaces を扱いません。

ただし、ひとつだけ覚えておくべき文書化済みの罠があります。XPath には「デフォルト namespace」という概念がありません。xmlns="urn:..." を宣言している文書に //book を当ててもヒットはゼロです。XPath では空のprefixが未定義だからです。これは lxml docs にも明記されています。人工的なprefixを割り当てる (namespaces={"c": "urn:..."}//c:book とする。これで3件すべて見つかる) か、//*[local-name()='book'] に切り替える必要があります(こちらも3件)。バグではなく XPath仕様が忠実に実装されているだけです。ただ、誰もが一度は引っかかります。

実際の汚れたページでの忠実度:11件の実URLscrape

合成テストはきれいです。webはきれいではありません。selectolaxパックの fixture セットから実際に取得した11ページ(2026-07-10時点、読み取り専用)を再利用し、lxml.html を主役として通しました。

FixtureサイズLinkslibxml2 recovered errorsStrict XML
news_bbc.html398 KB2554ok
docs_mdn_array.html243 KB5080raised
wiki_scraping.html227 KB4600raised
gov_whitehouse.html289 KB1540raised
oldstyle_craigslist.html561 KB3510raised
forum_reddit.html129 KB3180raised
docs_python.html80 KB3412raised
ecommerce_books.html51 KB940raised
news_hackernews.html35 KB2290raised
ecommerce_webscraper_allinone.html16 KB350raised
spa_quotes_js.html6 KB50raised

11件すべてが lxml.html で解析でき、11件すべてでlink数・見出し数・画像数は selectolax パックの再利用済み lxml の数値と一致しました。照合結果は true です。この一致が、再利用が本当に同条件比較であって、同じラベルを貼った別物ではないことを示しています。

副次的に分かったのは、strict XML parserは11件中10件で例外を投げたということです。現実のWebページは、圧倒的に well-formed XML ではありません。だからこそ libxml2 の HTML recovery mode は、そうしたページを飲み込むために存在しています。例外だったのは BBC News で、Next.js でレンダリングされ、厳格なXMLでもなんとか通る程度に整っていました。HTML と書かれているからといって、必ずしも recovery が必要とは限りません。

ひとつ、数え方でつまずきやすい点があります。docs_python.html では、//a[@href](属性の存在を見る)だと343件でしたが、selectolaxパックの if n.get("href")(真偽値として評価する)では341件でした。差の2件は空の href="" リンクです。これは属性が存在するか、属性値が空でないかという、count規約の違いであって、lxml の挙動差ではありません。scrapingでは、空の href を数に入れるかどうかはparserではなく、filterの設計次第です。そこは知っておく価値があります。

バグに見えるけれどバグではない深さ制限

selectolaxパックでは、1,000階層や5,000階層の <div> ネストで lxml が最深部の内容を落とすことを記録しており、「lxml は最深部を静かに失う」とまとめていました。そこで私は仕組みを知りたくて、デフォルトparserと huge_tree=True を比較しました。

lxml default depth guard around 253 levels and huge_tree to 2045

要求深度デフォルトパーサーの到達深度huge_tree=True の到達深度
300253(以降を落とす)299(回復)
1000253(以降を落とす)999(回復)
5000253(以降を落とす)2045(まだ落ちる)

デフォルトparserはおよそ253階層で打ち切り、それより深い部分を黙って捨てます。これはバグではありません。libxml2 による DoS 防御で、およそ256階層のネスト上限があり、敵対的な文書でstackが吹き飛ぶのを防いでいます。この点は XML_PARSE_HUGE に関する lxml launchpad thread でも説明されています。huge_tree=True を設定すると、300階層と1,000階層は完全に戻ります。ですが5,000階層では、huge_tree を有効にしても2,045階層までしか到達しません。つまり、設定可能な上限とは別に、libxml2 側にもっと硬い再帰上限があり、huge_tree ではそれは外れません。

したがって、実務の行動指針は具体的です。信頼できるソースから深いmarkupを解析するなら、lxml.html.HTMLParser(huge_tree=True) を使うこと。このパックが既存の観察に上乗せしたのは、仕組みが「data破損」ではなく「安全上の制限」であること、修正方法が huge_tree であること、そしてその修正でも届かない2段目の上限があることです。

DOMの読み書き、serializer、encoding

lxml は読み取り専用の抽出器ではなく、完全な読み書き可能な木構造です。編集面をケースごとに確認しました。8つのDOM操作はすべて通過しています。SubElementinsertremovereplacestrip_tags(タグだけ外してテキストは残す)、strip_elements(タグとそのテキストを落とす)、drop_treelxml.html 固有)、そして初心者がつまずきやすい text / tail の二枠モデルです。たとえば <p>head<b>bold</b>tail</p> では、p.text"head"b.text"bold"b.tail"tail" になります。

シリアライズは5項目すべて成功しました。XML と HTML モードでの tostring(HTML では void要素が自己閉じしないのが正しい)、pretty_print、C14N 正規化(method="c14n"、これも lxml 固有)、そしてきれいな round-trip きれいな round-trip です。

エンコーディングは、lxml が静かに差をつけるところです。"<p>café éè</p>".encode("latin-1") のような UTF-8 以外のバイト列を lxml.html.fromstring に食わせると、café éè をそのまま復元します。U+FFFD の置換文字もなく、byte落ちもありません。これは、selectolaxパックでの「クリーンな基準値」としての役割を、そのまま再現したものです。同じ入力は他の2engineでは静かに壊れていました(Lexbor は置換文字を出し、Modest はバイトを落としていました)。libxml2 に支えられた lxml の文字コード検出は、ここでは明らかに安定しています。

その反面、encodingをどう宣言するかには厳格です。XML宣言の encoding="latin-1"XMLSyntaxError: Unsupported encoding: latin-1 を投げますが、IANA標準の encoding="ISO-8859-1" なら問題なく解析され、café が返ります。libxml2 はエイリアスではなく、正規化された名前しか受け付けません。これは launchpad #613302 にも昔から書かれている細かな点です。知らなければ面倒ですが、知ってしまえば単純です。

最後にnodeのライフサイクルです。3つの stale-handle シナリオを、それぞれ独立した subprocess で検証しました(もし本当に壊れるなら、非ゼロ終了で分かります)。ツリーが garbage collection された後にnodeを保持する、drop_tree() の後にハンドルを読む、remove() 後にnodeを使う──どれも segfault は起きませんでした。lxml は use-after-free を防ぐために、nodeからtreeへの参照を生かしたままにします。このテストで selectolax も同じく問題ありませんでした。

速度とメモリ(借り物だが、正直にそう書く)

このセクションの数値はすべて、2026-07-13時点の selectolaxパックからの再利用です。このパック自身は timing数値を一切出していません。そこは二度言ってでも明確にしておきます。

指標lxml の値読み方
純粋な parse の p50 (10 MB)77.9 msselectolax-Lexbor より約33〜34%高速
full parse + extract の p50 (1 MB / 10 MB)14.18 ms / 172.9 ms小サイズでは Lexbor とほぼ互角
100kノード CSS スループット3,002,646 nodes/s3つのCエンジンの中で最速帯
10 MB の RSS 差分128.9 MB6つのparserで最も軽量、BeautifulSoup より約1.7倍軽い
import のコールドスタート14.1 msparsel系 import より約2.3倍高速

純粋なparseとスループットの数値は強力で、lxml は計測した6つのparserの中で最もメモリ消費が少ないです。ただし、threadの話には注意書きが必要です。再利用したデータでは、4スレッドの wall-clock speedup は 1.21x にとどまり、評価は「inconclusive」でした。ですが、それは共有デフォルトparserの経路です。lxml FAQ には、各スレッドが自分専用のparser、もしくはコピーしたデフォルトparserを使う場合にのみ、parse中に GIL が解放されると明記されています。共有parserはアクセスを直列化します。正しいやり方のAPI面(XMLParser.copy() の存在、get / set_default_parser の存在、XPathEvaluator が内部ロックを持つこと)は構造的に確認しましたが、スレッドごとのparserにした場合の speedup は計測していません。新しい timing測定になるためですし、このパックはそこまで出していません。なので、1.21x は「素朴な共有経路での値」と読んでください。lxml のthread上限そのものではありません。

また、全体にひとつアスタリスクがあります。これらは単一プラットフォーム、macOS arm64 の数値です。lxml の pure parse が Lexbor より速いという主張は、一般に Lexbor backend のparserが最速だという通説とぶつかるので、本当に確定的と扱う前に Linux x86_64 で再確認したいところです。

ライセンス:地味だが大きい勝利

lxml は BSD-3-Clause ライセンスです。バンドルされる Cライブラリ、libxml2 と libxslt もどちらも MIT です。つまり、どこにも copyleft のない、完全に permissive な構成です。再配布を考えた瞬間に、この違いは効いてきます。対比すると、selectolax の wheel は LGPL-2.1 の Modest と Apache-2.0 の Lexbor を同梱しているので、クローズドな製品に組み込むなら lxml のほうがすっきりしています。

実務上のインストール面でも利点があります。lxml は libxml2 と libxslt を静的リンクした事前ビルド wheel を公開しているため、通常 pip install lxml だけでシステム側の libxml2 や compiler は不要です。sourceからbuildする場合とはかなり違う体験です。

lxml の役割、そして AI抽出レイヤーが引き継ぐ場所

ここで境界をはっきりさせておきます。ありがちなカテゴリ混同を避けたいからです。lxml は parserです。木構造を渡してくれて、query engine は非常に優秀です。ですが、その木の外側は今でもあなたの仕事です。page取得、JavaScript rendering、bot対策の回避、XPathの作成と保守、結果の整形。これはhostedな抽出サービスとは別の層であり、競合というより隣人に近い関係です。

取得・rendering・selection・保守のstackを自分で持ちたくない開発者にとって、その上位レイヤーにあるのが Thunderbit のようなものです。この読者にとって重要なのはbrowser extensionではなく、API、MCP server、CLIです。Thunderbit Open API では、POST /distill でページをきれいな Markdown に変換し、POST /extract で JSON Schema に基づいた structured data を取得できます。renderMode 切り替えと、大量処理向けの batch jobもあります。同じengineは MCP server としても使えます(thunderbit_suggest_fieldsthunderbit_distillthunderbit_extract)。agentやcoding assistant向けです。CLI としては、npx @thunderbit/thunderbit-cli で terminal から直接実行できます。JS rendering、bot対策、CAPTCHA を最初から処理し、schemaに一致したJSONを返します。つまり、parserの上位レイヤーであって、置き換えではありません。

Webデータ抽出で Thunderbit を試す

整理はシンプルです。pipeline全体を自分で所有し、理解している木に対して外科的な XPath 制御が欲しいなら lxml を使う。selector や rendering の保守自体を持ちたくないなら、AI抽出APIを使う。実際には両方を使うsystemも多くあります。自分で管理できる構造化feedには lxml、扱いの難しいロングテールページには抽出service、という使い分けです。

このレビューで検証していないこと

これは暫定レビューであって、最終成績表ではありません。なので、未確認の点も明記しておきます。

timingとメモリの数値はすべて再利用済みで、単一プラットフォーム(macOS arm64、Python 3.14)です。そのパックの注意事項も引き継いでいます。つまり、「lxml は pure parse で速い」という結果は通説と逆なので、Linux x86_64 で再確認が必要です。スレッドごとのparserによる speedup は未検証です(新しい timing が必要)。iterparse のメモリは30万recordで測りましたが、GB級の実XMLでは測っていませんし、HTMLとXMLでの iterparse比較もしていません。長時間のsoak testもしていません。lxml の XSLT 1.0、RelaxNG / XMLSchema / DTD 検証、EXSLT 拡張はここでは完全に未検証です。機能としては大きいですが、今回の範囲はparseとselectionの核心までです。2,045で2つ目の深さ上限は観測しましたが、libxml2 の正確な再帰定数までは特定していません。検証したのは安定版 6.1.1 のみで、7.0.0 alpha は対象外です。Windows、source build、free-threaded 3.14t buildも未検証です。XPath の中でも、組み込みfunctionまでは見ましたが、XPath variable、カスタムPython extension function、事前コンパイルした etree.XPath オブジェクトの再利用は未確認です。

結論

lxml は新しい高速ツールではありません。だからこそ価値があります。これは20年物の libxml2 バインディングで、主流のPythonで競合できる実質的な XPath 1.0 エンジンを持ち、3段階の予測可能な厳格さと途中の error log、メモリに収まらない文書向けの本物の streaming parser、複数namespaceと encoding の正しい扱い、そして完全に permissive なlicenseを備えています。約253階層の深さ制限と共有parser時のthread数値という少し尖った点もありますが、それらは文書化され、設定可能で、いまでは仕組みも説明されました。

scraping pipelineを自分で持ち、XPath を前提にするなら、今でも lxml が第一候補です。selector や rendering を自分で保守したくないなら、Thunderbit API, MCP, CLI のような AI抽出レイヤーの出番です。競争ではなく、役割分担です。どちらにしても、これらの数値は暫定値として扱い、設計書に書く前に自分の環境で timing を再確認してください。

Webデータ抽出で Thunderbit を試す Get Started Free

よくある質問

lxml はウェブスクレイパーですか? いいえ。lxml は parser兼serializerです。libxml2/libxslt へのPythonバインディングで、マークアップを編集・検索可能な木構造に変換します。page取得、JavaScript rendering、bot対策は行いません。request層は requestshttpx、headless browser、または scraping serviceで用意し、そのバイト列を lxml に渡します。

BeautifulSoup や selectolax ではなく、いつ lxml を使うべきですか? XPath が必要なら lxml です。BeautifulSoup は内部で lxml をbackend parserとして使えますが、ネイティブな XPath は持っていません。selectolax はCSS専用で、その狭い領域では速いです。selectionロジックにテキスト内容のfilter、親や祖先への移動、属性やtext nodeの抽出、件数ベースのpredicateが必要なら、それらを直接表現できる主流Pythonの選択肢は lxml の XPath engineだけです。

なぜ lxml は深いネストを静かに落とすのですか? デフォルトparserは、ネストを約253階層で打ち切ります。これは敵対的な文書に対する libxml2 の DoS 防御であって、バグではありません。huge_tree=True(たとえば lxml.html.HTMLParser(huge_tree=True))を設定すると、300階層や1,000階層は完全に回復します。ただし、huge_tree でも外れない約2,045階層付近の、もうひとつ硬い再帰上限がある点には注意してください。

lxml はマルチスレッド解析で GIL を解放しますか? 条件付きです。lxml の FAQ では、各スレッドが自分専用のparser、またはコピーしたデフォルトparserを使う場合にのみ、解析中に GIL が解放されるとされています。共有parserを使うとアクセスは直列化されます。ここで再利用した 4スレッドの speedup 1.21x は、単純な共有parser経路の結果であり、スレッドごとのparserの場合の上限ではありません。後者はここでは計測していません。

2026年でも lxml はまだ保守されていますか? はい。安定版 6.1.1 は 2026-05-18 にリリースされ、リポジトリの最終 push は 2026-07-02 です。7.0.0 alpha も進行中です。GitHub Stars は約3,000あり、下層の libxml2 も活発に保守されているため、過去の遺物ではなく、現在進行形で使えるライブラリです。

Ke
Ke
Thunderbit の CTO | シニアデータサイエンティスト & ML エキスパート 機械学習とデータサイエンスで約10年の経験を持つ Ke Shen は、コロンビア大学の卒業生であり、Walmart Labs の元シニアデータサイエンティストです。Python、R、Java、統計学における深い専門性は同業者からも高く評価されており、理論段階の複雑な AI アルゴリズムを本番運用レベルのアーキテクチャへと落とし込むための、実践に裏打ちされた知見を共有しています。
Topics
Web Scraping ToolsAI Web Scraper
目次
Thunderbit · AIウェブデータエージェント

1クリックであらゆるページからデータを抽出

25万人以上のユーザーに支持されています
無料プランあり
Webページからスプレッドシートへ
欲しい内容を伝えるだけ — ThunderbitのAIエージェントが取得し、Excel、Google Sheets、Airtable、Notionへ出力します。すぐに無料で始められます。
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week