文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

Embulkでtsvパース時にjava.lang.Character out of VALUE_STRING tokenが起きる場合の対策

知ってないと解らない系の挙動はキツイっすね・・・
f:id:treeapps:20160821022019p:plain

Embulkをそろそろ使っていこうと思い、早速入門してみました。

しかし・・・


エラーでTSVをパースする事すらできないという洗礼を受けました


はい。

結果的に解決したのですが、どっと疲れました。。。

あまりにも意味不明な挙動で、StackTraceの内容からエラーの内容を読み取る事ができず、始まる前からやめてしまおうかと思う程でした。

Emblukでエラーが起きる手順

使用するデータ

"ID"	"名前"	"日時"
"1"	"鈴木\"一郎\""	"2016-08-20 10:11:12"
"2"	"鈴木
二郎"	"2016-08-21 10:11:13"
"3"	"鈴木\"三
郎\""	"2016-08-22 10:11:14"

名前列は改行と改行+エスケープシーケンスが動くかどうかをテストしています。

tsvでエラーが起きる手順

github.com

embulk example ./try1
embulk guess   ./try1/seed.yml -o config.yml
embulk preview config.yml
embulk run     config.yml

まず公式通りにこれをやってみようとしました。
scaffold的なコマンドであるexampleで初期ファイルを自動生成し、guessで推測によってconfig.ymlを自動生成し、previewで結果のプレビューし、最後にrunして実行、というものです。outputは標準出力です。

これだけ見ると非常に簡単なのですが、このサンプルはinputがgz圧縮されたcsvだったので、生のtsvの方が現場でよくあるケースなので、tsvで実行してみる事にしました。

config.yml
in:
  type: file
  path_prefix: /private/tmp/example/test.
  parser:
    charset: UTF-8
    newline: LF
    type: csv
    delimiter: '\t'
    quote: '"'
    escape: '\'
    null_string: ''
    trim_if_not_quoted: false
    skip_header_lines: 1
    allow_extra_columns: false
    allow_optional_columns: false
    columns:
    - {name: id, type: string}
    - {name: name, type: string}
    - {name: time, type: timestamp, format: '%Y-%m-%d %H:%M:%S'}
out: {type: stdout}
実行結果
tree:example tree$ embulk preview config.yml
2016-08-21 00:53:55.249 +0900: Embulk v0.8.9
2016-08-21 00:53:56.063 +0900 [INFO] (0001:preview): Listing local files at directory '/private/tmp/example' filtering filename by prefix 'test.'
2016-08-21 00:53:56.068 +0900 [INFO] (0001:preview): Loading files [/private/tmp/example/test.tsv]
org.embulk.config.ConfigException: com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.Character out of VALUE_STRING token
 at [Source: N/A; line: -1, column: -1]
       	at org.embulk.config.ModelManager.readObjectWithConfigSerDe(org/embulk/config/ModelManager.java:75)
       	at org.embulk.config.DataSourceImpl.loadConfig(org/embulk/config/DataSourceImpl.java:220)
       	at org.embulk.standards.CsvParserPlugin.transaction(org/embulk/standards/CsvParserPlugin.java:211)
・・・・snip・・・・

????

out of VALUE_STRING token ???
jacksonがデシリアライズに失敗した???
Source: N/A; line: -1, column: -1 ???

エラー内容から何が起きているのかさっぱり読み取れません。不明なソースのマイナス1行目のマイナス1列目でVALUE_STRINGトークンがはみ出した?という謎エラーが起きているようです。

正しい例

config.yml
in:
  type: file
  path_prefix: /private/tmp/example/test.
  parser:
    charset: UTF-8
    newline: LF
    type: csv
    delimiter: "\t"
    quote: '"'
    escape: '\'
    null_string: ''
    trim_if_not_quoted: false
    skip_header_lines: 1
    allow_extra_columns: false
    allow_optional_columns: false
    columns:
    - {name: id, type: string}
    - {name: name, type: string}
    - {name: time, type: timestamp, format: '%Y-%m-%d %H:%M:%S'}
out: {type: stdout}

はい。ここで間違い探しゲームです。

どこが変わったでしょう?

答えは・・・・

delimiter: '\t'  ->  delimiter: "\t"

でした!!


は?


は?



は?


最終的に色々調査してみた結果、なんと公式に答えがありました。
Configuration — Embulk 0.8 documentation
f:id:treeapps:20160821023209p:plain

oh...

しれっとドキュメントの方はダブルクォートになっていますね。。。

ドキュメントを読まないのが悪い、という結論に至るのですが、これはちょっと不親切ですね。

サンプルがシングルクォートを出力するのにtsvの場合はダブルクォートでないとだめと。

実行結果

一応実行結果です。ちゃんと実行できました。

tree:example tree$ embulk preview ./config.yml
2016-08-21 01:58:39.667 +0900: Embulk v0.8.9
2016-08-21 01:58:40.498 +0900 [INFO] (0001:preview): Listing local files at directory '/private/tmp/example' filtering filename by prefix 'test.'
2016-08-21 01:58:40.502 +0900 [INFO] (0001:preview): Loading files [/private/tmp/example/test.tsv]
+-----------+-------------+-------------------------+
| id:string | name:string |          time:timestamp |
+-----------+-------------+-------------------------+
|         1 |      鈴木"一郎" | 2016-08-20 10:11:12 UTC |
|         2 |       鈴木
二郎 | 2016-08-21 10:11:13 UTC |
|         3 |     鈴木"
" | 2016-08-22 10:11:14 UTC |
+-----------+-------------+-------------------------+

改行を含める事もできるし、改行+ダブルクォートのエスケープも動いています。

実はyaml自体の仕様だった

twitterで教えて頂いたのですが、この挙動、実はyamlの仕様なのでした。。。

ダブルクォートスタイルでは任意の文字列をあらわすのにエスケープシーケンス (\) を使うこともできます。こちらのスタイルは文字列に \n もしくは Unicode を埋め込む場合に適しています。
"ダブルクォートで囲まれた文字列\n"

http://symfony.com/legacy/doc/reference/1_4/ja/02-yaml

データ型は以下の通りで基本的に自動で認識されます。文字列に変換したい時はシングルクォートかダブルクォートで囲みます。ダブルクォートで囲む場合はエスケープシーケンスを使うことが可能です。

http://www.task-notes.com/entry/20150922/1442890800


この挙動はyamlの仕様でしたスンマセン


という事はつまりyamlがクソって事だな


(クソでは)ないです。

散々ansibleでyamlに慣れていたつもりが、こんな基本的な仕様を実は知らなかった、という事を思い知らされましたね。

私はあまりこういう「仕様だから」という片付け方は好きではないので、仕組みで何とかしたいと考えています。気づけなかった理由の一つとしては、VisualStudioCodeで '\t' がシンタックスエラーとして検出されなかったというのがあります。もしエディタがエラーを教えてくれていれば、「ああ、こういう仕様なのね」と気づくきっかけになりますね。

では、3種類のエディタでそれぞれ比較してみましょう。

エディタはシンタックスエラーを教えてくれるか

Visual Studio Codeの場合

f:id:treeapps:20160821193114p:plain

残念!シンタックスエラーになりません。

Sublime Textの場合

f:id:treeapps:20160821193144p:plain

残念!シンタックスエラーになりません。

Atomの場合

f:id:treeapps:20160821193202p:plain

シンタックスエラーで色が変わっていて気づける!!

が、しかし、これはescapeの値に \ を設定した場合のみのようです。

f:id:treeapps:20160821194103p:plain

残念!他に\を入力した場合は気付ける、という状態のようです。


こうして比較してみると、Atomで特定の状態であればようやく気づける、という感じのようです。

主要エディタがこのyamlの仕様を考慮しきれずシンタックスエラーとして検知できないという事は、この仕様を知らない人は実行時エラーから「あ、これyamlの仕様か」と気づくしかありません。今回のように、もしそのStackTrace等のエラーからyamlの仕様に辿りつけない場合、キツイものがありますね。

雑感

今回の件で感じたのは

  • 特定の状況の時のみ特殊な設定が必要になるなら、その旨をちゃんと解りやすい場所に解りやすく記載すべし。
  • 特殊な設定の仕方以外の方法が無いか検討する。type: tsvに対応する等。
  • エラーの内容は内部実装を知らない人が見ても一目で何のエラーなのか解るようにすべし。

という事です。

個人的にはこういう細かい部分がしっかりしているもの程クオリティ高いなと思うので、自分が実装する時もこういうった事に注意していきたいですね。できるかどうか不明ですが、yamlのシンタックスのバリデーションの仕組みがあれば、それを入れる事ができれば解決できそうな問題だと思いました。

その辺を疎かにすると、折角素晴らしいプロダクトなのに「なんじゃこりゃ。意味不明なエラーでた。調査してる時間無いから使うのやめるわ」という事に繋がる事もあります。

ユーザは非常に怠惰なので、ちょっと使ってみて面倒臭そうと感じた瞬間放り投げる事もあるので、注意したいですね。