Skip to content

Extracting fields

One line: Pulling parts out of text is what capturing groups are for, and the three things that go wrong are the same in every engine — a repeated group keeps only its last iteration, an optional group on the branch not taken is unset, and the way you refer to a group differs by language — so the shape that survives is one group per field, named, with the iteration done in the host language.

Level: 201 · you have a line of text and want the pieces

The shape

A date, a log line, a key-value pair: the pattern's job is to find the whole thing once and hand back its parts. Two decisions, and both matter more than the pattern:

  1. Name the groups. (?<year>\d{4}) survives an edit that inserts a group before it; (\d{4}) referred to as group 3 does not. The spelling is (?<name>…) in JavaScript, Ruby, Java, Go and Rust, (?P<name>…) in Python — and Perl, PCRE2, Go and Rust take both. See named group.
  2. Match one record, and loop in the language. findall / finditer / scan / FindAllStringSubmatch / captures_iter — not a quantifier around the group, which keeps only the last hit.

Measured

Verified output of topic_extracting_fields_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

construct                  python            perl            node            ruby              java            go                    rust                  fancy
whole match                2026-09-22        2026-09-22      2026-09-22      2026-09-22        2026-09-22      2026-09-22            2026-09-22            2026-09-22
year via $1                $1                2026            2026            $1                2026            2026                  2026                  2026
year via \1                2026              \u{1}           \\1             2026              1               \\1                   \\1                   \\1
named, (?<y>…) and ${y}    -                 n/a             ${d}/${m}/${y}  ${d}/${m}/${y}    22/09/2026      22/09/2026            22/09/2026            22/09/2026
named, (?P<y>…) and \g<y>  22/09/2026        g<d>/g<m>/g<y>  -               -                 -               \\g<d>/\\g<m>/\\g<y>  \\g<d>/\\g<m>/\\g<y>  \\g<d>/\\g<m>/\\g<y>
last iteration of a group  [$1]              [3,]            [3,]            [$1]              [3,]            [3,]                  [3,]                  [3,]
optional field, unset      int=$1 frac=[$2]  int=42 frac=[]  int=42 frac=[]  int=$1 frac=[$2]  int=42 frac=[]  int=42 frac=[]        int=42 frac=[]        int=42 frac=[]

Reading the table

  • Row 1. The whole match is the same in all eight. The disagreements are all about getting at the parts.
  • Rows 2–3, $1 against \1. Asking for the year through a replacement shows the replacement spelling war: $1 is text in Python and Ruby; \1 is a control character in Perl and the digit 1 in Java. In code you would call m.group(1) / $1 / m[1] / m.group(1) / m[1] / m[1] / caps[1] — the API names differ less than the template does.
  • Rows 4–5, named. ${d}/${m}/${y} rearranges the date in Java, Go and Rust; \g<d>/\g<m>/\g<y> does it in Python. JavaScript's spelling is $<d>, Ruby's \k<d>, Perl's $+{d}. Python's - on row 4 is (?<y> being refused, not the extraction.
  • Row 6, the last iteration. (\d+,)+ over 1,2,3, and then $1: 3, everywhere the reference works. No engine keeps the earlier iterations in group 1. This is the row that decides the shape above — the loop belongs outside the pattern.
  • Row 7, the optional field. (\d+)(?:\.(\d+))? on 42: group 2 is unset, and every engine inserts nothing for it in a replacement — frac=[]. In code, unset is None in Python, undefined in JavaScript, nil in Ruby, null in Java, an empty string in Go, None in Rust — six languages, five different values for "this group did not match", and the empty-string one (Go) cannot tell "matched nothing" from "did not match".

How the loop looks, per language

Language One match with groups Every match
Python m = re.search(p, s); m.group("y") / m["y"] for m in re.finditer(p, s)
Perl if ($s =~ /$p/) { $+{y} } while ($s =~ /$p/g) { … }
JavaScript const m = s.match(re); m.groups.y for (const m of s.matchAll(re)) — needs the g flag
Ruby m = s.match(re); m[:y] s.scan(re) — returns the groups, not the matches, when there are groups
Java Matcher m = p.matcher(s); m.find(); m.group("y") while (m.find())
Go m := re.FindStringSubmatch(s); m[re.SubexpIndex("y")] re.FindAllStringSubmatch(s, -1)
Rust let c = re.captures(s)?; &c["y"] re.captures_iter(s)

Two of those have a trap of their own: JavaScript's matchAll throws without the g flag, and Ruby's scan changes what it returns depending on whether the pattern has groups.

Keywords on this page

Capturing group · Named group · Non-capturing group · Replacement references · \G — for a tokenizer, where the next field must start where the last one ended.

See also