3 ポイント 投稿者 GN⁺ 2024-03-21 | 1件のコメント | WhatsAppで共有
  • Python re$ は、マルチラインモードがオフでも 文字列の末尾 だけでなく、末尾にある 最後の改行の直前 にもマッチする場合がある
  • ^ が「文字列の先頭」のように見えるからといって、$ も完全に対称的に動作すると考えてはいけない。実際の意味は 正規表現の実装ごと に異なる
  • "cat\n" に対する $\z\Z の結果は、PHP、ECMAScript、Python、Go、Java 8、.NET 7.0、Rust でそれぞれ異なり、Python の \zPython 3.14 で新たに追加された
  • 末尾の改行まで許容するなら、マルチラインモードの $ は表にあるすべてのプラットフォームで "cat\n" にマッチするが、改行を除いた末尾 だけにマッチさせるには構文の選択が変わる
  • 最後の改行にマッチさせたくない場合、多くのプラットフォームでは \z を使い、Python 3.14 より前と ECMAScript ではそれぞれ別の代替手段を検討する必要がある

Python re$ がマッチする位置

  • Python の正規表現モジュール re では、$ はマルチラインモードがオフでも 文字列の末尾、または文字列末尾の最後の改行の直前にマッチする場合がある
  • cat$"lolcat" にはマッチし、"internet cat video" にはマッチしないため単純に見えるが、"cat\n" のように末尾に改行があると予想と異なる可能性がある
  • re.MULTILINE が指定されると、$ は文字列の末尾と各行の末尾、つまり各改行の直前にマッチする
  • デフォルトでも $ は文字列の末尾にマッチし、文字列の末尾に改行がある場合はその改行の直前にもマッチする

最後の改行を除いてマッチさせる

  • 文字列の末尾だけに厳密にマッチさせたい場合、$ だけでは不十分なことがあり、\z\Z末尾アンカー の候補になる
  • Python 正規表現ドキュメント別の正規表現構文の説明 を基準に見ると、実装ごとに \z\Z の対応状況と意味が異なる
  • "cat\n" に対する違いは次のとおり
    • PHP: "cat$" はマルチラインかどうかに関係なくマッチし、"cat\z" はマッチせず、"cat\Z" はマッチする
    • ECMAScript: マルチラインの "cat$" はマッチし、マルチラインでない "cat$" はマッチせず、\z\Z はサポートされていない
    • Python: "cat$" はマルチラインかどうかに関係なくマッチし、"cat\z""cat\Z""cat\n" にマッチしない
    • Go と Rust: マルチラインの "cat$" はマッチし、マルチラインでない "cat$""cat\z" はマッチせず、\Z はサポートされていない
    • Java 8 と .NET 7.0: "cat$" はマルチラインかどうかに関係なくマッチし、"cat\z" はマッチせず、"cat\Z" はマッチする
  • Python の \zPython 3.14 で新たに追加 され、それ以前のバージョンではサポートされていなかった
  • 末尾の改行を許容するなら、マルチラインモードの $ は表にあるすべてのプラットフォームで一貫して "cat\n" にマッチする
  • 末尾の改行にマッチさせたくない場合、多くのプラットフォームでは \z を使い、Python 3.14 より前では \Z、ECMAScript ではマルチラインでない $ を使う必要がある
  • 表のデータは regex101.com で収集されたもので、実際のランタイムでテストしたものではない

1件のコメント

 
GN⁺ 2024-03-21
Hacker News のコメント
  • 昔から ^ は「行の先頭」、$ は「行の末尾」だと考えてきた
    正規表現を扱うときはテキストを行単位で処理することが多いので、結果は同じになることが多いが、その演算子を思い浮かべるときの感覚は今でも「文字列」より「行」に近い
    おそらく grep で正規表現に触れた影響が大きく、入力を文字列ではなく行として見る習慣がついたのだと思う

    • 私もタイトルを見て「そんなの当然違うでしょ、どこでそんな話を聞いたんだ?」と思った
      ほぼ20年正規表現を使ってきたが、$文字列の終端だという話は初めて聞いた気がするし、ずっと行末だと思っていた
    • 記事で ^ を「文字列の先頭」と言っているのが気になる
      実際には $ が「行の末尾」であるのと同じように、^ も「行の先頭」で、文字列の先頭は \A、文字列の末尾は \Z に近いように見える
    • 私もそう思っていたが、Perl で直接試してみると、$ はデフォルトでは文字列終端に対する肯定先読みアサーションのように動作する
      改行文字にマッチして消費するわけではない
      複数行モードでだけ改行位置にマッチするが、そのときも消費はしないようだ
      実際、$ を使って1行の最後の文字をキャプチャし、改行を消費してから次の行の最初の文字をキャプチャする正規表現は作れず、キャプチャグループは単に $ で終わってしまう
    • grep よりも Vim が私にそういう認識を植え付けた
  • POSIX 正規表現と Python の正規表現は違う
    一般に正規表現の構文は普遍的ではないので、使っている実装のドキュメントを見る必要がある
    POSIX 9章によれば、正規表現は文字列上で動作するが、一部のユーティリティは処理を行単位に制限している
    また $ はマッチ対象文字列の末尾に固定されるアンカーとされているため、結局 $ が文字列の末尾を意味するのか行末を意味するのかは、ユーティリティやモードが決める
    grepsedawk、Python のような一般的なツールはデフォルトで行単位に動作するため、通常は行末として扱う
    唯一の普遍的な正規表現構文など存在しない
    どの言語とオプションを使っているのか分からなければ、正規表現を安定して読んだり書いたりすることはできない
    https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1...

  • この話題なら、Robert Elder を知らない人に紹介するのにちょうどいい
    YouTube とブログで良いコンテンツを作っていて、正規表現シリーズでは複数のツールが実装している正規表現の挙動の違いをかなり深く掘り下げている
    最近の動画も良い: https://www.youtube.com/watch?v=ys7yUyyQA-Y
    HN 読者が興味を持ちそうなコンテンツも多く、コンサルティングの現実や苦労といったテーマも扱っている
    https://www.youtube.com/@RobertElderSoftware
    https://blog.robertelder.org/
    https://blog.robertelder.org/regular-expressions/
    https://www.youtube.com/watch?v=cK87ktENPrI

  • Perl を学んだとき、正規表現は初めて本当に身につけたものの一つで、今でも「Camel」本のおかげで Perl は心の片隅に居心地よく収まっている
    今いちばん重要な知識は実装ごとに違うという点なので、何か作業するたびに該当するリファレンス表を取り出す習慣がついた
    たとえば Emacs の正規表現は \w 形式の単語文字をサポートせず、\s_- のような文字クラスを使わなければならないので腹立たしいが、Emacs はドキュメント化と発見しやすさが最高だと思う
    ユーティリティによっては括弧のエスケープが必要で、別のものでは不要であり、この挙動が設定可能な場合もあればそうでない場合もある
    混乱、苛立ち、否認の段階をすべて通り過ぎて、今ではただ受け入れている
    概念はどこでも同じだが、方言が変わる

    • 私の頭は Perl 正規表現で考えていて、そのあと使っている言語の一貫性のない部分に合わせて翻訳している
      特にシェルでは、sed/grep/awk が GNU なのか BSD なのかまで思い出すくらいなら、パイプラインに perl を挟むことのほうがずっと多い
    • どうやって身につけたのか気になる
      Perl は猫がキーボードを踏んだように見える
  • 大勢のダメな採用担当者が「正規表現で文字列の終端はどうやってマッチしますか?」をひっかけ質問リストに追加する音が聞こえるようだ

  • 正規表現関連のリストから Perl を外すのは変だ
    perlre のドキュメントでは $ はこう説明されている: 文字列の終端にマッチする、または文字列終端の改行の前にマッチする、あるいは /m を使うと任意の改行の前にマッチする

    • 正規表現と最も強く結びついた言語と言える Perl を抜かしたのは、かなり大きな漏れに見える
      それだけ最近の Perl が関心の外に追いやられているということでもありそうだ
  • Raku、以前の Perl 6 は ^$文字列の先頭/末尾と定め、^^$$ を行の先頭/末尾として導入した
    複数行モードはなく、必要でもない
    \h は水平空白、\v は垂直空白も表す
    完全に考え直して書き直したおかげで、以前の挙動が人々を驚かせていたという事実から学べたのが利点だった

    • だからこの頑固者には Perl 6 は使えない
      何十年もかけて身につけたラインノイズのような構文をランダムに混ぜ合わせた感じがする
      デフォルトは逆のほうが明確だったはず
      ^$ は行に、^^$$ は文字列に使うほうが自然だったと思う
      ^^line1$\n^line2$\n^line3$\n$ のように見えるからだ
      それに Perl 6 はどこにでもあるわけではないが、Perl 5 はどこにでもある
    • 自分なら正確に逆を選んだと思う
      ^^ のほうが ^ よりも「先頭っぽく」見える
    • 自分が書いた正規表現はほとんどすべて文字列の先頭/末尾を前提にしていた
      たいてい行を正規表現に渡して処理するからで、単独の ^$ を文字列全体に使う選択は、ある程度の後方互換性を保ってくれる
  • 正規表現が標準化されていると考える人がいるのか疑問
    新しい環境に移るたびに、いつも学び直す必要があった

    • ある時点では、自分はすべての方言を知っていると感じていた
      もっと多くの正規表現方言はあるだろうが遭遇せず、自分の知っている範囲で大半は解決できる
      レンタカーの運転に似ている
      自分の車とは少し動きが違い、ない機能や追加された機能もあるが、全体としてはたいていかなり似ている
    • ISO/IEC 14882 C++ 標準ライブラリは、6 種類の事実上の法定標準である正規表現構文の実装を要求している: IEEE Std 1003.1-2008、つまり POSIX の BRE、ERE、awk、grep、egrep と ECMA-262 EcmaScript 3
      だから少なくとも私は、正規表現は複数の公開された公式標準として標準化されていると見ている
      https://open-std.org/jtc1/sc22/…
      https://pubs.opengroup.org/onlinepubs/9699919799/…
      https://262.ecma-international.org/14.0/…
    • 私が知っている大きな系統は POSIX、Perl/PCRE、そして Go 周辺で使われる RE2 くらい
      JavaScript を含め多くのシステムが PCRE を実装したのは、Perl が POSIX 体系に有用な拡張を多数加えたためだ
      記憶では、RE2 は既存体系の性能問題や特異な挙動を抑えようとするもので、全体が Go で実装されたものだと思っていた
      後で見たら、RE2 が Go より先に出ていたとは知らなかった
    • Perl 以後に出た言語はおおむね Perl 正規表現構文の何らかの変種を使うが、常に細かな違いがある
      それでも $ の意味と複数行モードへ切り替える方法は、通常は一貫しているほうだ
    • 興味深いことに、RFC 9485 https://datatracker.ietf.org/doc/rfc9485/ “I-Regexp: An Interoperable Regular Expression Format” が昨年 10 月に出たばかりだ
  • 人々は文字列と行を混同している
    文字列は文字のシーケンスであり、行は二通りに考えられる
    改行を行終端子と見るなら、行は改行でない文字 0 個以上に改行が付いたもので、末尾に改行がなければ完全な行ではない
    POSIX はこの見方を採用している
    改行を行区切りと見るなら、行は改行でない文字 0 個以上のシーケンスである
    どちらの場合でも、行の内容は改行の手前で終わる
    ^$ の意味論は、単一行モードでも複数行モードでも行ベースである
    文字列ベースの意味論、ファイルを扱うならファイル全体の意味論と見なせる場合には、\A\Z またはそれに相当するものを使うべきだ
    どちらの解釈にも利点がある
    シリアル接続でテキストを送るときは、改行を行終端子にしておくと完全な行を受け取ったかどうか分かりやすい
    テキストファイルでは、改行を行区切りと見るほうが最後の行が不正な状態にならず便利なことがあるが、行終端子を置けば不完全に書かれた行を検出できる

  • このため Ruby ベースのアプリで深刻なバグが何度か起きた
    常に \A\z を使うべき
    https://homakov.blogspot.com/2012/05/saferweb-injects-in-var...
    https://sakurity.com/blog/2015/02/28/openuri.html
    https://sakurity.com/blog/2015/06/04/mongo_ruby_regexp.html