C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

rust で XPath できる crate たちのメモ; (1) amxml, (2) sxd-xpath

XPath 使いたい需要に対応できる XML パーサーを crates.io で探すと有用そうな crate が2つ見つかりました。簡単な XPath で使い勝手を確認したメモです。

  1. amxml
  2. sxd-xpath ( + sxd-document )

1. amxml

cargo add amxml
#[cfg(test)]
mod tests
{
 /// 共通: 入力XML
 const INPUT: &str = "<abc><x/><y/><z/></abc>";

 /// ルートノードの名前をXPathで確認
 #[test]
 fn amxml_root()
 {
  let document = amxml::dom::new_document(INPUT).unwrap();
  let xpath_result = document.eval_xpath("/*");
  let root_name = xpath_result.unwrap().get_item(0).as_nodeptr().unwrap().name();
  assert_eq!(root_name, "abc");
 }

 /// 第2階層(=ルート直下)ノード群の名前をXPathで確認
 #[test]
 fn amxml_sec()
 {
  let mut sec_names: Vec<String> = vec![];

  let document = amxml::dom::new_document(INPUT).unwrap();
  document
   .each_node("/*/*", |node| {
    sec_names.push(node.name().clone());
   })
   .unwrap();
  assert_eq!(sec_names, ["x", "y", "z"]);
 }
}
  • 👍 嬉しいところ:
    • XPath-3.1 対応
    • read only という事になっているけれど、基本的な DOM 操作は実装されている; append_child, insert_as_previous_sibling, insert_as_next_sibling, delete_child, replace_with, set_attribute, delete_attribute
    • XPath を渡して結果を取得する実装が簡単で扱いやすい印象。実装コスト低く、保守性も良好。
      • eval_xpath でも each_node でも NodePtr または事実上 NodePtr として扱える Item が可換が列挙される (≈XPathだけでなくXPath+Rustコード実装によるDOM的な処理を行いやすい)
  • ⚠ 注意が必要なところ:
    • 2018 年から更新が無く、ごく簡単な Issue/PR も放置されている
    • note(†): 文字列値を直接取り出すとダブルクォート囲いなリテラル付きの表現になります
      • 例えばルート要素の名前を XPath で直接 /*/name() のように取得すると "\"abc\"" が取れます
// 参考おまけ: note(†) 
let xpath_result = document.eval_xpath("/*/name()"); // XPath では /name() で要素名の文字列値を取れる
let root_name = xpath_result.unwrap().get_item(0).to_string(); // XPathの結果がノードではなく文字列値なので Item::to_string を使える
assert_eq!(root_name, "\"abc\""); // 文字列リテラル表現で取れてくるので比較など後の処理で"文字列リテラルな文字列"を気にする事になります

2. sxd-xpath ( + sxd-document )

cargo add sxd-xpath sxd-document
#[cfg(test)]
mod tests
{
 /// 共通: 入力XML
 const INPUT: &str = "<abc><x/><y/><z/></abc>";

 /// ルートノードの名前をXPathで確認
 #[test]
 fn sxd_root()
 {
  let package = sxd_document::parser::parse(INPUT).unwrap();
  let document = package.as_document();
  let xpath_result = sxd_xpath::evaluate_xpath(&document, "/*");
  let value = xpath_result.unwrap();
  let root_name = match value
  {
   sxd_xpath::Value::Nodeset(nodes) =>
   {
    let node = nodes.iter().next().unwrap();
    let qname = node.expanded_name().unwrap();
    qname.local_part()
   },
   _ => panic!("Failed: `Value` -> `Nodeset`")
  };
  assert_eq!(root_name, "abc");
 }

 /// 第2階層(=ルート直下)ノード群の名前をXPathで確認
 #[test]
 fn sxd_sec()
 {
  let package = sxd_document::parser::parse(INPUT).unwrap();
  let document = package.as_document();
  let xpath_result = sxd_xpath::evaluate_xpath(&document, "/*/*");
  let value = xpath_result.unwrap();

  let sec_names: Vec<String> = match value
  {
   sxd_xpath::Value::Nodeset(nodes) =>
   {
    nodes
     .iter()
     .map(|node| node.expanded_name().unwrap().local_part().to_string())
     .collect()
   },
   _ => panic!("Failed: `Value` -> `Nodeset`")
  };

  // ☢ 要注意: sxd-xpath の nodes の列挙順序は実行ごとに変わります。
  let expected = ["x", "y", "z"];
  assert_eq!(sec_names.len(), expected.len());
  for element in expected.iter()
  {
   assert!(sec_names.contains(&element.to_string()));
  }
 }
}
  • 👍 嬉しいところ:
    • 更新頻度、コミッター数はぼちぼち
  • ❓ 嬉しいかもしれないけれど判断の難しいところ:
    • めんどくさいので触れませんでしたが Context を使うとより変態的な実装もできそうです
  • ⚠ 注意が必要なところ:
    • XPath-1.0 のみ対応; (XSLT-1.0 には将来的に対応予定と README に記載あり。 libxml, libxslt の Rust による完全な置き換えを目標にしている)
    • evaluate_xpath から Value を取り出して Nodeset を取り出して Node を列挙する実装を書くのががややめんどくさい(XPath の規格通りの "QName に LocalPart があって…" などの構造を辿りやすいものの…)
    • XPath で直接要素名を取る /*/name() のような、XPath の結果がノード群になりえない XPath は失敗するようです

どちらを使うのが嬉しそうか?

  • XPath-3.1 対応が欲しい 👉 amxml
  • Author / Comitter が活きていて欲しい 👉 sxd-xpath
  • ユーザーコードを簡潔に済ませたい 👉 amxml
  • XPath 以外の XML の DOM アクセスや read だけでなく write も欲しい 👉 sxd-xpath > amxml (基本的なDOM操作はどちらでもできます)

追記: どちらが速そう?; 2020-08-05

  • たぶん sxd-xpath の方がはやいです (少なくとも↑の test の程度の小さな XML のパースなら)

↓は

test benches::amxml_root ... bench:      37,902 ns/iter (+/- 4,120)
test benches::amxml_sec  ... bench:      25,728 ns/iter (+/- 1,255)
test benches::sxd_root   ... bench:      13,392 ns/iter (+/- 2,052)
test benches::sxd_sec    ... bench:      16,544 ns/iter (+/- 2,413)

参考