web-dev-qa-db-ja.com

単一の正規表現で繰り返しパターンを折りたたみ、キャプチャする

文字列から多数のトークンをキャプチャする必要がある状況に何度も遭遇し、無数の試行の後、プロセスを簡略化する方法を見つけることができませんでした。

したがって、テキストが次のとおりだとします。

start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end

この例では内部に8つのアイテムがありますが、3〜10のアイテムを含めることができます。

私はこのようなものが理想的です:
start:(?:(\w+)-?){3,10}:endすてきでクリーンですが、最後の一致のみをキャプチャします。 こちらをご覧ください

私は通常、このようなものを単純な状況で使用します。

start:(\w+)-(\w+)-(\w+)-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?:end

最大10個の制限があるため、3つのグループは必須で、別の7つのグループはオプションですが、これは「見栄えがよくない」ように見え、最大制限が100であり、一致がより複雑である場合、記述して追跡するのは面倒です。 デモ

そして、私がこれまでにできる最高のこと:

start:(\w+)-((?1))-((?1))-?((?1))?-?((?1))?-?((?1))?-?((?1))?-?((?1))?:end

特に、試合が複雑でも長い場合は短くなります。 デモ

誰もそれを1正規表現のみのソリューションとしてプログラミングなしで機能させることができましたか

PCREでこれをどのように行うことができるかについて主に興味がありますが、他のフレーバーも大丈夫です。

更新:

目的は、OS /ソフトウェア/プログラミング言語の制限なしに、一致を検証し、RegExだけでmatch 0内の個々のトークンをキャプチャすることです

アップデート2(バウンティ):

@nhahtdhの助けを借りて、\Gを使用して以下のRegExpに行きました:

(?:start:(?=(?:[\w]+(?:-|(?=:end))){3,10}:end)|(?!^)\G-)([\w]+)

デモ さらに短いが、コードを繰り返すことなく説明できる

ECMAフレーバーにも興味があります。これは\Gをサポートしていないため、特に/g修飾子を使用せずに別の方法があるかどうか疑問に思います。

32
CSᵠ

これを最初に読んでください!

この投稿は、問題に対する「すべての正規表現」アプローチを推奨するのではなく、可能性を示すためのものです。著者は3〜4種類のバリエーションを作成しましたが、それぞれに現在の解決策に到達する前に検出が難しい微妙なバグがあります。

具体的な例として、一致の確認や区切り文字に沿った一致の分割など、より保守しやすい他のより良い解決策があります。

この投稿では、具体的な例を扱います。完全な一般化が可能かどうかは本当に疑わしいですが、背後にある考え方は、同様のケースで再利用できます。

概要

  • .NETは CaptureCollection クラスで繰り返しパターンのキャプチャをサポートします。
  • \Gおよび後読みをサポートする言語の場合、グローバルマッチング関数で動作する正規表現を構築できる場合があります。それを完全に正しく書くことは簡単ではなく、微妙なバグのある正規表現を書くことは簡単ではありません。
  • \Gおよび後読みサポートのない言語の場合:単一の一致の後に入力文字列を選択することにより、\G^でエミュレートすることが可能です。 (この回答ではカバーされていません)。

解決

このソリューションは、正規表現エンジンが\G一致境界、先読み(?=pattern)、および後読み(?<=pattern)をサポートしていることを前提としています。 Java、Perl、PCRE、.NET、Ruby=正規表現のフレーバーは、上記の高度な機能をすべてサポートしています。

ただし、.NETでは正規表現を使用できます。 .NETは CaptureCollection クラスを介して繰り返されるキャプチャグループと一致するすべてのインスタンスのキャプチャをサポートしているため。

あなたのケースでは、\G一致境界を使用して、1つの正規表現で行うことができ、先読みを繰り返し回数を制限します。

(?:start:(?=\w+(?:-\w+){2,9}:end)|(?<=-)\G)(\w+)(?:-|:end)

[〜#〜] demo [〜#〜] 。構築は\w+-が繰り返され、次に\w+:endが繰り返されます。

(?:start:(?=\w+(?:-\w+){2,9}:end)|(?!^)\G-)(\w+)

[〜#〜] demo [〜#〜] 。最初のアイテムの構造は\w+で、次に-\w+が繰り返されます。 (提案のためにkaᵠに感謝します)。この構成は、代替が少ないため、その正しさについて推論するのが簡単です。

\G一致境界は、トークン化を行う必要がある場合に特に役立ちます。この場合、エンジンがスキップせず、無効であるはずのものを一致させないようにする必要があります。

説明

正規表現を分解してみましょう:

(?:
  start:(?=\w+(?:-\w+){2,9}:end)
    |
  (?<=-)\G
)
(\w+)
(?:-|:end)

認識する最も簡単な部分は、最後の前の行の(\w+)です。これは、キャプチャするWordです。

最後の行も非常に簡単に認識できます。一致する単語の後には-または:endが続く場合があります。

正規表現に文字列のどこからでも自由にマッチングを開始を許可します。つまり、start:...:endは文字列のどこにでも、何度でも出現できます。正規表現は単にすべての単語に一致します。返された配列を処理して、一致したトークンが実際にどこから来たかを区別する必要があります。

説明については、正規表現の先頭で文字列start:の存在を確認し、次の先読みでは単語数が指定された制限内にあり、:endで終了していることを確認します。 それとも、または前の一致の前の文字が-であることを確認し、前の一致から続行します。

他の構造の場合:

(?:
  start:(?=\w+(?:-\w+){2,9}:end)
    |
  (?!^)\G-
)
(\w+)

start:\w+という形式の繰り返しに一致する前に、最初に-\w+に一致することを除いて、すべてがほぼ同じです。最初の構造とは対照的に、最初にstart:\w+-を照合し、\w+-(または最後の繰り返しの場合は\w+:end)の繰り返しインスタンスを照合します。

文字列の途中で一致するようにこの正規表現を機能させるのは非常に困難です。

  • start::endの間の単語数をチェックする必要があります(元の正規表現の要件の一部として)。

  • \Gも文字列の先頭に一致します! (?!^)は、この動作を防ぐために必要です。この処理を行わないと、start:がないときに正規表現が一致を生成する可能性があります。

    最初の構成では、後読み(?<=-)がこのケースをすでに防止しています((?!^)(?<=-)によって暗示されます)。

  • 最初の構築(?:start:(?=\w+(?:-\w+){2,9}:end)|(?<=-)\G)(\w+)(?:-|:end)の場合、:end以降の面白いものに一致しないようにする必要があります。後読みはその目的のためです::endの後のガベージが一致しないようにします。

    2番目の構成ではこの問題は発生しません。すべてのトークンを一致させた後、::endの)でスタックするためです。

検証バージョン

入力文字列が形式に従うことを検証する場合(前後に余分なものはありません)、andデータを抽出し、アンカーを次のように追加できます。

(?:^start:(?=\w+(?:-\w+){2,9}:end$)|(?!^)\G-)(\w+)
(?:^start:(?=\w+(?:-\w+){2,9}:end$)|(?!^)\G)(\w+)(?:-|:end)

(後読みも必要ありませんが、(?!^)が文字列の先頭と一致しないようにするには、\Gが必要です)。

建設

繰り返しのすべてのインスタンスをキャプチャする必要があるすべての問題について、正規表現を変更する一般的な方法は存在しないと思います。変換する "ハード"(または不可能?)ケースの1つの例は、特定の条件を満たすために繰り返しが1つ以上のループをバックトラックする必要がある場合です。

元の正規表現が入力文字列全体(検証タイプ)を記述している場合、通常、文字列の中央から一致しようとする正規表現(マッチングタイプ)に比べて変換が簡単です。ただし、元の正規表現と常に一致させることができ、一致する型の問題は検証型の問題に変換されます。

次の手順を実行して、このような正規表現を作成します。

  • 繰り返す前の部分をカバーする正規表現を記述します(例:start:)。これをprefix regexと呼びましょう。
  • 最初のインスタンスを一致させてキャプチャします。 (例:(\w+)
    (この時点で、最初のインスタンスと区切り文字は一致しているはずです)
  • 代替として\Gを追加します。通常は、文字列の先頭と一致しないようにする必要もあります。
  • 区切り文字(ある場合)を追加します。 (例:-
    (このステップの後、残りのトークンも一致しているはずですが、最後の多分を除きます)
  • 繰り返し後のパーツを覆うパーツを追加します(必要な場合)(例::end)。繰り返しの後の部分を呼びましょうsuffix regex(これを構造に追加するかどうかは関係ありません)。
  • 今、難しい部分です。次のことを確認する必要があります:
    • prefix regex以外に、マッチを開始する方法は他にありません。 \Gブランチに注意してください。
    • 一致を開始する方法はありませんaftersuffix regexが一致しました。 \Gブランチがどのようにマッチを開始するかに注意してください。
    • 最初の構築では、サフィックスregex(例::end)とデリミタ(例:-)を交互に使用する場合は、サフィックスregexをデリミタとして許可しないようにしてください。
36
nhahtdh

理論的には単一の式を書くことは可能かもしれませんが、最初に外側の境界を一致させてから、内側の部分で分割を実行する方がはるかに実用的です。

ECMAScriptでは、次のように記述します。

'start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end'
    .match(/^start:([\w-]+):end$/)[1] // match the inner part
    .split('-') // split inner part (this could be a split regex as well)

PHPの場合:

$txt = 'start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end';
if (preg_match('/^start:([\w-]+):end$/', $txt, $matches)) {
    print_r(explode('-', $matches[1]));
}
6
Ja͢ck

もちろん、この引用文字列で正規表現を使用できます。

"(?<a>\\w+)-(?<b>\\w+)-(?:(?<c>\\w+)" \
"(?:-(?<d>\\w+)(?:-(?<e>\\w+)(?:-(?<f>\\w+)" \
"(?:-(?<g>\\w+)(?:-(?<h>\\w+)(?:-(?<i>\\w+)" \
"(?:-(?<j>\\w+))?" \
")?)?)?" \
")?)?)?" \
")"

それは良い考えですか?いいえ、そうは思いません。

1
minopret

その方法でそれを行うことができるかどうかはわかりませんが、グローバルフラグを使用して、コロンの間のすべての単語を検索できます。以下を参照してください。

http://regex101.com/r/gK0lX1

ただし、グループの数を自分で検証する必要があります。グローバルフラグがないと、すべての一致ではなく、単一の一致のみが取得されます。{3,10}{1,5}に変更すると、代わりに「sir」という結果が得られます。

import re

s = "start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end"
print re.findall(r"(\b\w+?\b)(?:-|:end)", s)

作り出す

['test', 'test', 'lorem', 'ipsum', 'sir', 'doloret', 'etc', 'etc', 'something']

0
spiralx

組み合わせると:

  1. あなたの観察:単一のキャプチャグループのあらゆる種類の繰り返しは、最後のキャプチャの上書きをもたらし、キャプチャグループの最後のキャプチャのみを返します。
  2. 知識:全体ではなく、パーツに基づいてキャプチャを行うと、正規表現エンジンが繰り返す回数に制限を設定することができなくなります。制限はメタデータでなければなりません(正規表現ではありません)。
  3. 答えがプログラミング(ループ)を含むことができないという要件ではなく、質問で行ったようにキャプチャグループを単にコピーアンドペーストすることを含む答え。

それができないと推論することができます。

更新:pの正規表現エンジンがいくつかあります。 1は必ずしも真ではありません。その場合、指定した正規表現start:(?:(\w+)-?){3,10}:endがジョブを実行します( source )。

0