質問: num_regex.findall(text) print(text) >>>[('3', ''), ('', ''), ('1,234', ',234'), ('', ''), ('12', ''), ('', ''), ('34,567', ',567'), ('', ''), ('6,368,745', ',745'), ('', '')] ``` このコードで実現したいことは、 三桁ごとにカンマが付いた数字のみに一致させたいです。 一致したい例は、上では 3 1,234 6,368,745 一致して欲しくない例は、上では12,34,567 他では、1234です。 なって欲しいイメージは >>>[("3",""),("1,234",",234"),("6,368,745",",368,745")] です。 また、上記のコードの実行結果('6,368,745', ',745')の最後がなぜ、,368,745にならないのでしょうか? 回答の程よろしくお願いします。
回答: 仰るような動作をさせたいのであれば、御呈示の正規表現に対して以下 3 点の修正が必要かと存じます。 ##### 1) 0 文字ではマッチしないよう、パターンに最低 1 文字の制限を入れる 現状のパターンでは、パターン全体が **空の文字列に対しても真となる** ため、一つ結果が得られる度、続く 0 文字にマッチして `('', '')` という値が得られてしまっています。少なくとも 1 文字以上の数字が必須であることを示すため、 `\d{,3}` を `\d{1,3}` へと変更したほうが良いでしょう。 ##### 2) 数字の一部を取り出してしまわないよう、区切り文字を明示する 現状のパターンでは、 **どこからどこまでがひとまとまりの数字か** が明示されていないため、数字の **一部分でもパターンに一致してしまえば正しいと見做されてしまう** 状態です。その結果、 `12,34,567` という値が `12` と `34,567` の二つに分ければ OK と判断され取り出されてしまっています。 具体的には、例えばパターン先頭に `(?:^| )` (文字列先頭または半角スペースに一致) を、パターン末尾に `(?=$| )` (文字列末尾または半角スペースが一文字先に存在) を入れると良いのではないでしょうか。 ※ここでパターン末尾で肯定先読みを使っているのは、単純にマッチさせてしまうと、次の数字のまとまりの判定時、先頭に期待する半角スペースを既に前のマッチで消費済みとして見失ってしまうためです ##### 3) パターン繰り返しの括弧と、取り出しの括弧を分ける `6,368,745` の第二グループが `,745` になっているのは、パターンを繰り返すための括弧をそのまま取り出している為です。実際、御書きのパターンでは **括弧の中が `,\d{3}` となっているわけですから、これにマッチするのは `,368` または `,745` のどちらかになる** 、と申し上げれば御分り頂けますでしょうか。このうち最後にマッチした`,745` が結果として返ってきています。 この部分は、本当に取り出したいのが **パターンを繰り返した後の全体なのであれば、繰り返しを含めて括弧で囲んで取り出す** 書き方としましょう。つまり、 `(,\d{3})*` となっている部分を `((?:,\d{3})*)` として、アスタリスクまでを含んだ部分を囲ってやれば良いかと思います。 ##### x) 無理な「正規表現一発」に拘らない 以上の修正を加えると、パターンは `(?:^| )(\d{1,3}((?:,\d{3})*))(?=$| )`という形になり、これで恐らく希望の動作をしてくれるはずです。 但し、修正 2 点目で述べた通り、 **結局のところ数字と数字の区切りはどこかという情報が必要** なのですから、無理につながった状態で正規表現に掛けようとせず、その区切りで **分けた後に一つ一つ調べていった方が簡単で分かりやすい** ように思います。例えば、次のようなコードにしてやれば、あまり正規表現の複雑さに悩まず必要な数字だけを取り出すことが可能です。 ``` mo = [ num for num in text.split(' ') if re.match(r'^\d{1,3}(,\d{3})*$', num) ] ``` 仮に数字の区切りが正規表現でなければ扱えないほどに複雑であったとしても、「数字を分ける正規表現」と「正しさを判定する正規表現」との 2 回に分けた方が簡単です。特別な理由のない限り、単純なやり方を組み合わせることを推奨致します。