nom で csv

json

前回はcombineでCSVをパースしようとして、どろ沼に嵌りこんでしまった。ならば、前々囘にやったnomではどうか? 名誉挽回したい所。exampleに例が出てた。

なんで、jsonばかり採り上げられる?

JSONファイルとは?基本構造からファイルの読み書き方法まで解説

こんなのを見ると、表現が多用だからってのがありそう。rustではよく使われる Serde JSON なんてのもあるけど、それの前段階として勉強するには、うってつけと思えるぞ。 rustばかりだと、井の中の蛙になりそうなので、有名な処理系に少し触れてみる。

python

デフォは何と言ってもpickle.pyだな。

Create portable serialized representations of Python objects.

高所から捉えると、シリアライズの一種なのね。 でも、世の中の趨勢でjsonはデフォで用意されてた。json/scanner.rbをみると

def py_make_scanner(context):
    parse_object = context.parse_object
    parse_array = context.parse_array
    parse_string = context.parse_string
    match_number = NUMBER_RE.match
    strict = context.strict
    parse_float = context.parse_float
    parse_int = context.parse_int
    parse_constant = context.parse_constant
    object_hook = context.object_hook
    object_pairs_hook = context.object_pairs_hook
    memo = context.memo

ふーん、そんなものですかい。

ruby

勿論jsonは有った。それに加えてyamlも提供されてる。一時持て囃されたね。

今は亡き るびま でも採り上げられていたな。そんなアーカイブは読めるのかな。 library yaml からリンクされてた。

プログラマーのための YAML 入門 (初級編)

なかなかしぶといな。

scheme

しぶといと言ったら、これでしょ。何でも括弧で構造を表してしまうから、専用のフォーマットなんて無いんよ。(print hoge) で終り。読む時は (read) 簡単明瞭。

しいて注意点を挙げるとすれば、データの可搬性を担保する目的でprintじゃなくてwriteを使えって亊ぐらいか。

gosh> (print "hoge")
hoge
#<undef>
gosh> (write "hoge")
"hoge"#<undef>

printは人間用、writeはマシン用。再現性重視って亊だ。

json by nom

重視したいのは、ファイルから問題無く読めるかと、読み込んだデータを普通に使えるかだ。この2点を満足しないと、実用にはならない。

fn main() {
    let data = std::fs::read_to_string("data.json").unwrap();
    let res = root::<(&str, ErrorKind)>(&data).unwrap().1;
    println!("parsing a valid file:\n{:#?}\n", &res);
    println!("again:\n{:#?}\n", &res);
}

example/json.rsを簡略化したもの。データはファイルで渡してる。

[sakae@fb /tmp/json]$ cat data.json
{"id" : 1234, "name" : "tanaka"}

テスト用の極小サンプル。お試しの化粧品みたいな物だ。

parsing a valid file:
Object(
    {
        "name": Str(
            "tanaka",
        ),
        "id": Num(
            1234.0,
        ),
    },
)

again:
Object(
    {
        "name": Str(
            "tanaka",
     :

何とか、クリアしたな。癖が無い使い易い木箱だと思うぞ。安心して使えるな。

for csv

jsonの作りを、訳も解らず真似してみる。

失敗策

use nom::bytes::complete::tag;
use nom::{character::complete::digit1, combinator::map_res, IResult};
use std::str::FromStr;

#[derive(Debug, PartialEq)]
enum CSV {
    Line2([i32; 2]),
}

pub fn num<'a>(i: &'a str) -> IResult<&'a str, i32> {
    map_res(digit1, FromStr::from_str)(i)
}

fn line2<'a>(i: &'a str) -> Result<(&'a str, CSV), nom::Err<nom::error::Error<&'a str>>> {
    let (i, ym) = num(i).unwrap();
    let (i, _) = tag(",")(i)?;
    let (i, hi) = num(i).unwrap();
    Ok((i, CSV::Line2([ym, hi])))
}

fn main() {
    println!("{:?}", line2("12,34,56,78\n"));
}

作成途中の結果。

Ok((",56,78\n", Line2([12, 34])))

型が付いてしまって、型無しだな。それに、line2のパース方法が、手続的で、格好悪いぞ。

素朴版

やっぱり原本の説明書と若干の地図である Rust: nom によるパーサー実装を参考にするのが確実。ああ、若干の地図じゃなくて、優れた地図に訂正です。これが無かったら、確実に迷子になっていたでしょう。

原本の説明書は、下記のようにして、手元で開くのがお勧め。

cargo doc; cd target/doc;  python3 -m http.server 8080
firefox http:://ip_addr:8080/nom/

直接firefoxを開いていないのは、cargoが仮想PC上で動いていて、Windows上のブラウザーを使いたかった為。手元でサーバーが動いているので、検索の応答も速くて、なかなな快適。

仮想PCとしてVirtualBOXをNATモードで動かしているなら、ネットワークの高度な設定で、80 -> 8080とポートフォワード設定をしておけば、http://localhost/nom と言う具合にアクセス出来る。全く違和感なし。

use nom::multi::{separated_list0, many1};
use nom::bytes::complete::tag;
use nom::IResult;
use nom::character::complete::digit1;
use nom::sequence::terminated;

fn line(s: &str) -> IResult<&str, Vec<&str>> {
    terminated(
        separated_list0(tag(","), digit1),
        tag("\n"))(s)
}

fn main() {
    //    let input = "12,34\n56,78\n98,76\n";
    let input = std::fs::read_to_string("current.csv").unwrap();
    let out = many1( line )(&input);
    let slm = &out.as_ref().unwrap().1;
    println!("{:?}", out);
    println!("{:?}", slm);
    println!("{}",   slm.len());
}

実行結果

Ok(("", [["12", "34"], ["56", "78"], ["98", "76"]]))
[["12", "34"], ["56", "78"], ["98", "76"]]
3

今後の為のメモを残しておく。全てコンビネータ式で実現。小さな関数を組合せている。schemeで言う所の、高階関数の組み合わせ。

核はlineと言うパーサー。一行の構造を表現。カンマ区切で数値が並んでます。それをリストに変換してください。リストはrustにおいては、Vecと言う表現になる。最後は改行が残る。それが、一行の終端になるんで、ターミネーターだとよ教えてあげる。それで、一行が解析出来て、結果はVec<&str>って亊になる。

mainの方では、行が繰替えされているって亊をmany1で表している。一行は既に定義されたlineだ。これで最終結果はVec<Vec<&str>>て亊になる。

区切文字列の表現はtagを使った。本当は文字列じゃなくて、1文字なんでchar('\n')なんてやるのが正解。だけど、このtagを使うとtag<"<body>") .. tag<"</body>") みたいな、HTMLの厚い括弧記号にも対応出来るんで、忘れないように、あえて使ってみた。

完成版

パースの結果を数値で欲しいので、失敗策で作ったnumを取り込んでみた。高階関数は、貴方を後悔させませんです。

use nom::multi::{separated_list0, many1};
use nom::bytes::complete::tag;
use nom::IResult;
use nom::character::complete::digit1;
use nom::sequence::terminated;
use std::str::FromStr;
use nom::combinator::{ map_res, };

fn num<'a>(i: &'a str) -> IResult<&'a str, i32> {
   map_res(digit1,  FromStr::from_str)(i)
}

fn line(s: &str) -> IResult<&str, Vec<i32>> {
    terminated(
        separated_list0(tag(","), num),
        tag("\n"))(s)
}

fn main() {
    let input = "1,2,3,4\n5,6,7,8\n9,0,1,2\n";
//  let input = std::fs::read_to_string("current.csv").unwrap();
    let out = many1( line )(&input);
    let slm = &out.as_ref().unwrap().1;
    println!("{:?}", out);
    println!("{:?}", slm);
    println!("{}",   slm.len());
}

一応、検算。

Ok(("", [[1, 2, 3, 4], [5, 6, 7, 8], [9, 0, 1, 2]]))
[[1, 2, 3, 4], [5, 6, 7, 8], [9, 0, 1, 2]]
3

本当は、各レコードの先頭2箇分をほしかったので、takeを当ってみたんだけど、それは文字列に対してのみ定義されてた。まあ、しょうがない。

ソースを覗き味する。

上でやったように、自前でdocすると、Docs.rs を遠慮しながら使わなくてもいいので、ソースを思う存分眺められる。

combinatorに有る

map      Maps a function on the result of a parser.
map_res  Applies a function returning a Result over the result of a parser.

これの違いがよー解らんので、ソースにご対面。

pub fn map<I, O1, O2, E, F, G>(mut parser: F, mut f: G) -> impl FnMut(I) -> IResult<I, O2, E>
where
  F: Parser<I, O1, E>,
  G: FnMut(O1) -> O2,
{
  move |input: I| {
    let (input, o1) = parser.parse(input)?;
    Ok((input, f(o1)))
  }
}

pub fn map_res<I: Clone, O1, O2, E: FromExternalError<I, E2>, E2, F, G>(
  mut parser: F,
  mut f: G,
) -> impl FnMut(I) -> IResult<I, O2, E>
where
  F: Parser<I, O1, E>,
  G: FnMut(O1) -> Result<O2, E2>,
{
  move |input: I| {
    let i = input.clone();
    let (input, o1) = parser.parse(input)?;
    match f(o1) {
      Ok(o2) => Ok((input, o2)),
      Err(e) => Err(Err::Error(E::from_external_error(i, ErrorKind::MapRes, e))),
    }
  }
}

ジェネリックな型システムで、対応範囲を拡げているんだな。頑張って、 ジェネリック型、トレイト、ライフタイム あたりを読み直してみよう。現実問題として身を入れて読めるだろう。

csv-parser

ちゃんと、ちゃんと、やると csv-parser こういう物が出来上がるとな。こんな風に、汎用で使えるように設計されてる。

use csv_parser;

fn main() {
    let csv_to_parse = "\"nom\",age\ncarles,30\nlaure,28\n";
    if let Ok(parsed_csv) = parse_csv(csv_to_parse) {
        // and we're all good!
    }
}

面倒くさい、ダブルクォートにも対応してる。Real World Haskell本でも、このあたりや、行末記号の扱いで熱く語っていた。我等がmatzさんの処女作である、オブジェクト指向スクリプト言語Rubyな本でも、CSVのパースを熱く採り上げていたぞ。鬼門なんだな。

Cargo.tomlを確かめると、nom = "~1.1.0" なんて言う指定がなされていた。得意のgit log では、最終更新日が、Date: Wed Nov 16 16:27:42 2016 +0100 となってる、時代がかった物である亊が判明。

現代のnomで通用するのか? 軽くcargo checkしてみると

error: cannot find macro `named` in this scope
  --> src/parser.rs:10:1
   |
10 | named!(string_between_quotes, delimited!(char!('\"'), is_not!("\""), char!('\"')));
   | ^^^^^

  :
error: cannot find macro `map_res` in this scope
   --> src/parser.rs:106:5
    |
106 |     map_res!(input,
    |     ^^^^^^^

ってな具合に、マクロ使いまくりで作られていたんだねぇ、なんて言う時代考証が出来ましたよ。

エラーは兎も角、考えかたは現代のnomにも通用するはずなんで、UIだからね。

最近は世界情勢が変ってきて、金さえ積めばなんでもすぐに手に入るって言う資本主義パラダイスが通用しなくなってるように思える。 大事に大事に修理して末永くつかいましょ、ですかね。

それにしても、マクロを捨てて大転換した理由はなんだろう? マクロは難しくて、将来が見通せなかったから? ここぞと言う時、マクロを繰り出すのはいいけど、全対的にマクロってのは、破綻の前兆って達ったんだろうね。想像だけど。

gitのdocに upgrading_to_nom_5.md なんてのが鎮座してた。その一節に

Unfortunately, macros were sometimes hard to manipulate, since nom was relying
on a few lesser known tricks to build its DSL, and macros parsing errors were
often too cryptic to understand.

こんな亊が出てた。DeepLで日本語にしてみた。

残念ながら、マクロは操作しにくいことがありました。
nomは、DSLを構築するためにあまり知られていないいくつかのトリックを使用しており、
マクロのパースエラーは暗号めいてて、理解するのは難しい。

多少オイラーが脚色したんで、間違いはオイラーの責です。

web contents

cargo docで作成されるコンテンツが面白かったので、ちょっと調べた亊を書いておく。target/doc/で目立つのは、長いファイル名のやつ。

-rw-r--r-- 1 sakae sakae 677868 Mar 26 15:09 NanumBarunGothic.ttf.woff
-rw-r--r-- 1 sakae sakae 399468 Mar 26 15:09 NanumBarunGothic.ttf.woff2

Web用にcssと連動出来るフォントだそうだ。最初の仕様がwoffってやつで、それより圧縮率を高めたやつがバージョン2ってやつ。互換性がないので、同じものを提供。広く色々なブラウザーでも使ってもらえるようにとな。

それから、各種のcssやらjsファイルが置かれている。残った奴は、

csv  implementors  memchr  minimal_lexical  nom  settings.html  src

csvって名前の箱を作ったので、それがdirの形で現れている。他の木箱も同様。

sakae@pen:/tmp/csv/target/doc$ tree src
src/
├── csv
│   └── main.rs.html
:
└── nom
    ├── bits
    │   ├── complete.rs.html
    │   ├── mod.rs.html
    │   └── streaming.rs.html
    ├── branch
    │   └── mod.rs.html
    :

src/は、htmlに変換されて、格納されてる。同時にdirの構造も表しているので、ソースを開いた時、左上に出て来る三角タグで、階層の表示と、指定したファイルへのジャンプがすぐに出来る。

setting.htmlをアクセスすると、全体の見栄えを多少カスタマイズ出来る。

それぞれの木箱をアクセスすると、そこで定義されてるモジュールやらが左ペインに出て来る。そこから辿って、関数の定義が参照出来る。勝手知ったるやつ。

Function csv::line                                         [−][src]

pub(crate) fn line(s: &str) -> IResult<&str, Vec<i32>>

何と、関数の型だけが表示された。これはもう、haskell流の型確認だな。隠れhaskellであるrustの真髄を、さりげなく表しているな。rustならhaskell人口よりづっと多いでしょ。GCは無いので安心して使えるしね。

etc