正規表現の $ は常に「文字列の末尾」ではない
(sethmlarson.dev)- Python
reの$は、マルチラインモードがオフでも 文字列の末尾 だけでなく、末尾にある 最後の改行の直前 にもマッチする場合がある ^が「文字列の先頭」のように見えるからといって、$も完全に対称的に動作すると考えてはいけない。実際の意味は 正規表現の実装ごと に異なる"cat\n"に対する$、\z、\Zの結果は、PHP、ECMAScript、Python、Go、Java 8、.NET 7.0、Rust でそれぞれ異なり、Python の\zは Python 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"はマッチする
- PHP:
- Python の
\zは Python 3.14 で新たに追加 され、それ以前のバージョンではサポートされていなかった - 末尾の改行を許容するなら、マルチラインモードの
$は表にあるすべてのプラットフォームで一貫して"cat\n"にマッチする - 末尾の改行にマッチさせたくない場合、多くのプラットフォームでは
\zを使い、Python 3.14 より前では\Z、ECMAScript ではマルチラインでない$を使う必要がある - 表のデータは regex101.com で収集されたもので、実際のランタイムでテストしたものではない
1件のコメント
Hacker News のコメント
昔から
^は「行の先頭」、$は「行の末尾」だと考えてきた正規表現を扱うときはテキストを行単位で処理することが多いので、結果は同じになることが多いが、その演算子を思い浮かべるときの感覚は今でも「文字列」より「行」に近い
おそらく
grepで正規表現に触れた影響が大きく、入力を文字列ではなく行として見る習慣がついたのだと思うほぼ20年正規表現を使ってきたが、
$が文字列の終端だという話は初めて聞いた気がするし、ずっと行末だと思っていた^を「文字列の先頭」と言っているのが気になる実際には
$が「行の末尾」であるのと同じように、^も「行の先頭」で、文字列の先頭は\A、文字列の末尾は\Zに近いように見える$はデフォルトでは文字列終端に対する肯定先読みアサーションのように動作する改行文字にマッチして消費するわけではない
複数行モードでだけ改行位置にマッチするが、そのときも消費はしないようだ
実際、
$を使って1行の最後の文字をキャプチャし、改行を消費してから次の行の最初の文字をキャプチャする正規表現は作れず、キャプチャグループは単に$で終わってしまうgrepよりも Vim が私にそういう認識を植え付けたPOSIX 正規表現と Python の正規表現は違う
一般に正規表現の構文は普遍的ではないので、使っている実装のドキュメントを見る必要がある
POSIX 9章によれば、正規表現は文字列上で動作するが、一部のユーティリティは処理を行単位に制限している
また
$はマッチ対象文字列の末尾に固定されるアンカーとされているため、結局$が文字列の末尾を意味するのか行末を意味するのかは、ユーティリティやモードが決めるgrep、sed、awk、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 はドキュメント化と発見しやすさが最高だと思うユーティリティによっては括弧のエスケープが必要で、別のものでは不要であり、この挙動が設定可能な場合もあればそうでない場合もある
混乱、苛立ち、否認の段階をすべて通り過ぎて、今ではただ受け入れている
概念はどこでも同じだが、方言が変わる
特にシェルでは、
sed/grep/awkが GNU なのか BSD なのかまで思い出すくらいなら、パイプラインにperlを挟むことのほうがずっと多いPerl は猫がキーボードを踏んだように見える
大勢のダメな採用担当者が「正規表現で文字列の終端はどうやってマッチしますか?」をひっかけ質問リストに追加する音が聞こえるようだ
正規表現関連のリストから Perl を外すのは変だ
perlreのドキュメントでは$はこう説明されている: 文字列の終端にマッチする、または文字列終端の改行の前にマッチする、あるいは/mを使うと任意の改行の前にマッチするそれだけ最近の Perl が関心の外に追いやられているということでもありそうだ
Raku、以前の Perl 6 は
^と$を文字列の先頭/末尾と定め、^^と$$を行の先頭/末尾として導入した複数行モードはなく、必要でもない
\hは水平空白、\vは垂直空白も表す完全に考え直して書き直したおかげで、以前の挙動が人々を驚かせていたという事実から学べたのが利点だった
何十年もかけて身につけたラインノイズのような構文をランダムに混ぜ合わせた感じがする
デフォルトは逆のほうが明確だったはず
^と$は行に、^^と$$は文字列に使うほうが自然だったと思う^^line1$\n^line2$\n^line3$\n$のように見えるからだそれに Perl 6 はどこにでもあるわけではないが、Perl 5 はどこにでもある
^^のほうが^よりも「先頭っぽく」見えるたいてい行を正規表現に渡して処理するからで、単独の
^と$を文字列全体に使う選択は、ある程度の後方互換性を保ってくれる正規表現が標準化されていると考える人がいるのか疑問
新しい環境に移るたびに、いつも学び直す必要があった
もっと多くの正規表現方言はあるだろうが遭遇せず、自分の知っている範囲で大半は解決できる
レンタカーの運転に似ている
自分の車とは少し動きが違い、ない機能や追加された機能もあるが、全体としてはたいていかなり似ている
だから少なくとも私は、正規表現は複数の公開された公式標準として標準化されていると見ている
https://open-std.org/jtc1/sc22/…
https://pubs.opengroup.org/onlinepubs/9699919799/…
https://262.ecma-international.org/14.0/…
JavaScript を含め多くのシステムが PCRE を実装したのは、Perl が POSIX 体系に有用な拡張を多数加えたためだ
記憶では、RE2 は既存体系の性能問題や特異な挙動を抑えようとするもので、全体が Go で実装されたものだと思っていた
後で見たら、RE2 が Go より先に出ていたとは知らなかった
それでも
$の意味と複数行モードへ切り替える方法は、通常は一貫しているほうだ人々は文字列と行を混同している
文字列は文字のシーケンスであり、行は二通りに考えられる
改行を行終端子と見るなら、行は改行でない文字 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