できない.dev

正規表現に一致する部分をすべて取り出すには

1 件目だけでなく、文字列の中で一致するすべての箇所を取り出す基本形を各言語で示す。
一致全体を返す API とキャプチャグループを返す API の使い分けまでを扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

import re
 
log = "error: 404 / warn: 301 / error: 500 / error: 503"
 
# キャプチャグループがあると findall はグループだけを返す
codes = re.findall(r"error: (\d+)", log)
print(codes)  # ['404', '500', '503']
 
# 一致した全体や位置も欲しいときは finditer
for m in re.finditer(r"error: (\d+)", log):
    print(m.group(0), m.start())  # error: 404 0 / error: 500 25 / error: 503 38

re.findall はキャプチャグループがあるとグループだけを、無ければ一致全体を返す。
同じパターンでも括弧の有無で戻り値の中身が変わるので、一致全体と位置の両方が要るときは finditer を使って Match オブジェクトのまま回す。

JavaScript 実行確認済み

const log = "error: 404 / warn: 301 / error: 500 / error: 503";
 
// matchAll は g フラグが無いと TypeError になる
const codes = [...log.matchAll(/error: (\d+)/g)].map((m) => m[1]);
console.log(codes); // [ '404', '500', '503' ]
 
// 一致した全体だけで良ければ match に g を付けても取れる(グループは取れない)
console.log(log.match(/error: \d+/g)); // [ 'error: 404', 'error: 500', 'error: 503' ]

matchAll は g フラグが無いと TypeError になるため、全件を取りに行くときは必ず /g を付ける。
match に g を付けても全件は取れるが、その場合の戻り値は一致全体の配列でキャプチャグループは失われる。

TypeScript 実行確認済み

const log: string = "error: 404 / warn: 301 / error: 500 / error: 503";
 
// matchAll は IterableIterator<RegExpMatchArray> を返すので、展開してから map する
const codes: string[] = [...log.matchAll(/error: (\d+)/g)].map((m) => m[1]);
console.log(codes); // [ '404', '500', '503' ]
 
// 取り出した値は string のままなので、数として使うならここで変換する
const nums: number[] = codes.map(Number);
console.log(nums.reduce((a, b) => a + b, 0)); // 1407

matchAll の戻り値はイテレータなので、配列として扱うにはスプレッド構文で展開する。
取り出した値の型は string のままで、数値として計算するなら Number などで明示的に変換する。

Go 静的確認

package main
 
import (
	"fmt"
	"regexp"
)
 
func main() {
	log := "error: 404 / warn: 301 / error: 500 / error: 503"
	re := regexp.MustCompile(`error: (\d+)`)
 
	// 第 2 引数の -1 は「見つかるだけ全部」。正の数を渡すとその件数で打ち切る
	for _, m := range re.FindAllStringSubmatch(log, -1) {
		fmt.Println(m[0], m[1]) // m[0] が一致全体、m[1] がグループ
	}
 
	// グループが要らなければ FindAllString で足りる
	fmt.Println(re.FindAllString(log, -1))
}

FindAll 系はどれも第 2 引数に上限件数を取り、-1 が「全件」を意味する。
グループまで欲しいときは FindAllStringSubmatch、一致全体だけで足りるときは FindAllString と、必要な形に合わせて選ぶ。

Rust 静的確認

use regex::Regex;
 
fn main() {
    let log = "error: 404 / warn: 301 / error: 500 / error: 503";
    let re = Regex::new(r"error: (\d+)").unwrap();
 
    // captures_iter は一致ごとに Captures を返す
    let codes: Vec<&str> = re
        .captures_iter(log)
        .map(|c| c.get(1).unwrap().as_str())
        .collect();
    println!("{:?}", codes); // ["404", "500", "503"]
 
    // グループが要らなければ find_iter で一致全体だけを回せる
    for m in re.find_iter(log) {
        println!("{} {}", m.as_str(), m.start());
    }
}

captures_iter は一致ごとに Captures を返し、find_iter は一致全体だけを返す。
どちらも遅延評価のイテレータなので、必要な分だけ回すか collect でまとめて受け取るかを選べる。
regex クレートを Cargo.toml に足しておく。

Java 実行確認済み

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class RegexFindAll {
    public static void main(String[] args) {
        String log = "error: 404 / warn: 301 / error: 500 / error: 503";
        Pattern p = Pattern.compile("error: (\\d+)");
 
        Matcher m = p.matcher(log);
        List<String> codes = new ArrayList<>();
        while (m.find()) { // find を呼ぶたびに次の一致へ進む
            codes.add(m.group(1));
        }
        System.out.println(codes); // [404, 500, 503]
 
        // Java 9 以降は results() でストリームとしても扱える
        List<String> viaStream = p.matcher(log).results().map(r -> r.group(1)).toList();
        System.out.println(viaStream); // [404, 500, 503]
    }
}

Matcher は find を呼ぶたびに次の一致へ進む状態を持つので、while で回して 1 件ずつ取り出す。
Java 9 以降は results で Stream<MatchResult> にでき、同じ処理を 1 行で書ける。

C# 実行確認済み

using System.Text.RegularExpressions;
 
var log = "error: 404 / warn: 301 / error: 500 / error: 503";
 
// Matches は MatchCollection を返す。各 Match の Groups[1] が丸括弧の中身
var codes = Regex.Matches(log, @"error: (\d+)")
    .Select(m => m.Groups[1].Value)
    .ToArray();
 
Console.WriteLine(string.Join(", ", codes)); // 404, 500, 503
Console.WriteLine(codes.Length);             // 3

Regex.Matches は一致した Match をまとめて返し、各 Match の Groups[1] が丸括弧の中身になる。
MatchCollection は IEnumerable なので、LINQ をそのままつないで必要な形に変換できる。

つまずき

「全件が取れない」ときは、1 件で止まる API を呼んでいることが多い。
Python の re.search、JavaScript の match(g フラグなし)、Go の FindStringSubmatch、Rust の captures はいずれも最初の 1 件しか返さない。
全件が欲しいときは findall / finditer、matchAll、FindAll 系、captures_iter のように「複数を返す側」の名前に切り替える。

一致全体とグループのどちらが返るか

同じ「全件」でも、返ってくるのが一致全体かキャプチャグループかは API ごとに違う。
Python の findall は括弧の有無で戻り値が変わり、JavaScript の match に g を付けると一致全体だけになってグループが落ちる。
パターンに括弧を足したり外したりしたときは、受け取る側のコードも合わせて見直す。

重なった一致は取れない

どの言語の全件取得も、一致した範囲の次の文字から探索を再開する。
そのため「aaa」から 2 文字ずつの一致を全部といった重なりのある取り出しはできず、得られるのは重ならない一致だけになる。
重なりまで拾いたいときは先読みを使ってパターン側で幅を持たせないようにする。

この記事は役立ちましたか?