数か月おきに、より高速なHTML parserが出てきては benchmark が話題になり、古いものはもういらないって空気になります。けれど、特定の単語を含む段落を全部拾いたいとか、一致したノードの親要素が欲しいとか、そういう場面になると、なぜ lxml を別タブで開きっぱなしにしているのか、すぐ思い出すはずです。
lxml は、libxml2 に対する20年前からあるバインディングです。派手さはありません。新しくもありません。でも、ひとつだけはっきり得意な仕事――本物の XPath が必要な処理――に関しては、Python の主要どころで真正面から競えるものは他にありません。ここでは、その実力、静かに勝っている場面、そしてデフォルトを知らないと足をすくわれる落とし穴を、実地で検証したレビューとしてまとめます。
lxmlをひと言でいうと何か
lxml は、C ライブラリのlibxml2 と libxslt への Python バインディングです。parser であり serializer であって、scraper でも browser でもありません。マークアップを tree に変換して検索・編集できるようにし、その tree をまた byte 列に戻します。ElementTree 互換の API、完全な XPath 1.0 engine、XSLT 1.0、schema 検証を備え、Stefan Behnel が 「Python language で XML と HTML を処理するための、最も機能豊富で使いやすい library」 というタグラインのもとでメンテナンスしています。
2026-07-14 時点の GitHub と PyPI の snapshot では、位置づけはこんな感じです。
| 項目 | 値 |
|---|---|
| リポジトリ | lxml/lxml |
| Star数 | 3,043 |
| Fork数 | 620 |
| 公開Issue | 16 |
| ライセンス | BSD-3-Clause |
| 作成日 | 2011-02-11 |
| 最終push | 2026-07-02 |
| PyPI安定版 | 6.1.1(2026-05-18) |
| 同梱エンジン | libxml2 2.14.6 + libxslt 1.1.43 |
最初に明言しておくと、このレビューに“隠しネタ”はありません。lxml は十分に古いので、ここで触れる挙動はすべて lxml のドキュメント、libxml2 の changelog、あるいは launchpad のスレッドのどこかに書かれています。未公開の裏技みたいなものは見つかりませんでしたし、でっち上げるつもりもありません。ここでの価値は、新情報であることではなく、整理され、定量化され、lxml を主役として体系化されていることにあります。
テスト環境(そして、time measurement だけは借り物である理由)
このレビューには2種類の data があり、それぞれ出どころが違うので、最初に切り分けておきます。
機能テスト――XPath の挙動、2つの parser API、namespace、encoding、node の lifecycle――は、1台の machine で新規に実行しました。macOS arm64、Python 3.14.2、lxml 6.1.1、libxml2 2.14.6 です。artifacts/raw/*.json に入っている数値は、手入力ではなく script 実行で算出しています。機能テストは決定的な真偽値や enum なので、1回の実行で結果は安定しており、machine 負荷が //a/@href が attribute string を返すかどうかに影響することはありません。
時間と memory の数値は、この pack で新規取得したものではありません。 以前の selectolax benchmark pack からそのまま再利用しており、同じ machine、同じ virtual environment、同じ lxml/libxml2 build、2026-07-13 時点の benchmark です。ここでは再実行していません。これは意図的です。機能テストの束と time benchmark を同時に回すと CPU 競合が起こり、再利用した数値が汚染されかねませんし、二重作業にもなります。lxml は前の pack ですでに十分に測定済みの対照 library だったからです。同じ benchmark を再利用することで、比較条件を揃えたままにでき、微妙に異なる2回目の measurement を持ち込まずに済みます。なので、下に出てくる millisecond の数字は「2026-07-13 時点の同じ test environment」という意味で読んでください。「今日あらためて再測定した」という意味ではありません。
結果には confidence tag を付けています。決定的な機能テストは single-observation、再利用した time distribution は triple-run、まだ切り分けていない mechanism を hypothesis として述べる場合は hypothesis です。
XPath:selectolaxとBeautifulSoupが持っていない唯一のもの
ここが要点なので、最初に触れます。

lxml の xpath() を、事前登録した37項目の matrix で検証しました。各 case の expected value は test 実行前に source へ書き込んであるので、後から甘く採点する余地はありません。10 の axis、9 種類の predicate、10 の built-in function、3 つの scalar return value、そして XPath 2.0 専用の syntax をわざと使った5つの trap case――lxml の 1.0 engine なら拒否するはずのもの――を含めています。
| カテゴリ | 対象 | 結果 |
|---|---|---|
| 軸 | child / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding | 10/10 合格 |
| 述語 | [1] / last() / position()<n / 属性の等価比較 / 属性の有無 / and / or / ネストした [.//a] / not() | 9/9 合格 |
| 関数 | text() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string() | 10/10 合格 |
| 戻り値の型 | boolean / number のスカラー | 3/3 合格 |
| 罠ケース | matches() / sequence / if-then-else / except / 構文エラー | 5/5 正しく拒否 |
スコアは37/37 で、重要なのは trap case の列です。matches()、sequence 式、if/then/else、except はいずれも XPath 2.0 syntax であり、libxml2 の 1.0 engine は中途半端に対応するのではなく、XPathEvalError を投げてきっぱり拒否します。つまり、これは“易しい問題を並べて満点”ではなく、“壊しにいっても満点”です。ここでの挙動はすべて lxml の XPath ドキュメント に書かれている通りで、それこそが重要です。
なお、harness が1回だけ誤判定したことは正直に書いておきます。ちゃんと信用できる37/37 は、その修正後のものです。//div[.//a[@href]] の expected value を最初は2件と見積もっていましたが、実際の結果は1件でした。最初の30秒ほどは lxml が間違っていると思ったのですが、fixture を見直したら2つ目の要素は <footer> で、<div> ではありませんでした。誤りは engine ではなく私の expected value のほうです。source comment にそのまま残してあります。疑う順番としては、まず自分の test、20年ものの C library はその次です。
XPath vs CSS:CSSでは本当に表現できないこと
「XPath のほうが強力」という抽象論だけでは足りないので、差を数値化しました。lxml は .xpath() と .cssselect() の両方を提供します(後者は内部で CSS を XPath に変換します)。そこで10の選択対象を用意し、CSS で実際に表現できるかを確認しました。

| 対象 | XPath | CSS(cssselect) |
|---|---|---|
テキスト内容で絞り込む(contains(text(),"bargain")) | 可 | テキスト述語なし |
子要素から親を選ぶ(//b/parent::p) | 可 | 親セレクタなし |
属性値を返す(//a/@href) | 可 | 要素のみ |
テキストノードを返す(//p/text()) | 可 | テキストノード不可 |
ancestor軸(//td/ancestor::div) | 可 | 上方向の移動不可 |
子数で親を絞る(//ul[count(li)=4]) | 可 | count述語なし |
テキスト長で絞る(string-length(text())>5) | 可 | 長さ述語なし |
nth-child / last-child / 隣接兄弟 | 可 | 可(3つは基本対応) |
10項目のうち7項目には、CSS に相当するものがまったくありません。text 内容の filtering、親や ancestor への上方向 navigation、attribute 値や生の text node を結果として抜き出すこと、数で絞る predicate――CSS では表現できません。両方で使えるのは、nth-child、last-child、隣接 sibling の3つだけです。これが、「lxml を使うと何が得られるのか」に対する定量的な答えです。selectolax は CSS 専用で、xpath() method 自体がありません。なので、上の7種類の query は、selectolax では Python の複数ステップの loop に分解するか、そもそも実行できません。scraping の logic がこれらに依存しているなら、その時点で選択はもう決まっています。
(そして、ここでも harness が2回目の誤りを拾いました。string-length(text())>5 は空集合だと予想していたのですが、6文字の string が2件 hit しました。expected value を直したのであって、tool が間違っていたわけではありません。)
厳格さの3段ギア:etree、recover、lxml.html
XPath が lxml を選ぶ理由だとすれば、3段階の strictness 制御は lxml を“使い続ける”理由です。

多くの parser は壊れた input に対して1つの挙動しか持ちません。lxml は3つあり、しかも予測可能です。そこで6種類の不正な markup をそれぞれ通し、各 path の expected behavior を事前登録して検証しました。
| 不正入力 | lxml.etree(厳格) | etree + recover=True | lxml.html(寛容) |
|---|---|---|---|
タグの閉じ忘れ <root><a>x</root> | 例外 | 復元 | 受理 |
入れ子の崩れ <b><i></b></i> | 例外 | 復元 | 受理 |
未定義エンティティ | 例外 | 復元 | 受理 |
単独の &(Tom & Jerry) | 例外 | 復元 | 受理 |
複数ルート <a>1</a><b>2</b> | 例外 | 復元 | 受理 |
| 正常なXML | 受理 | 受理(エラー0件) | 受理 |
ブール属性 <input disabled> | 例外 | 復元 | 受理 |
7件すべてが事前登録した expected value と一致しました。lxml.etree は、6種類すべての不正 class に対して XMLSyntaxError を投げます。同じ parser に recover=True を付けると、error を飲み込みつつ使える tree を再構築します。そして見落とされがちなのが、parser.error_log に飲み込んだ error がすべて列挙される点です。lxml.html は何も文句を言わずにすべて受け入れます。
「例外/復元/受理」を判定する classifier そのものも、固定値ではなく実行時の error_log の長さに基づいています。だから、recover=True で正常な document を処理した場合は、error log が空なので正しく「受理」となり、「復元」にはなりません。最初の実装では recover=True の結果を全部「復元」としてしまい、きれいな input を誤 label していました。実際の error_log を見るようにしたら直りました。
実務上の意味は明快です。壊れた feed をきっちり失敗させたいなら lxml.etree。汚れた現実の HTML をとにかく通したいなら lxml.html。そして、多くの tool が苦手とする中間――「寛容に受け入れつつ、何が壊れていたかを正確に知らせてほしい」――には recover=True と error_log の参照が効きます。selectolax には寛容モードしかなく、厳格モードも error log もありません。
iterparse:selectolaxにはないストリーミングギア
ここは速度の話ではなく、機能の話です。selectolax は1つの string 全体を読み込むだけで、段階的な interface はありません。lxml の iterparse は、要素が閉じられるたびに順次返します。定番の高速化 pattern(elem.clear() を呼び、前の sibling 要素を削除しながら進む)と組み合わせると、document がどれだけ大きくなっても memory 使用量をほぼ一定に保てます。

memory 特性は直接計測しました。ru_maxrss による peak RSS を、各対象ごとに新しい process で測り、合計約15MBの300,000個の <record> 要素を使っています。
| モード | ピークRSS増加 | 備考 |
|---|---|---|
iterparse + clear(fast_iter) | 約1〜2 MB | 進みながら解放。件数に関わらずほぼフラット |
iterparse + clearなし | 約386 MB | 参照を保持するため、フルロード級 |
etree.parse(フルロード、基準) | 約386 MB | 重いことを示す比較基準 |
制限付き mode では、フルロードの約386MB に対して peak RSS 増加を約1〜2MB に抑えられます。これは0.3〜0.4%程度の差です。しかも最初の record event は file の読み込みが終わる前に発火するので、擬似 streaming ではなく本物の逐次処理です。学びが大きいのは真ん中の行です。同じ iterparse loop でも clear() を飛ばすと、すべてへの参照を保持するため、memory はまた約386MB まで膨らみます。勝ち筋は iterparse そのものではなく clear() にあります。さらに、フルロードの基準が制限付き mode よりかなり高く出ていることで、RSS 計測器が差の大きさをちゃんと見られていることも確認できます。(この memory test はこの pack で実施したもので、借り物の time measurement とは別の footprint 測定です。)
実世界での意味はこうです。RAM に収まらない multi-gigabyte の XML export には、selectolax では対応できません。必要なのは lxml の streaming parser か、別 language です。
名前空間:RSS、SVG、そしてデフォルト名前空間の落とし穴
12件の namespace case を検証しました。RSS では3つの namespace をまたぎ、SVG では default namespace と xlink を含み、さらに default namespace 付き XML も対象です。12件すべて合格しました。
lxml は RSS feed から //dc:creator/text() を正確に ["Alice", "Bob"] として取り出し、同一 document 内の3つの別 namespace にまたがる //atom:link/@href と //content:encoded を解決し、SVG の2つ目の namespace では //s:rect と //s:use/@xlink:href を扱い、QName で Clark 記法の {uri}local 形式を分解し、nsmap で introspection できます。これらはメンテされ、document 化された挙動であり、HTML5 専用で任意の XML namespace を扱わない selectolax にはまったくない次元です。
ただし、1つだけ必ず覚えておくべき落とし穴があります。XPath には default namespace という概念がありません。xmlns="urn:..." を宣言した document に対して //book を投げても、hit は0件です。XPath では空の prefix は未定義だからです。これは lxml のドキュメント にも明記されています。対処法は、人工的な prefix を bind する(namespaces={"c": "urn:..."} で //c:book と書けば3件すべて見つかる)か、//*[local-name()='book'] に落とすかです(これも3件)。bug ではなく、XPath specification がそのまま実装されているだけです。とはいえ、誰でも一度は驚きます。
実際の汚れたページでの再現性:11件の実スクレイプ
Synthetic な test はきれいですが、Web はそうではありません。selectolax pack の fixture set から11ページの実 capture を再利用し(2026-07-10 時点、read-only)、lxml を主役として lxml.html で通しました。
| fixture | サイズ | リンク数 | libxml2が復元したエラー数 | 厳格XML |
|---|---|---|---|---|
| news_bbc.html | 398 KB | 255 | 4 | ok |
| docs_mdn_array.html | 243 KB | 508 | 0 | 例外 |
| wiki_scraping.html | 227 KB | 460 | 0 | 例外 |
| gov_whitehouse.html | 289 KB | 154 | 0 | 例外 |
| oldstyle_craigslist.html | 561 KB | 351 | 0 | 例外 |
| forum_reddit.html | 129 KB | 318 | 0 | 例外 |
| docs_python.html | 80 KB | 341 | 2 | 例外 |
| ecommerce_books.html | 51 KB | 94 | 0 | 例外 |
| news_hackernews.html | 35 KB | 229 | 0 | 例外 |
| ecommerce_webscraper_allinone.html | 16 KB | 35 | 0 | 例外 |
| spa_quotes_js.html | 6 KB | 5 | 0 | 例外 |
11件すべてが lxml.html で解析でき、リンク数・見出し数・画像数は、selectolax pack で再利用していた lxml の数値と11件すべてで一致しました。cross-check は true です。この一致があるからこそ、再利用した数値が本当に同条件比較であって、同じ label を貼った別 measurement ではないと判断できます。
副次的な発見として、厳格な XML parser は11件中10件で例外を投げました。実際の Web page は、ほとんどが整形式 XML ではありません。だからこそ、libxml2 の HTML 復元 mode が存在するのです。唯一の例外は BBC News で、Next.js で render されており、厳格 XML parse にも耐えられるほど整っていました。HTML と書かれていても、必ずしも復元 mode が必要とは限りません。
数え方でつまずきやすい点も1つあります。docs_python.html では、//a[@href](attribute の存在)で343件だったのに対し、selectolax pack の if n.get("href")(truthy 判定)は341件でした。追加の2件は空の href="" link です。これは attribute が存在するかどうかと、attribute value が空でないかどうかの違いであって、lxml の挙動差ではありません。predicate を揃えれば数は一致します。scraping では、空の href を数に入れるかどうかは parser ではなく filter の設計次第だと覚えておくと役立ちます。
バグに見えるけれど、バグではない深さ制限
selectolax pack では、1,000階層と5,000階層のネストした <div> markup で lxml が最深部の内容を落とすことが記録されており、「lxml が静かに最深部を失う」と表現されていました。仕組みを知りたかったので、default parser と huge_tree=True を比較しました。

| 要求深度 | デフォルトパーサーが到達 | huge_tree=True が到達 |
|---|---|---|
| 300 | 253(以降を破棄) | 299(復元) |
| 1000 | 253(以降を破棄) | 999(復元) |
| 5000 | 253(以降を破棄) | 2045(それでも破棄) |
デフォルト parser は約253階層で打ち切り、それより深い部分は静かに捨てます。これは bug ではなく、libxml2 の DoS 対策です。攻撃的な document が stack を吹き飛ばすのを防ぐためのおよそ256階層の nesting 上限であり、XML_PARSE_HUGE に関する lxml の launchpad スレッド にも説明があります。huge_tree=True を設定すると、300 や 1,000 の深さは完全に戻ります。とはいえ、深さ5,000では huge_tree を有効にしても到達点は2,045までで、もう1段上の、より硬い libxml2 の recursion 上限があり、huge_tree ではそこは解除されません。
なので、対処は具体的です。信頼できる source から非常に深い markup を読むなら、lxml.html.HTMLParser(huge_tree=True) を使ってください。この pack で新たに付け加えたのは、仕組みが data corruption ではなく safety 上の上限であること、修正は huge_tree であること、そしてその修正でも届かない第2の上限があることです。
DOMの読み書き、シリアライズ、エンコーディング
lxml は読み取り専用の extractor ではなく、完全な read-write tree です。編集面も case ごとに確認しました。8つの DOM 操作はすべて通過しました。SubElement、insert、remove、replace、strip_tags(tag だけ消して text は残す)、strip_elements(tag も text も消す)、drop_tree(lxml.html 専用)、そして新規利用者がつまずきやすい text/tail の2 slot model です。たとえば <p>head<b>bold</b>tail</p> では、p.text は "head"、b.text は "bold"、b.tail は "tail" になります。
シリアライズも5項目すべて合格しました。XML と HTML mode の tostring(HTML では void 要素が自己終了されず正しく出力される)、pretty_print、C14N 正規化(method="c14n"、これも lxml 専用)、そしてきれいな往復変換です。
encoding では、lxml の地味な強さが出ます。非 UTF-8 byte 列――たとえば "<p>café éè</p>".encode("latin-1") を lxml.html.fromstring に渡す――でも、café éè をそのまま復元し、U+FFFD の replacement character も、byte 落ちもありません。これは selectolax pack での「基準実装」の役割をそのまま再現しており、同じ input は他の2 engine では静かに壊れていました(Lexbor は replacement character を出し、Modest は byte をそのまま落としました)。libxml2 ベースの character set 推定は、この点では単純に安定しています。
反対側には、「どう宣言したか」に厳しいという面があります。XML 宣言で encoding="latin-1" と書くと XMLSyntaxError: Unsupported encoding: latin-1 が出ますが、IANA 正規名の encoding="ISO-8859-1" なら問題なく解析され、café が返ります。libxml2 は別名ではなく正規名しか受け付けません。これは launchpad #613302 にもある仕様です。知らないと面倒ですが、知ってしまえば単純な話です。
最後に node の lifecycle です。3つの stale handle シナリオを分離した sub process で実行しました(hard crash なら非0終了になるはずです)。tree が garbage collected された後に node を保持する、drop_tree() の後に handle を読む、remove() 後に node を使う――いずれも segfault は起きませんでした。lxml は use-after-free を防ぐため、node から tree への参照を生かし続けます。この test では selectolax も同じく問題なしでした。
速度とメモリ(借り物だけど、正直にそう言う)
この節の内容はすべて selectolax pack からの再利用で、2026-07-13 時点のものです。この pack 自身は time measurement を1つも生んでいません。何かを再測定したと誤解されるくらいなら、同じことを2回言ってでもはっきりさせます。
| 指標 | lxmlの値 | 読み取り |
|---|---|---|
| 純粋なparseのp50(10 MB) | 77.9 ms | selectolax-Lexborより約33〜34%高速 |
| parse+extractのp50(1 MB / 10 MB) | 14.18 ms / 172.9 ms | 小さいサイズではLexborとほぼ互角 |
| 10万ノードのCSSスループット | 3,002,646 nodes/s | 3つのC engine の中で最速帯 |
| 10 MBでのRSS増加 | 128.9 MB | 6パーサー中で最軽量、BeautifulSoupより約1.7倍軽い |
| インポートのコールドスタート | 14.1 ms | parsel系の import より約2.3倍高速 |
純粋な parse と throughput の数値はかなり強く、memory 効率も6パーサー中で lxml が最良です。ただし、thread に関しては注意が必要です。再利用 data では4 thread の wall-clock speedup は1.21倍にとどまり、結論は保留でした――ただしそれは共有デフォルト parser を使う経路です。lxml の FAQ には、各 thread が自分専用の parser(またはコピーした default)を使うときだけ parse 中に GIL が解放され、共有 parser では access が直列化されると明記されています。正しく使うための API 面は構造的に確認しました(XMLParser.copy() はあり、get/set_default_parser もあり、XPathEvaluator は内部 lock を持ちます)。ただし、thread ごとの parser を使ったときの speedup は測定していません。新しい time measurement になってしまうからです。この pack ではそこまでは作っていません。なので、1.21x は「素朴な共有経路での結果」であって、lxml の thread 上限そのものではない、と読んでください。
そして、すべてに footnote を付けておくべき点があります。これらは単一 platform、macOS arm64 の数値です。lxml の純粋な parse が Lexbor を上回るという主張は、一般的な「Lexbor ベースの parser が最速」という認識とぶつかるため、誰かが確定事項として扱う前に Linux x86_64 での再確認が本当に必要です。
ライセンス:地味だけど強い勝利
lxml の license はBSD-3-Clauseで、同梱される C library の libxml2 と libxslt もどちらも MIT です。これは完全に permissive な license chain で、copyleft は一切含まれません。再配布する瞬間にこの差は効きます。対照的に、selectolax の wheel は LGPL-2.1 の Modest と Apache-2.0 の Lexbor を同梱しているため、closed な product に入れるなら lxml のほうが話がすっきりしています。
実務上の install 面でも利点があります。lxml は libxml2 と libxslt を static link した prebuilt wheel を配布しているので、通常は pip install lxml だけで、system の libxml2 も compiler も不要です。source から build する場合とは体験が違います。
lxmlの居場所、そしてAI抽出レイヤーが引き継ぐ場所
ここで境界をはっきりさせておきます。混同しやすいからです。lxml は parse 用 library です。tree と優秀な query engine を渡してくれますが、その tree の周辺はすべて自分の仕事です。page の取得、JavaScript の rendering、bot 対策の突破、XPath の作成と保守、結果の整形――これらは別 layer です。hosted な extraction service とは競合というより隣り合わせです。
取得・rendering・selection・保守を自分で持ちたくない developer にとって、その上位 layer にあるのが Thunderbit のようなものです。この文脈では、browser extension ではなく API、MCP server、CLI が主役です。Thunderbit Open API では POST /distill で page をきれいな Markdown に変換し、POST /extract で JSON Schema に沿った structured data を抽出できます。renderMode の切り替えや、件数向けの batch job にも対応しています。同じ engine は MCP server (thunderbit_suggest_fields、thunderbit_distill、thunderbit_extract) として agent や coding 支援 tool から使えますし、npx @thunderbit/thunderbit-cli で terminal から直接実行できる CLI もあります。JS rendering、bot 対策、CAPTCHA を最初から処理し、schema に合った JSON を返します。つまり、parse の上にある layer であって、置き換えではありません。
整理すると、選び方はシンプルです。pipeline を自分で持ち、理解している tree に対して外科手術のように XPath を使いたいなら lxml。selector や rendering を一切保守したくないなら、AI extraction API を使うのがよいでしょう。実際の system では両方を使うことも多く、管理可能な structured feed は lxml、手に負えない long tail page は extraction service、という住み分けがよくあります。
このレビューで扱っていないこと
これは暫定レビューであって最終採点表ではないので、未検証の点も明示しておきます。
time と memory の数値はすべて再利用 data で、単一 platform(macOS arm64、Python 3.14)由来であり、その pack の注意事項を引き継いでいます。つまり、「lxml は純粋な parse で速い」という結果は常識に逆らっているので、Linux x86_64 での再確認が必要です。thread ごとの parser を使ったときの speedup は未検証です(新規の time measurement が必要になります)。300k record で iterparse の memory は測りましたが、GB級の実 XML では測っていませんし、HTML と XML での iterparse 比較も、多時間の soak test もしていません。lxml の XSLT 1.0、RelaxNG / XMLSchema / DTD 検証、EXSLT 拡張はここでは完全に未検証です。かなり広い機能範囲ですが、parse と selection の中核からは外れます。深さ制限の第2上限は2,045まで観測しましたが、libxml2 の正確な recursion 定数までは特定していません。検証したのは安定版6.1.1のみで、7.0.0 alpha は対象外です。Windows、source build、free-threaded の 3.14t build も未検証です。XPath 自体についても、built-in function までは見ましたが、XPath variable、Python 製の extension function、事前 compile した etree.XPath object の再利用までは扱っていません。
結論
lxml は新しくて速い話題の tool ではありません。だからこそ、推奨できます。これは20年ものの libxml2 binding で、mainstream な Python 代替が誰も一致できない完全な XPath 1.0 engine を備え、3段階の予測可能な parse strictness mode とその中間の error log、memory に収まらない document 向けの本物の streaming parser、複数 namespace と encoding の正しい処理、そして完全に permissive な license を持っています。気をつけるべき鋭い点――約253階層の depth 制限と共有 parser での thread 数値――も、document 化され、調整可能で、ここで仕組みまで説明しました。
scraping の pipeline を自分で持ち、XPath を軸にするなら、今でも lxml が最初の選択肢です。selector や rendering の保守を抱えたくないなら、Thunderbit API、MCP、CLI のような AI extraction layer の出番です。競争というより、役割分担です。どちらを使うにしても、ここで示した数値は暫定と考え、自分の environment で time measurement を再確認してから design 書に引用してください。
Webデータ抽出にThunderbitを試す Get Started Free
FAQ
lxmlはWebスクレイパーですか?
いいえ。lxml は parser 兼 serializer です。libxml2/libxslt への Python バインディングで、マークアップを編集・検索可能な tree に変換します。page 取得、JavaScript rendering、bot 対策は行いません。request 層(requests、httpx、headless browser、あるいは scraping service)を別途用意し、その byte 列を lxml に渡します。
BeautifulSoupやselectolaxではなく、いつlxmlを使うべきですか? XPath が必要なときは lxml です。BeautifulSoup は backend parser として lxml を使うことはありますが、native な XPath は提供しません。selectolax は CSS 専用で、その狭い領域では高速です。text 内容の filtering、親・祖先への移動、attribute や text node の抽出、数で絞る predicate が必要なら、それらを直接表現できるのは Python の主流では lxml の XPath engine だけです。
なぜlxmlは深くネストした内容を静かに落とすのですか?
default parser が約253階層で nesting を制限しているからです。攻撃的な document に対する libxml2 の DoS 対策であって、bug ではありません。huge_tree=True(たとえば lxml.html.HTMLParser(huge_tree=True))を設定すると、300階層や1,000階層は完全に復元できます。ただし、huge_tree でも解除できない、約2,045階層付近の別の recursion 上限がある点には注意してください。
lxmlはマルチスレッド解析でGILを解放しますか? 条件付きです。lxml の FAQ では、各 thread が自分専用の parser、あるいはコピーした default parser を使う場合に限って、parse 中に GIL が解放されるとされています。共有 parser では access が直列化されます。再利用した4 thread の speedup 1.21倍は、素朴な共有 parser 経路の結果であって、thread ごとの parser の上限ではありません。後者はここでは測定していません。
2026年でもlxmlはまだメンテナンスされていますか? はい。安定版 6.1.1 は 2026-05-18 にリリースされ、リポジトリの最終 push は 2026-07-02、さらに 7.0.0 alpha も進行中です。GitHub Star は約3,000あり、下層の libxml2 も活発に保守されているため、legacy ではなく現役の、しっかりサポートされた library です。


