Skip to content

正規表現の名前付きキャプチャグループ snippet

名前付きキャプチャグループは、マッチ位置を読みやすい契約に変えます。(?<year>\d{4}) はどのコードレビューでも m[1] に勝ります。罠はエンジンの方言です。.NET、Java (7+)、ES2018 の JavaScript は (?<name>...) と綴る一方、Python は長年 (?P<name>...) が必要で、今日もこの P が必須です。そして POSIX/ERE だけのエンジン(grep -E、awk)には名前がまったくありません。重複する名前(一部のエンジンではブランチリセット付きで合法)と、参加しなかったグループへのアクセス(空文字列ではなく None/null)が現代のエッジです。

名前付きキャプチャグループは、マッチ位置を読みやすい契約に変えます。(?<year>\d{4}) はどのコードレビューでも m[1] に勝ります。罠はエンジンの方言です。.NET、Java (7+)、ES2018 の JavaScript は (?<name>...) と綴る一方、Python は長年 (?P<name>...) が必要で、今日もこの P が必須です。そして POSIX/ERE だけのエンジン(grep -E、awk)には名前がまったくありません。重複する名前(一部のエンジンではブランチリセット付きで合法)と、参加しなかったグループへのアクセス(空文字列ではなく None/null)が現代のエッジです。

Runnable recipe · 11 languagesregex-tester ツールを開く →
Regex & Text Processingregexcapture-groupsnamed-groupspattern-matchingtext-parsing

Every language

11 languages, copy-ready. One at a time with syntax highlighting, or all inline.

JSJavaScript
const re = /(?<year>\d{4})-(?<month>\d{2})/u; // the (?< spelling is ES2018

const m = '2026-08-26'.match(re);
m.groups.year;  // '2026'
m.groups.month; // '08'

// a group that skipped the match is undefined — NOT '':
const tz = /(?<tz>Z|[+-]\d{2}:\d{2})?/u.exec('2026-08-26');
const zone = tz?.groups?.tz ?? 'UTC'; // 'UTC' — never test with === ''

The named syntax is ES2018: engines before it throw SyntaxError at parse time, never at match time. The u flag rides along for the same modern baseline and keeps \p{...} classes available. A group that did not participate reads as undefined — default it with ?? , never === ''.

TSTypeScript
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;

function parseYear(s: string): number | null {
  const m = dateRe.exec(s);
  const year = m?.groups?.year; // string | undefined
  return year !== undefined ? Number(year) : null;
}

exec returns RegExpExecArray | null — the ?. chain absorbs the null match and the absent groups bag in one hop. The lib types the bag's values as string, which is optimistic: a group that skipped is undefined at runtime, so treat every named group as string | undefined.

GoGo
import "regexp"

// Go kept the P: (?P<Name>...) — the .NET spelling fails MustCompile.
var dateRe = regexp.MustCompile(`(?P<Year>\d{4})-(?P<Month>\d{2})`)

func dateParts(s string) (year, month string, ok bool) {
	m := dateRe.FindStringSubmatch(s)
	if m == nil {
		return "", "", false
	}
	for i, name := range dateRe.SubexpNames() { // [0] is the whole pattern
		switch name {
		case "Year":
			year = m[i]
		case "Month":
			month = m[i]
		}
	}
	return year, month, true
}

No GroupByName in the stdlib: FindStringSubmatch returns a flat slice and SubexpNames() is the only name-to-index map — its [0] entry is the whole pattern and unnamed groups read as "", which is why the switch falls through for them.

RsRust
use regex::Regex;

fn main() {
    let date = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
    // the (?P<year>...) spelling compiles too — the crate accepts both

    if let Some(caps) = date.captures("2026-08-26") {
        let year = caps.name("year").map(|m| m.as_str());   // Option<&str>
        let month = caps.name("month").map(|m| m.as_str()); // Some("08")
        println!("{year:?}-{month:?}"); // Some("2026")-Some("08")
    }
}

The regex crate documents both spellings — (?P<name>...) and (?<name>...). name() returns Option<Match>, so a group that did not participate is None with no empty-string ambiguity; unwrap() on Regex::new is idiomatic for a static pattern (a bad pattern should die in a test, not at match time).

PHPPHP
$date = '2026-08-26';

if (preg_match('/(?<year>\d{4})-(?<month>\d{2})/', $date, $m) === 1) {
    $year = $m['year']; // and $m[1] — the same capture under both keys
    $month = $m['month'];
}

// trap: a group that did not participate is OMITTED from $matches:
preg_match('/(?<tz>Z|[+-]\d{2}:\d{2})?/', $date, $t);
$tz = $t['tz'] ?? null; // null — the key is absent, not ''

PCRE spells names (?<name>...), (?P<name>...) or (?'name'...) — all three compile. A participating group appears TWICE in $matches ($m[1] and $m['year'] alias one slot, so count($m) double-counts), while a group that did not participate is omitted entirely on current PHP (verified on 8.5: reading it warns 'Undefined array key') — pull it with ?? or array_key_exists.

PyPython
import re

date_re = re.compile(r'(?P<year>\d{4})-(?P<month>\d{2})')

m = date_re.search('shipped on 2026-08-26')
if m:
    year = m.group('year')   # '2026'
    full = m.groupdict()     # {'year': '2026', 'month': '08'}

# a group that skipped the match is None, not '':
t = re.search(r'(?P<tz>Z|[+-]\d{2}:\d{2})?', '2026-08-26')
t.group('tz')  # None
t.groupdict()  # {'tz': None}

The P is NOT optional: re rejects the .NET-style (?<name>...) with re.PatternError even on current CPython (verified on 3.14), and (?'name'...) too. A group that did not participate is None in both group('name') and groupdict() — never ''.

C#C#
using System.Text.RegularExpressions;

var m = Regex.Match("2026-08-26", @"(?<year>\d{4})-(?<month>\d{2})");

if (m.Success)
{
    string year = m.Groups["year"].Value; // Groups[1] is the same slot

    var tz = Regex.Match("2026-08-26", @"(?<tz>Z|[+-]\d{2}:\d{2})?");
    if (!tz.Groups["tz"].Success) { /* the group skipped */ }
}

Groups["name"] is never null — Group.Success is the only way to tell a skipped group from one that matched the empty string, because .Value reads "" in both cases. .NET is also the engine most relaxed about duplicate names in alternations; the branch-reset (?|...) construct is PCRE/Perl.

JvJava
import java.util.regex.*;

Pattern date = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})");

Matcher m = date.matcher("2026-08-26");
if (m.find()) {
    String year = m.group("year");   // Java 7+
    String month = m.group("month"); // m.group(1) is the same slot
}

Named groups are Java 7 — older targets reject the pattern at compile. The name must start with a Latin letter: Pattern.compile("(?<2026>a)") dies with 'capturing group name does not start with a Latin letter' (verified), so numbered names need a prefix. A group that did not participate returns null from group("name").

SwSwift
import Foundation

let s = "2026-08-26"
let re = try NSRegularExpression(pattern: #"(?<year>\d{4})-(?<month>\d{2})"#)

if let m = re.firstMatch(in: s, range: NSRange(s.startIndex..., in: s)) {
    let year = Range(m.range(withName: "year"), in: s).map { String(s[$0]) }
    // Optional("2026") — nil when the name missed or the range is NSNotFound
}

The Foundation API predates names being first-class: range(withName:) is a post-match NSRange lookup, not a typed capture, and a skipped group surfaces as a nil Range rather than a value. The modern road is the Regex/RegexBuilder API (Swift 5.7+), which types captures into the match's output tuple.

KtKotlin
val date = Regex("""(?<year>\d{4})-(?<month>\d{2})""") // raw string: no \\d

fun main() {
    val m = date.find("2026-08-26")
    val year = m?.groups?.get("year")?.value // by name — null when it skipped

    val (y, mo) = m!!.destructured // positional: component1(), component2()
    println("$year $y-$mo")        // 2026 2026-08
}

Triple-quoted strings kill the double-backslash noise of the Java spelling. Destructuring is POSITIONAL (component1()..componentN()) — it ignores names entirely; the by-name read is groups.get("year"), which returns null when the group did not participate.

RbRuby
date = /(?<year>\d{4})-(?<month>\d{2})/

if (m = date.match('2026-08-26'))
  m[:year]          # '2026' — Symbol, String, or index: same slot
  m['month']        # '08'
  m.named_captures  # {'year' => '2026', 'month' => '08'}
end

tz = /(?<tz>Z|[+-]\d{2}:\d{2})?/.match('2026-08-26')
tz[:tz] # nil — non-participation, not ''

MatchData#[] takes a Symbol, a String, or an index — m[:year], m['year'], and m[1] all alias one capture. named_captures (Ruby 2.4+) hands back the whole contract as a hash, with nil for groups that did not participate.

Keep going

Try the interactive regex-tester tool →Read the regex-tokens cheatsheet →