csv形式は正規表現で定義できますか?


19

同僚と私は最近、純粋な正規表現がcsv形式を完全にカプセル化できるかどうかを議論しました。これにより、任意のエスケープ文字、引用文字、および区切り文字を含むすべてのファイルを解析できます。

正規表現は、作成後にこれらの文字を変更できる必要はありませんが、他のエッジケースで失敗してはなりません。

これはトークナイザーだけでは不可能だと主張しました。これを行うことができる唯一の正規表現は、トークン化だけでなく、非常に複雑なPCREスタイルです。

私は次のラインに沿って何かを探しています:

... csv形式はコンテキストのない文法であるため、正規表現のみで解析することは不可能です...

それとも私は間違っていますか?POSIX正規表現だけでcsvを解析することは可能ですか?

たとえば、エスケープ文字と引用文字の両方がの"場合、これらの2行は有効なcsvです。

"""this is a test.""",""
"and he said,""What will be, will be."", to which I replied, ""Surely not!""","moving on to the next field here..."

どこにもネストがないため、CSVではありません(IIRC)
ラチェットフリーク

1
しかし、エッジケースは何ですか?多分、CSVには、私が思っていた以上のことがあるのでしょうか?
c69

1
@ C69どのようにエスケープと引用符文字についての両方です"。そして、以下が有効です:"""this is a test.""",""
スペンサーRATHBUN

ここから正規表現を試しましか?
-dasblinkenlight

1
あなたはエッジケースに注意する必要がありますが、正規表現はあなたがそれを説明したようにcsvをトークン化できるはずです。正規表現は、任意の数の引用符をカウントアップする必要はありません-正規表現で可能な3カウントまでです。他の人が述べたように、csvトークンが期待するものの明確に定義された表現を書き留めてみるべきです
...-comingstorm

回答:


20

理論的には素晴らしいが、実際にはひどい

CSV Iで説明したように規則を意味と仮定するつもりですRFC 4180

基本的なCSVデータを照合するのは簡単ですが、

"data", "more data"

注:ところで、.split( '/ n')。split( '"')関数を使用すると、このような非常にシンプルで適切に構造化されたデータを使用する方がはるかに効率的です。正規表現はNDFSM(非決定性有限ステートマシン)は、エスケープ文字などのエッジケースの追加を開始すると、バックトラッキングに多くの時間を無駄にします。

たとえば、私が見つけた文字列に一致する最も包括的な正規表現は次のとおりです。

re_valid = r"""
# Validate a CSV string having single, double or un-quoted values.
^                                   # Anchor to start of string.
\s*                                 # Allow whitespace before value.
(?:                                 # Group for value alternatives.
  '[^'\\]*(?:\\[\S\s][^'\\]*)*'     # Either Single quoted string,
| "[^"\\]*(?:\\[\S\s][^"\\]*)*"     # or Double quoted string,
| [^,'"\s\\]*(?:\s+[^,'"\s\\]+)*    # or Non-comma, non-quote stuff.
)                                   # End group of value alternatives.
\s*                                 # Allow whitespace after value.
(?:                                 # Zero or more additional values
  ,                                 # Values separated by a comma.
  \s*                               # Allow whitespace before value.
  (?:                               # Group for value alternatives.
    '[^'\\]*(?:\\[\S\s][^'\\]*)*'   # Either Single quoted string,
  | "[^"\\]*(?:\\[\S\s][^"\\]*)*"   # or Double quoted string,
  | [^,'"\s\\]*(?:\s+[^,'"\s\\]+)*  # or Non-comma, non-quote stuff.
  )                                 # End group of value alternatives.
  \s*                               # Allow whitespace after value.
)*                                  # Zero or more additional values
$                                   # Anchor to end of string.
"""

単一引用符と二重引用符で囲まれた値を合理的に処理しますが、値の改行、エスケープされた引用符などは処理しません。

ソース:スタックオーバーフロー-JavaScriptで文字列を解析する方法

一般的なエッジケースが次のように導入されると、悪夢になります...

"such as ""escaped""","data"
"values that contain /n newline chars",""
"escaped, commas, like",",these"
"un-delimited data like", this
"","empty values"
"empty trailing values",        // <- this is completely valid
                                // <- trailing newline, may or may not be included

改行としてのエッジケースだけで、野生で見つかったRegExベースのパーサーの99.9999%を破るのに十分です。唯一の「合理的な」代替手段は、高レベルの分析に使用されるステートマシンとペアになった基本制御/非制御文字(つまり、端末と非端末)のトークン化にRegExマッチングを使用することです。

出典:広範な痛みと苦痛として知られている経験。

私はjquery-CSVの著者であり、世界で唯一のJavaScriptベースで完全にRFCに準拠したCSVパーサーです。私は何ヶ月もこの問題に取り組み、多くの知的な人々と話し、コアパーサーエンジンの3つの完全な書き直しを含む異なる実装を試してみました。

tl; dr-話の教訓、PCREだけで、最も単純で厳密な通常の(Ie Type-III)文法以外の構文解析は得策ではありません。とはいえ、終端文字列と非終端文字列のトークン化には便利です。


1
うん、それは私の経験でもありました。非常に単純なCSVパターン以上のものを完全にカプセル化しようとすると、これらの問題にぶつかり、大規模な正規表現の効率性の問題と複雑さの問題の両方に突き当たります。node-csvライブラリを見ましたか?この理論も検証しているようです。自明ではない実装はすべて、内部でパーサーを使用します。
スペンサーラスブン

@SpencerRathbunうん。以前にnode-csvのソースを見たことがあると思います。処理には典型的な文字トークン化状態マシンを使用するようです。jquery-csvパーサーは、端末/非端末トークン化に正規表現を使用することを除いて、同じ基本概念で機能します。文字ごとに評価および連結する代わりに、regexは複数の非終端文字を一度に照合し、それらをグループ(つまり文字列)として返すことができます。これにより、不要な連結が最小限に抑えられ、効率が向上するはずです。
エヴァンプライス

20

正規表現は通常の言語を解析でき、再帰的な文法のような派手なものは解析できません。しかし、CSVはかなり規則的なようで、正規表現で解析できます。

定義から作業してみましょう。許可されているのは、シーケンス、選択肢からの選択(|)、繰り返し(Kleene star、the *)です。

  • 引用符で囲まれていない値は通常です:[^,]*#カンマ以外の任意の文字
  • 引用された値は規則的です:"([^\"]|\\\\|\\")*"#引用"またはエスケープされた引用\"またはエスケープされたエスケープ以外のシーケンス\\
    • 一部のフォームには、("")*"上記の式にバリアントを追加する、引用符による引用符のエスケープが含まれる場合があります。
  • 許可される値は通常です:<unquoted-value> |<quoted-value>
  • 単一のCSV行は通常です:<値> (,<値>)*
  • で区切られた一連の行\nも明らかに規則的です。

私はこれらの表現のそれぞれを綿密にテストせず、キャッチグループを定義しませんでした。私はまた、いくつかの代わりに使用できる文字の変形のような専門的、上で練り,"または行区切り:これらは、規則性を壊さない、あなただけのいくつかのわずかに異なる言語を取得します。

この証明で問題を発見できる場合は、コメントしてください!:)

しかし、これにもかかわらず、純粋な正規表現によるCSVファイルの実用的な解析には問題がある場合があります。どのバリアントがパーサーに送られているかを知る必要がありますが、そのための標準はありません。1行が成功するまで各行に対して複数のパーサーを試すか、形式フォームのコメントを分割することができます。しかし、これには、正規表現以外の手段が効率的に、またはまったく必要になる場合があります。


4
実用的なポイントについては絶対に+1。私が確信していることは、どこか深いところに、引用された値のバージョンを壊す(推測された)値の例がありますが、それが何であるかはわかりません。複数のパーサーでの「楽しい」は、「これらの2つの作業ですが、異なる答えを与える」

1
バックスラッシュエスケープ引用符と二重引用符エスケープ引用には、明らかに異なる正規表現が必要です。前者のタイプのcsvフィールドの正規表現はのよう[^,"]*|"(\\(\\|")|[^\\"])*"であるべきです、そして、後者はのようであるべき[^,"]*|"(""|[^"])*"です。(私はこれらのいずれもテストしていないので注意してください!)
comingstorm

用ハンティング何か囲まれたレコード区切り文字と値-標準のかもしれないが、見逃している場合があります。また、これを処理するための複数の異なる方法がある場合、実用的な解析がさらに楽しくなります

いい答えですが、実行perl -pi -e 's/"([^\"]|\\\\|\\")*"/yay/'してパイプインすると"I have here an item,\" that is a test\""、結果は「いや、それはテストです」です。正規表現に欠陥があると考えます。
スペンサーラス

@SpencerRathbun:時間があれば、実際に正規表現をテストし、おそらくテストに合格する概念実証コードを貼り付けます。申し訳ありませんが、仕事の日は続いています。
9000

5

簡単な答え-おそらくそうではありません。

最初の問題は、標準の欠如です。厳密に定義された方法でcsvを記述することはできますが、厳密に定義されたcsvファイルを取得することは期待できません。「あなたは何をするかに保守的であり、他人から受け入れるものに寛大であること」-ジョン・ポスタル

許容できる標準的なスタイルを持っていると仮定すると、エスケープ文字と、これらのバランスを取る必要があるかどうかの問題があります。

多くのcsv形式の文字列はとして定義されstring value 1,string value 2ます。ただし、その文字列にカンマが含まれている場合は、"string, value 1",string value 2です。引用符が含まれている場合、になり"string, ""value 1""",string value 2ます。

この時点では不可能だと思います。問題は、読み込んだ引用符の数と、値の二重引用符モードの内側または外側にコンマがあるかどうかを判断する必要があることです。括弧のバランスを取ることは、不可能な正規表現の問題です。一部の拡張正規表現エンジン(PCRE)はそれを処理できますが、それは正規表現ではありません。

/programming/8629763/csv-parsing-with-a-context-free-grammarが役立つ場合があります。


修正済み:

私はエスケープ文字のフォーマットを見てきましたが、任意のカウントを必要とするものは見つかりませんでした-それはおそらく問題ではありません。

ただし、エスケープ文字とレコード区切り文字(最初から)の問題があります。 http://www.csvreader.com/csv_format.phpは、野生のさまざまな形式に関する優れた資料です。

  • 引用符付き文字列のルール(単一引用符付き文字列または二重引用符付き文字列の場合)は異なります。
    • 'This, is a value'"This, is a value"
  • エスケープ文字の規則
    • "This ""is a value""""This \"is a value\""
  • 埋め込みレコード区切り文字({rd})の処理
    • (生埋め込み)"This {rd}is a value"vs(エスケープ)"This \{rd}is a value"vs(翻訳済み)"This {0x1C}is a value"

ここで重要なことは、常に複数の有効な解釈を持つ文字列を使用できることです。

関連する質問(エッジケースの場合)「受け入れられる無効な文字列を持つことは可能ですか?」

あるアプリケーションによって作成されたすべての有効なCSVと一致し、解析できないすべてのcsvを拒否できる正規表現があることを、私はまだ強く疑っています。


1
引用符内の引用符のバランスをとる必要はありません。代わりに、埋め込み引用符の前に偶数の引用符が必要です("")*"。これは明らかに規則的です。値のの相場がオフバランスである場合、それはすでに私たちのビジネスではありません。
9000

これは私の過去の「データ転送」の恐ろしい言い訳にぶつかった私の立場です。それらを適切に処理したのはパーサーだけで、純粋な正規表現は数週間ごとに壊れていました。
スペンサー

2

最初にCSVの文法を定義し(テキストに表示される場合、フィールド区切り記号は何らかの方法でエスケープまたはエンコードされますか?)、正規表現で解析可能かどうかを判断できます。最初の文法:2番目のパーサー:http : //www.boyet.com/articles/csvparser.htmlこのメソッドはトークナイザーを使用することに注意する必要があります-しかし、すべてのエッジケースに一致するPOSIX正規表現を構築することはできません。CSV形式の使用が不規則で、文脈に依存しない場合...あなたの答えはあなたの質問にあります。概要:http : //nikic.github.com/2012/06/15/The-true-power-of-regular-expressions.html


2

RFCで説明されているように、この正規表現は通常のCSVをトークン化できます。

/("(?:[^"]|"")*"|[^,"\n\r]*)(,|\r?\n|\r)/

説明:

  • ("(?:[^"]|"")*"|[^,"\n\r]*) -引用符付きまたは引用符なしのCSVフィールド
    • "(?:[^"]|"")*" -引用されたフィールド。
      • [^"]|""-各文字はでないか"、または"エスケープされています""
    • [^,"\n\r]* -引用符で囲まれていないフィールド。 , " \n \r
  • (,|\r?\n|\r)-次の区切り文字、,または改行
    • \r?\n|\r -改行、 \r\n \n \r

この正規表現を繰り返し使用して、CSVファイル全体を照合および検証できます。次に、引用符で囲まれたフィールドを修正し、区切り文字に基づいて行に分割する必要があります。

正規表現に基づいたJavaScriptのCSVパーサーのコードは次のとおりです。

var csv_tokens_rx = /("(?:[^"]|"")*"|[^,"\n\r]*)(,|\r?\n|\r)/y;
var csv_unescape_quote_rx = /""/g;
function csv_parse(s) {
    if (s && s.slice(-1) != '\n')
        s += '\n';
    var ok;
    var rows = [];
    var row = [];
    csv_tokens_rx.lastIndex = 0;
    while (true) {
        ok = csv_tokens_rx.lastIndex == s.length;
        var m = s.match(csv_tokens_rx);
        if (!m)
            break;
        var v = m[1], d = m[2];
        if (v[0] == '"') {
            v = v.slice(1, -1);
            v = v.replace(csv_unescape_quote_rx, '"');
        }
        if (d == ',' || v)
            row.push(v);
        if (d != ',') {
            rows.push(row)
            row = [];
        }
    }
    return ok ? rows : null;
}

この答えがあなたの議論を解決するのに役立つかどうかは、あなたが決めることです。小さくてシンプルで正しいCSVパーサーがあればうれしいです。

私の意見では、lexプログラムは多かれ少なかれ大きな正規表現であり、それらはCプログラミング言語などのはるかに複雑な形式をトークン化できます。

RFC 4180定義を参照して:

  1. 改行(CRLF)-正規表現はより柔軟で、CRLF、LF、またはCRを使用できます。
  2. ファイル内の最後のレコードには、終了改行がある場合とない場合があります-正規表現には最終改行が必要ですが、パーサーはそのために調整します。
  3. オプションのヘッダー行があるかもしれません-これはパーサーに影響を与えません。
  4. 各行には、ファイル全体で同じ数のフィールドが含まれている必要があります-強制されていない
    スペースはフィールドの一部と見なされ、無視されるべきでは
    ありません-OK
  5. 各フィールドは、二重引用符で囲まれていてもいなくてもかまいません...-わかりました
  6. 改行(CRLF)、二重引用符、およびコンマを含むフィールドは二重引用符で囲む必要があります-OK
  7. フィールド内に現れる二重引用符は、その前に別の二重引用符を付けてエスケープする必要があります-わかりました

正規表現自体は、RFC 4180要件のほとんどを満たしています。他の人には同意しませんが、パーサーを調整してそれらを実装するのは簡単です。


1
より多くの自己宣伝のような問題に取り組むよりも、このルックスは、尋ね見答えるためにどのように
GNAT

1
@gnat、私は答えを編集してより多くの説明を与え、RFC 4180に対して正規表現をチェックし、自己促進を少なくしました。Excelや他のスプレッドシートで使用される最も一般的な形式のCSVをトークン化できるテスト済みの正規表現が含まれているため、この回答には価値があると思います。これで問題が解決すると思います。小さなCSVパーサーは、この正規表現を使用してCSVを簡単に解析できることを示しています。
サムワトキンス

過度に自分自身を宣伝したくはありませんが、ここに小さなスプレッドシートアプリの一部として使用している完全な小さなcsvおよびtsvライブラリがあります(Googleシートは私には重すぎます)。これは、私が公開するすべてのものと同様のオープンソース/パブリックドメイン/ CC0コードです。これが他の誰かに役立つことを願っています。sam.aiki.info/code/js
サムワトキンス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.