薄っぺらりん

厚くしていきたい

Rustでタグをパースするクレートを公開したよ!

parsercher

parsercherというクレートを公開しました。
HTMLやXMLといったタグで記述されたドキュメントをパースし、タグやテキストを抽出することができます。 パース結果は構造体のツリーとして返してくれるので、APIによる操作の他に自分でツリーを操作することができます。

(追記: v3.1.0の記事を書きました

github.com

目次

使い方

APIの使い方や出力例は上のdocs.rsに記載しています。 また、ソースコードexamplesAPI毎の例を用意しています。

ここでは簡単な使用例と共にparsercherを紹介します。

まずはCargo.tomlにparcherserを加えましょう。

[dependencies]
parsercher = "1.0.0"

例に使用するHTML

今回は次のHTMLを例として使用します。ファイル名はindex.htmlです。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>sample</title>
  </head>
  <body>
    <h1>Hello, world!</h1>

    <div id="content"></div>

    <ul id="list1">
      <li class="listItem">リンゴ</li>
      <li class="listItem">ゴリラ</li>
      <li class="listItem">ラッパ</li>
    </ul>

    <ol id="list2">
      <li id="target" class="listItem">first</li>
      <li class="listItem">second</li>
      <li id="target" class="listItem">therd</li>
    </ol>

    <!-- All script code becomes one text -->
<script>
  let content = document.getElementById('content');
  content.textContent = 'content';
</script>

  </body>
</html>

ドキュメントをパースする

index.htmlをパースして構造体のツリーを作りましょう。 これにはparsercher::parse()を使用します。
以下はパースした結果を表示する例です。

use std::fs::OpenOptions;
use std::io::prelude::*;
use parsercher;

fn load_input() -> String {
    let mut f = OpenOptions::new().read(true).open("index.html").unwrap();
    let mut input = String::new();
    f.read_to_string(&mut input).unwrap();
    input
}

fn main() {
    let input = load_input();
    if let Ok(dom) = parsercher::parse(&input) {
        println!("{:#?}", dom);
    }
}

標準出力にはDom構造体のツリーが表示されます。長すぎるのでheadタグからmetaタグまでを抜き出すと、以下のようになっています。(似たような出力の全体像がドキュメントの例にあります)

Dom {
    dom_type: Tag,
    tag: Some(
        Tag {
            name: "head",
            attr: None,
            terminated: false,
            terminator: false,
        },
    ),
    text: None,
    comment: None,
    children: Some(
        [
            Dom {
                dom_type: Tag,
                tag: Some(
                    Tag {
                        name: "meta",
                        attr: Some(
                            {
                                "charset": "UTF-8",
                            },
                        ),
                        terminated: false,
                        terminator: false,
                    },
                ),
                text: None,
                comment: None,
                children: None,
            },

ツリーをprintln!マクロで表示したのでは正しくパースが行えているか確認することが困難なので、そんなときはparsercher::print_dom_tree()が便利です。main関数を次のように変更します。

fn main() {
    let input = load_input();

    if let Ok(dom) = parsercher::parse(&input) {
        parsercher::print_dom_tree(&dom);
    }
}

今度はDom構造体のツリーが見やすく出力されます。

<root>
  <!DOCTYPE html="">
  <html>
    <head>
      <meta charset="UTF-8">
      <title>
        TEXT: "sample"
    <body>
      <h1>
        TEXT: "Hello, world!"
      <div id="content">
      <ul id="list1">
        <li class="listItem">
          TEXT: "リンゴ"
        <li class="listItem">
          TEXT: "ゴリラ"
        <li class="listItem">
          TEXT: "ラッパ"
      <ol id="list2">
        <li id="target" class="listItem">
          TEXT: "first"
        <li class="listItem">
          TEXT: "second"
        <li id="target" class="listItem">
          TEXT: "therd"
      <!--" All script code becomes one text "-->
      <script>
        TEXT: "\n  let content = document.getElementById('content');\n  content.textContent = 'content';\n"

Dom構造体は次のような定義になっており、タグはTag構造体、コメントはComment構造体、それ以外のテキスト部分はText構造体になります。ドキュメント内で<script></script>で囲まれたコード部分はscriptタグの子のテキストになります。

pub struct Dom {
    /// Type of Dom structure
    pub dom_type: DomType,
    tag: Option<Tag>,
    text: Option<Text>,
    comment: Option<Comment>,
    children: Option<Vec<Box<Dom>>>,
}

タグ名でタグを抽出する

指定したタグ名に一致するタグを抽出することができます。
これにはparsercher::search_tag_from_name()を使います。
例えば、liタグを全て抽出するなら次のようにします。

fn main() {
    let input = load_input();

    if let Ok(dom) = parsercher::parse(&input) {
        if let Some(tags) = parsercher::search_tag_from_name(&dom, "li") {
            for (i, tag) in tags.iter().enumerate() {
                println!("{}: {:?}", i, tag);
            }
        }
    }
}

結果はTag構造体のVecで返されます。

0: Tag { name: "li", attr: Some({"class": "listItem"}), terminated: false, terminator: false }
1: Tag { name: "li", attr: Some({"class": "listItem"}), terminated: false, terminator: false }
2: Tag { name: "li", attr: Some({"class": "listItem"}), terminated: false, terminator: false }
3: Tag { name: "li", attr: Some({"id": "target", "class": "listItem"}), terminated: false, terminator: false }
4: Tag { name: "li", attr: Some({"class": "listItem"}), terminated: false, terminator: false }
5: Tag { name: "li", attr: Some({"id": "target", "class": "listItem"}), terminated: false, terminator: false }

特定のタグを抽出する

タグ名や属性、値といった条件を満たすタグを抽出することができます。
これにはparsercher::search_tag()を使います。
例えば、id属性の値がtargetであるliタグを全て抽出するなら次のようにします。

use parsercher::dom::Tag;

fn main() {
    let input = load_input();

    if let Ok(dom) = parsercher::parse(&input) {
        // <li id="target">
        let mut tag = Tag::new(String::from("li"));
        let mut attr = HashMap::new();
        attr.insert(String::from("id"), String::from("target"));
        tag.set_attr(attr);

        if let Some(tags) = parsercher::search_tag(&dom, &tag) {
            for (i, tag) in tags.iter().enumerate() {
                println!("{}: {:?}", i, tag);
            }
        }
    }
}

結果はTag構造体のVecで返されます。

0: Tag { name: "li", attr: Some({"class": "listItem", "id": "target"}), terminated: false, terminator: false }
1: Tag { name: "li", attr: Some({"class": "listItem", "id": "target"}), terminated: false, terminator: false }

特定のタグの子テキストを抽出する

タグ名や属性、値といった条件を満たすタグを指定し、その子となるテキストのみ抽出することができます。
これにはparsercher::search_text_from_tag_children()を使います。
例えば、id属性の値がtargetであるliタグの子テキストを全て抽出するなら次のようにします。

use parsercher::dom::Tag;

fn main() {
    let input = load_input();

    if let Ok(dom) = parsercher::parse(&input) {
        // <li id="target">
        let mut tag = Tag::new(String::from("li"));
        let mut attr = HashMap::new();
        attr.insert(String::from("id"), String::from("target"));
        tag.set_attr(attr);

        if let Some(texts) = parsercher::search_text_from_tag_children(&dom, &tag) {
            for (i, text) in texts.iter().enumerate() {
                println!("{}: {:?}", i, text);
            }
        }
    }
}

結果はStringのVecで返されます。

0: "first"
1: "therd"

タグ同士の十分条件を判定する

pタグがqタグであるための十分条件であることを判定することができます。 Dom構造体のツリーを自分で操作する際に便利です。
これにはparsercher::satisfy_sufficient_condition()を使います。 結果はboolで返します。
例えばp=>qの場合、次のようになります。

use parsercher::dom::Tag;

fn main() {
    // <h1 class="target">
    let mut p = Tag::new("h1".to_string());
    let mut attr = HashMap::new();
    attr.insert("class".to_string(), "target".to_string());
    p.set_attr(attr);

    // <h1 id="q" class="target">
    let mut q = Tag::new("h1".to_string());
    let mut attr = HashMap::new();
    attr.insert("id".to_string(), "q".to_string());
    attr.insert("class".to_string(), "target".to_string());
    q.set_attr(attr);

    assert_eq!(parsercher::satisfy_sufficient_condition(&p, &q), true);

    // <h1 id="q">
    let mut q = Tag::new("h1".to_string());
    let mut attr = HashMap::new();
    attr.insert("id".to_string(), "q".to_string());
    q.set_attr(attr);

    assert_eq!(parsercher::satisfy_sufficient_condition(&p, &q), false);
}

まとめ

パーサは書いていて楽しいですね。オライリーの言語実装パターンをまた読み直したいと思いました。

parsercherという名前はparserとsearcherから作った造語です。いままでHTMLを操作するときはgrepsedを使ってある程度削ってから都度プログラムを書いていましたが、今後はparsercherを使っていきたいなと思います。検索の機能がまだ弱いので、Dom構造体ツリーの部分一致するところだけ柔軟に抽出する機能とかを作りたいですね。

今回初めてcrates.ioにクレートを公開しましたが、公開と共にdocs.rsにドキュメントページが作成されるのは大変便利だと思いました。Doc commentsを書こうという意識が生まれます。