Rustの話です。
XMLのサポートを謳うクレートは数多あるのですが、その中でマトモなものはどれなんでしょうという話です。
普通に考えれば使用実績の多いクレートがマトモそうに思えるのですが、本当にそうでしょうか?
ということで、色々探してみた結果の私見です。
どうなっていれば「マトモ」か?
とりあえず周辺規格は置いておいて、XML仕様に適合しているか否かをベースに見てみることにします。
バグ起因の仕様違反は見逃しましょう。そんなものまで目くじらを立てていては、おそらく「マトモ」な実装なんてこの世にはないでしょう。明らかに処理をサボっているとかだけを対象にします。
「DTDによる妥当性検証をしないこと」は「マトモではない」わけではありません。XML仕様においても"non-validating processor"という括りでの適合性が述べられているので、実装上サボっても特に問題はありません。
ただし「DTDを読み飛ばすこと」は、その読み飛ばし方によってはマトモとは見做せません。DTDの整形式性制約を検査することや、DTDから読み取った属性宣言によって属性値を正規化することや既定値を提供すること、内部実体宣言によって実体参照を置換することは非検証プロセッサでも必須です。
とはいえ、きょう日DTDをマトモに処理できなくても困る人はあんまりいないでしょうし、整形式性制約違反の報告はともかくとして、属性値正規化や属性既定値の提供、実体参照の展開は最悪できなくても、目を瞑れる部分ではあります。
実体参照の展開を真面目にやろうとすると、まずzero-copy/zero-allocationにはなり得ません。文字参照の展開や行末正規化や属性値の正規化も、工夫すればzero-allocationにはなるでしょうが、zero-copyにはなり得ません。
zero-copyやzero-allocationを謳うクレートはこのあたりをサボっていると見てまず間違いなく、その意味で完璧に仕様適合したXMLプロセッサではないでしょう。
もちろん、私なんぞでは思いつきもしない手段でそれを達成する仕様適合プロセッサが存在する可能性もありますが。
使用実績の多いクレート
crates.ioでkeywordに"xml"を持つクレートをダウンロード数の多い順で探してみます。
https://crates.io/keywords/xml
ダウンロード数の多い順といっても、全期間のダウンロード数と最近のダウンロード数の2つの基準があります。ここでは最近のダウンロード数が多い順に並べます。
上位から独断と偏見で何個か選びますが、ここではquick-xml, xml, roxmltree, libxmlあたりを見てみましょう。
xmlparserは最終更新が3年前と古く、READMEではトークナイザであると名乗っているように見えるので外しました。
xml-rsはxmlに移行しているので、外したというかxmlを見るので十分です。
逆にlibxmlは最上位というわけでもないのですが、そこは独断と偏見です。
quick-xml
圧倒的によく使われているクレートです。とにかく速いことがウリで、またserdeによる直列化のサポート、encoding_rsによる非UTFエンコーディングのサポート、async/awaitサポートなどがあります。
パーサはStAXパーサのみが提供されていて、SAXパーサはありません。また、文書木もありません。
名前空間はサポートしているようです。パーサのオプションで切り替えるのではなく、パーサが名前空間サポート用と非サポート用で分かれています。
roxmltreeのリポジトリにベンチマーク比較用のコードが置いてあり、その中にquick-xmlも入っているのですが、群を抜いて圧倒的に速いのがわかります。
ただこの速さは整形式性制約の検査をサボることによって実現されています。つまり、XMLとして正しくない文書に対してエラーを出さないことがあります。
この中で特にパフォーマンス上顕著なのが、文字がXMLで規定された範囲に含まれるかの検査です。UTF-8のデコードは重いため、XMLプロセッサを実装する上でボトルネックになるのですが、quick-xmlは一切検査をせずに読み飛ばすため速いわけです。
結果として、quick-xmlは以下のようにXMLではないXMLっぽい文書をエラーなく通します。
use quick_xml::{Reader, events::Event}; const NOT_XML: &str = "<\0\0\0 \0\0='\0\0\0'>\0\0\0\0</\0\0\0>"; fn main() { let mut reader = Reader::from_str(NOT_XML); while !matches!(reader.read_event().unwrap(), Event::Eof) {} }
NULL文字はXML1.0でも1.1でも、名前文字はおろか文字ですらないので、上のコードのNOT_XMLはXMLではありません。XML仕様に適合するために整形式性制約エラーの報告はMUSTであるため、この点では全くマトモではありません。
たぶん、速さに魅力を感じて使う人が多いクレートなのだと思いますが、確実に整形式であることが分かりきっているXML文書以外に使うのはおすすめしません。
ちなみに、DTDの検証もしませんし、実体参照の展開、行末正規化、属性値の正規化、属性既定値の提供も一切しません。
以上のことから考えると、XMLプロセッサというより、トークナイザに近いような気がします。
xml
もともとxml-rsという名前のクレートでしたが、v1.0.0を迎えるあたりでxmlに改称したようです。
xml-rsはv1.0.0で止まっているので、xml-rsを使っていた方はバージョンを上げる際にxmlに変更する必要があります。また、新規に採用する場合はxml-rsでなくxmlを採用するべきです。
たぶん老舗のXMLクレートなのでしょう。quick-xmlのREADMEには"Syntax is inspired by xml-rs"と書いてあるので、少なくともquick-xmlよりは以前からあったクレートだと思います。
quick-xmlと同様StAXパーサのみを提供しています。quick-xmlのパーサはIteratorを実装していませんが、xmlのパーサはIteratorを実装しているため、forでループできます。
quick-xmlはzero-copyを実現するためにライフタイムとのせめぎあいをした結果Iteratorを実装できなかった一方、xmlはイベントごとに文字列をクローンしているので、普通にIteratorを実装できます。
JAXPでいうところのStreamReaderとEventReaderの関係にあるようなものでしょうか?Javaに詳しくないので合っているかわかりませんが。
名前空間はデフォルトでサポートされていますが、無効にすることもできません。
serdeサポートや非同期サポートなどはなく、純粋にパーサ(とライタ)のみを提供するクレートです。高機能・多機能を要求するなら他のクレートを探したほうがよいかもしれません。
パフォーマンスはかなりひどいもので、先述のroxmltreeクレートのベンチマークによると、quick-xml比で数倍〜数十倍遅いです。
私が自作のXMLプロセッサのベンチ目的で数百MB程度のXMLのパース速度を比較した際は、quick-xmlと100倍近い差が開いていました。(自作のXMLプロセッサはxmlより50倍速かったです)
整形式性制約の検査についていえば、W3CのXML Conformance Test Suitesをある程度は通しているようなので、一定レベルの適合性は期待できそうです。
妥当性検証はサポートしない、とREADMEに明記されているので、妥当性検証に関するテストはスキップしているのでしょう。
非UTFエンコーディングはサポートしないとも記載があるので、EBCDICのような古いエンコーディングや、latin-1を除くISO-8859シリーズ、Shift_JISやEUC、ISO-2022シリーズに関連するテストもスキップしているでしょう。
実体参照は、内部実体のみ展開するとREADMEに記載があります。実際試してみると、正しく展開されているように見えます。
属性リスト宣言は保持しない作りになっているようなので、属性型に応じた属性値正規化や属性既定値の提供はしません。
以下のようなコードを書いて試してみると、内部実体の内容に応じて"ent"が要素として認識されますし、全属性型に共通な属性値の正規化は行われています。一方で、属性"att"の既定値は提供されていませんし、属性"id"はID型に従った属性値正規化がされておらず、空白文字が残っています。
use xml::EventReader; const XML: &str = "<!DOCTYPE root [<!ENTITY ent1 '<ent>internal</ent>'><!ATTLIST root id ID REQUIRED><!ATTLIST root att NMTOKEN 'nmtoken'><!ELEMENT root ANY>]><root id='\troot\n'>chardata\rnewline&ent1;</root>"; fn main() { let reader = EventReader::new(XML.as_bytes()); for ev in reader { println!("{ev:?}"); } }
以上のことから考えると、quick-xmlと比べるとマトモなXMLプロセッサと言えると思います。
あまりにも遅すぎることや、機能が若干貧弱なのが微妙ではあるものの、巨大文書を読むとかでなければ満足できるでしょう。
roxmltree
先程からベンチマークの話で名前が出てきているクレートですが、これはquick-xml, xmlと異なり、文書木を組み立てるパーサです。
文書の内容をすべて保持するので、巨大文書ではメモリの使用量がとんでもないことになりそうです。理論上はSAXパーサやStAXパーサと比べてその点不利ですが、quick-xmlやxmlがどういう文書ストリームの読み方をしているのかは見ていないので、それらと比べてどうなるかは不明です。
名前空間はデフォルトでサポートされており、xmlと同じく、無効にはできないように見えます。
READMEを見る限り、属性値正規化、実体参照の展開(内部・外部問わず)はできると書いてあります。
実体参照については、試してみると確かに展開できるようです。ちょっとEntityResolverの使い方がわからずめちゃくちゃになっていますが、以下のようなコードで試せます。
use roxmltree::{Document, ParsingOptions}; const EXT_ENT: &str = "<!DOCTYPE root [<!ENTITY ent SYSTEM 'ext.ent'>]><root>&ent;</root>"; fn main() { let doc = Document::parse_with_options( EXT_ENT, ParsingOptions { allow_dtd: true, entity_resolver: Some(&|_pubid, sysid| match sysid { "ext.ent" => Ok(Some("internal")), _ => Ok(None), }), ..Default::default() }, ) .unwrap(); println!("{doc:?}"); }
ただ、リポジトリに置いてあるparsing.mdを読むと、DTDにおいて認識するのは実体宣言だけとのことなので、属性型に対応する属性値正規化は行われません。
上のコードのEXT_ENTの中身をxmlのコード例のXMLに置き換えて試すと分かりますが、ID型属性値の前後の空白は除去されません。
READMEの比較表ではlibxml2と同様にできるような書き方になっていますが、同等の動作にはなりません。そこは注意が必要です。
エンコーディングのサポートについてはxml以上に貧弱で、非UTFエンコーディングどころか、非UTF-8エンコーディングは一切サポートしないとREADMEに書いてあります。
XML仕様は、すべてのXMLプロセッサはUTF-8、UTF-16でエンコーディングされた実体を読み取れなければならない、と規定しているので、その点では仕様を満たしていません。
整形式性制約の遵守度合いについては、xmlのようにW3Cのテストスイートを通しているわけではないので、なんとも言えません。
quick-xmlのような、文字種の検査すらサボるということまではしていないようです。
しかし、DTDの読み飛ばしはダメなようです。マークアップ宣言であるか、それが整形式であるかのチェックをロクにせずに読み飛ばしているので、例えば以下のような文書は整形式であるにも関わらず、パースを試行するとパニックして落ちます。
const CORRECT_DTD: &str = "<!DOCTYPE root [<!ATTLIST root att CDATA '>'>]><root/>"; // mainの中身は上のコードと同じ
属性値に">"をそのまま含むのは完全に合法ですから、通らないのはダメです。もちろん、xmllintのようなツールで試せば普通に整形式文書として検査をパスできます。
割とこの手のマークアップ宣言を正しく読み飛ばしできなくて落ちる雑な実装は多くて、ついこの間までquick-xmlも同じような問題を抱えていました。
この点で言えば、roxmltreeもマトモとは言い難い実装なのかもしれません。
有名所のquick-xmlやxmlが文書木構築用のAPIを持っていないので、DOMみたいな操作がしたいときには重宝するかもしれませんが、信頼できない文書をパースするのに使ってもいいのかは微妙なところです。
libxml
最後はlibxmlです。名前から察せられますが、C言語で書かれた有名なXMLライブラリであるlibxml2のRustバインディングです。
枯れに枯れきったライブラリのバインディングですので、仕様適合性という意味で言えば、上の3つのライブラリよりも断然適合性が高いです。
パフォーマンスについても、素のlibxml2はquick-xmlより2倍遅いかどうかという程度なので、全く満足できる水準だと思います。
バインディング側ではまだすべての機能を用意できているわけではないようで、XMLプロセッサのほか、XSD、XPathプロセッサは用意がありますが、XSD以外のスキーマ(RELAX NG, Schematron)やXInclude, XPointer, Catalog, C14Nなど、素のlibxml2に存在する多数の機能が使えません。
ちょっと面倒なところとしては、システムライブラリとしてlibxml2をインストールしておかないとならないことでしょう。
まあ最初の一回面倒なだけではありますが、他のクレートならcargo addするだけで済むクレートのインストールなのに、追加でシステムライブラリも用意しなければならないというのは面倒なものです。
また、ちょっと前には大元のlibxml2の主要メンテナが辞任してしまうという事件もあり、開発体制が大丈夫かという心配もあったりはしますが、現在は新しいメンテナの元ちゃんと開発が継続しているらしいと噂に聞くので、その点はたぶん大丈夫でしょう。
それ以外のクレート
ここまでは使用実績が多めのクレートから選びましたが、ここからは更に独断と偏見で数個だけ、新興のクレートから選んでみます。
私が最近気になっている、新興で実装がマトモそうなXML操作クレートはxmloxide, uppsala, fastxmlの3つあります。
どれも出てきたのはここ1年以内くらいで、libxml2並に多機能・高パフォーマンスを目指しているとともに、XML仕様への適合性にある程度気を使って実装されているように見えます。
実績アリなクレートから選ぶならlibxmlをおすすめしたいところなのですが、ピュアなRust実装がいいよ〜という場合は、この3つから探してみると幸せになれるんじゃないかなあというのが今の所の私見です。
xmloxide
libxml2を純粋にRustだけで再実装するよというのがこのクレートの目標のようです。今の時代にありがちですが、かなりAIを使い込んで実装されているらしいと聞きます。
ちなみに、xml_oxideという瓜二つの名前のクレートも存在しており、これはxmloxideとは別物です。xml_oxideのほうが昔からあるクレートですが、長いことメンテナンスされていないようです。
わざわざlibxml2_compatという名前のテストを組み込むくらいにはlibxml2との互換性に気を使っているようで、C言語側からxmloxideを使えるようなバインディングが用意され、WASMやPythonからも使えるように実装されています。
しかしlibxml2の機能だけにこだわるわけでもなく、HTML用のモジュールがHTML Living Standardに対応していたり、libxml2にはないCSSセレクタによる文書木操作もできるようになっていたりと、このクレートなりの進化も目指しているようです。
仕様適合性という点では、W3Cテストスイートをすべて通しているように見えるので、高い適合性を持っているのではないかと思われます。
これまで挙げたクレートはlibxmlを除いてすべて、妥当性検証をサポートしない実装になっていましたが、このクレートは妥当性検証においてもW3Cテストスイートを通しています。
パーサはlibxml2と同様、SAX、SAX(push type)、StAXとあり、文書木も扱えます。
注意点として、Push type SAXパーサはlibxml2のそれと異なり、外観上はメモリチャンクを少しずつパーサに読ませているように見えるのですが、その実パーサ内部で文書ストリームがすべて揃ったら初めてパースするという形になっているので、全くメモリの節約にならないどころか、メモリ無駄遣いになっている可能性すらあります。本来は、読んだチャンクをインクリメンタルにイベントに分解してパースし、パースできた部分は捨てるという形でメモリを削減するものだと思うのですが、なんでそうなっていないのでしょうか。
内部実装をちゃんと読んでいないのですが、この感じだとSAXパーサなども、文書ストリームを一旦全部かき集めてからパース開始するという、無駄な実装になっている可能性もあるのかもしれません。
ともあれ、XML仕様への適合性という点では外観上は特に文句はつけようもないですが、それ以外で気になるところをちょっと後述します。
uppsala
こちらは特にlibxml2にこだわっているというわけではなく、このクレートの作者が別で実装しているbergshamraというXMLセキュリティ?ライブラリのために実装したように見えます。
ただ、だからテキトーな実装というわけでもなく、W3Cテストスイートを通していたり、XPath, XSD, XSLTなど、真面目に実装すると重たそうな機能を実装していたりと、実用的なライブラリに仕上がっています。
ほかの2つのクレートはAIを使い込んで実装しているようですが、このクレートはそれらしい痕跡がありません。まあローカルで使っていてGitHubにそれっぽいファイルをアップロードしていないだけなのかもしれませんが。
READMEが述べるこのライブラリの特徴としては、SSE2を使用することでパース速度を底上げしている点かと思います。どの辺でそれが役に立っているのかはイマイチよくわかりませんが、quick-xmlやroxmltreeがマークアップの区切りを見つけるのにmemchrを使うのと同じノリで使っているのでしょうか。
とにかく、READMEが主張する限りでは、roxmltreeの等倍から最大9倍程度のパフォーマンス向上を成し遂げたとのことです。(本当かなあ?)
XML仕様適合性の点で気になるところがあるとするなら、たしかにW3Cテストスイートを通してはいるのですが、READMEの表によればその総数1208件とのことで、そんなにテストの件数って少なかったっけな?というのが気になります。
私の手元で試した限りでは数件スキップしても2000件弱あるはずなのですが…まあだから適合性に疑いがあるかと言われるとわかりませんが、表に書いてある「100%」とは何なのかという気になってしまいます。
そのほか、こちらも、xmloxideと同様に気になる点があるので、後述します。
fastxml
上の2つはダウンロード数の伸びからしても注目されているな〜というのがわかりますが、このクレートはそうでもないようです。
しかし、サポートする機能としては完全なXML仕様準拠、XPath, XSDサポート、非同期サポートという、特にuppsalaとは遜色ないものです。
このクレートにもfast-xmlという瓜二つの名前のクレートがありますが、別物です。
fast-xmlはquick-xmlが一時的に非公開になっていた時期にフォークして代替として作成されたクレートで、quick-xmlが復活した際に放棄されています。メンテナンスもされていません。
このクレートはStAXパーサと文書木パーサを提供しており、特にCityGMLのような巨大なXML文書においてパフォーマンスが出るように実装しているとのことです。
READMEを信用するならば、パフォーマンスは文書木パーサ、StAXパーサともにlibxml2と比べて高いということらしいです。ただ、libxml2のパフォーマンスは文書木を組む場合のみしか示されておらず、文書木を組まずに(xmllint --saxに相当)パースする場合のパフォーマンスが示されていないので、それと比べてどうなのかは不明です。
仕様適合性について言えば、このクレートもW3Cテストスイートを一定数通しており、やはり高い適合性を示しています。
ちなみに、こちらはv0.11.0現在ではPass=1691, Fail=147件となっており、計1838件のテストを実施していることになります。ますますuppsalaの1208件で100%とは?という気になります。
XML仕様適合以外の気になる点
一応今回の主題はXML仕様に適合しているかという点でマトモか?が主題なので、その点で言えばこの3つのクレートは現時点で使用実績が多いクレートと比べてもだいぶマトモという印象を受けます。
が、ちょっとそこから道を逸れて、リポジトリのソースを読んでいるなかでたまたま気になってしまった点を呟いておこうかと思います。
それはXSDへの適合性の話で、fastxmlは現在時点総数39613件のテストを実施し、うち(blockedとあるものを除き)97%通していると主張しています。実際、XSDテストスイートの総数はそんなものだったと思うので、たぶん正しいのでしょう。
一方で、uppsalaはどうかというと、21123件に対して100%と主張しています。
さらに、uppsalaはXSD 1.1をサポートしていると主張しています。XSD 1.1が1.0からどう更新されたのか、その詳細を語れるほど詳しくはないのですが、パッと見てわかる更新ポイントは組み込み型の派生構造が変わったことでしょう。anyAtomicTypeというものが組み込み原子型とanySimpleTypeの間に挟まり、dateTimeの派生型としてdateTimeStampが、durationの派生型としてdayTimeDuration, yearMonthDurationが、それぞれ追加されました。
dateTimeStamp, dayTimeDuration, yearMonthDurationを使用するスキーマをuppsalaに読ませると、そんな型はない、というエラーが出ます。それでXSD 1.1サポート、と謳うのは流石にダメじゃないのかな?と思います。読めた上で正しく検査できないとかならともかく。
そもそも通しているテストスイートも、XSD 1.1ではなく1.0用のテストスイートです。
xmloxideについていえば、libxml2への互換を目指しているとは思えないほど実装がスカスカです。
リポジトリのソースコードを読むと分かりますが、XSDの実装がテストを除き2300行ほどしかありません。libxml2の実装が3万行以上あるのに、たった2300行で実装できるわけありません。
なぜこんなに実装が軽いかというと、明らかにわかる一点は、スキーマ文書自体の検証がザルになっていることです。つまり、以下のようなスキーマ文書ではない文書を、xmloxideのXSDプロセッサは受け付けます。
<schema> <hogehoge/> <element bar="foo" name="root" type="xs:string"/> </schema>
色々とツッコミどころはあると思いますが、XSD名前空間配下の要素ですらない謎の要素のみで構成された文書をスキーマとして受け付けます。
以下の文書も受け付けます。
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:hogehoge/> <xs:element bar="foo" name="root" type="xs:string"/> </xs:schema>
今度はXSD名前空間配下のスキーマっぽい要素で構成されているように見えますが、中にはSchema of Schemaで許可されない謎の要素やローカル属性が含まれており、これもスキーマとして正しくありません。こんな実装でXSDサポートを謳うのはダメじゃないでしょうか。
XSDのモジュールのドキュメントに"This module implements a subset of the W3C XML Schema Definition Language..."とも書いてあるので、フル実装ではないのは明らかですが、であればLimitationにそう記すべきと思います。
uppsala, xmloxideはこんな調子で実態と違う機能アピールが罷り通っているクレートでもあるので、個人的にはこの3つならfastxmlを推したいかなあと思う次第です。
別にXSDの実装が不十分なのが良くないわけではなく、明らかに分かっていて実装をサボっている機能をアピールしているのがちょっと気に食わないなと思いました。XSDだけじゃなくて他にもそういうのがあるかもしれませんから。
まあたまたま見つけられていないだけで、fastxmlにもそういうのがあるのかもわかりませんが…
おわりに
結論らしい結論を述べていませんが、システムライブラリをインストールすることが面倒でない、Pure Rustにこだわりがないなら、身も蓋もないようではありますがlibxmlを使うのがベストかなと個人的には思います。
Pure Rustがいい、あるいはシステムライブラリをインストールするのが億劫だと言う場合は、xmloxide, uppsala, fastxmlから選ぶのがいいかなと思います。ただ、新興ライブラリなので、プロジェクト継続性は不明なのがネックです。
他の言語には仕様をかっちり遵守するマトモなフル機能ライブラリがあるのにRustにはそういうライブラリがないというのは悲しいので、新興のつよつよライブラリにも頑張って欲しいです。
C言語のlibxml2、Java/C++のXercesのような、とりあえずこれ使っとけばいいよという信頼できる実装がほしいです。
ちなみに…
私もXML操作クレート作っています。私はかなりマトモな実装だと思っていますが、流石に自分のは本編では紹介しませんでした。
GitHub - tayu0110/anyxml: A fully spec-conformant XML library · GitHub
SAX, Push type SAX (私のクレートではProgressiveと呼びます), StAX全部あります。文書をちゃんとチャンク化して読み取るので、メモリ使用量小さいです。
ちゃんとDTDを全部読んで実体の展開、属性値正規化、属性規定値の提供、行末正規化なども全部やりますし、外部実体も読めますし、妥当性検証もできます。
UTF-8/UTF-16はもちろん、UCS4, EBCDIC, ISO-8859, EUCなどもいくらか使えます。
XPath, Catalog, XInclude, XPointer, C14N, RELAX NGも実装しました。(XPointer, C14Nはフル実装じゃないですが…)特にRELAX NG Compact Syntaxは他のクレートでサポートしているものは私の知る限りはありません。
XSDは実装中ですが、大いに苦労しているのでいつ実装終わるかわかりません…
とまあアピールポイントはこんな感じです。今の時代にあってもAIレスコーディングなので、実装に時間がかかっています。
テストスイートが存在するものについてはテストを通していますが、それでもたまにアホみたいなバグが出ることがあるので、見つけたら教えてください。