RustのXML操作ライブラリのquick_xmlを使ってみた

なんとなくRustを勉強している中で、virshのxmlを編集する必要が出てきたため、ググって一番最初に見つかったquick_xmlを使ってみました。

github.com

quick_xmlは特徴としてRustのXMLライブラリの中でも特に早く動くらしいです。

cargo.tomlへの設定

0.20.0が最新っぽいのでこれを使います

[dependencies]
quick-xml = "0.20.0"

実際に使ってみる

SYNOPSISを参考にtemplate.xmlからdump.xmlを作成する例題を書いてみました。

github.com

Readerはquick_xml専用の実装を使う必要があり、Reader::from_readerに任意のReaderを渡すことで作成可能です。

let mut reader = Reader::from_reader(BufReader::new(File::open(file)?));

Writerも同様です。

 let mut writer = Writer::new(BufWriter::new(File::create("dump.xml").unwrap()));

用意したReaderを使って実際にxmlを読んでみます。

公式の通りreader.read_eventで読み進めて、読んだ先の構造を型でマッチングする世界観のようです。

loop {
        match reader.read_event(&mut buf) {
            Ok(Event::Start(ref e)) if e.name() == XML_NAME_ATTRIBUTE => {
                writer
                    .write_event(Event::Start(e.clone()))
                    .expect("faild write event");
                reader.read_event(&mut Vec::new()).expect("faild read event");
                let elem = BytesText::from_plain_str("anatofuz-vm");
                writer.write_event(Event::Text(elem)).unwrap();
            }

            Ok(Event::Text(ref e)) if e.escaped() == b"ie-virsh-template" => {
                let elem = BytesText::from_plain_str("anatofuz-vm");
                writer.write_event(Event::Text(elem)).unwrap();
            }
            Ok(Event::End(ref e)) if e.name() == b"this_tag" => {
                assert!(writer
                    .write_event(Event::End(BytesEnd::borrowed(b"my_elem")))
                    .is_ok());
            }
            Ok(Event::Eof) => break,
            // you can use either `e` or `&e` if you don't want to move the event
            Ok(e) => writer.write_event(&e).unwrap(),
            Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
        }
        buf.clear();
    }

この例ではEvent::Eofが読まれるまで無限ループでxmlをパースし続けます。

Eventは名前からなんとなくわかりますが、例えば<hoge>foo</hoge>のような構造の場合は、<hoge>Event::StartfooEvent::Text、最後の</hoge>Event::Endに対応します。

面白いのは後置のifのようなsyntaxで、特定のEventでかつ、特定の文字列が来た場合などの判定が可能なものです。

例えばこのブロックでは</this_tag>のケースのみ実行されます。

            Ok(Event::End(ref e)) if e.name() == b"this_tag" => {
                assert!(writer
                    .write_event(Event::End(BytesEnd::borrowed(b"my_elem")))
                    .is_ok());
            }

ここで<name>template</name>のようなXMLの構造が来た場合に、中のtemplateを書き換える処理を考えてみます。

まず<name>で来た場合にキャプチャをする必要があるので、Event::Startでキャプチャを行います。

const XML_NAME_ATTRIBUTE: &[u8; 4] = b"name";

///省略

Ok(Event::Start(ref e)) if e.name() == XML_NAME_ATTRIBUTE => {

writerの先のxmlへはwriter.write_eventなど、型によってwriter_*の専用メソッドが用意されています。

この例ではEvent単位で読みながらファイルを書き出していくので、基本的にはwriter.write_eventを使用することになります。

writerへの書き込みはref eで受けていた場合はe.clone()する必要があり、refで受けていない場合はポインタを取るのみで可能です。

writer
        .write_event(Event::Start(e.clone()))
        .expect("faild write event");

次に<name>template</name>templateを書き換える為に、read_eventで更に1EVENT読み進めます。

    reader.read_event(&mut Vec::new()).expect("faild read event");

読んだ先は使わないので特に束縛せず、雑にVec::new()しています。

続いて一度BytesTextインスタンスを作ってから、Event::Text型を作り、これをwrite_event経由で書き込みます。 文字列リテラルからのBytesTextの作成はfrom_plain_strを使うと可能です。

docs.rs

    let elem = BytesText::from_plain_str("anatofuz-vm");
    writer.write_event(Event::Text(elem)).unwrap();

そもそも、xmlのcontentを決め打ちでリプレイスする場合は、Event::Textでマッチングすると楽です。

Ok(Event::Text(ref e)) if e.escaped() == b"ie-virsh-template" => {
    let mut elem = BytesText::from_plain_str("anatofuz-vm");
    writer.write_event(Event::Text(elem));
}