renorm#

Description#

Regex-based data extraction with typed normalization.

renorm lets you embed small, reusable “spec” objects inside regular expressions. Each spec defines both a regex fragment and a normalization function, so matched values are returned as real Python types with zero post-processing.

Normalization depends on the exact structure of the matched text, so extraction and normalization naturally belong together. renorm follows this principle by treating each spec as a single unit of pattern + normalization logic, making typed extraction composable and predictable.

API overview#

Mirrors the familiar Python re API.

renorm.compile(pattern, *specs, **named_specs, flags) -> Pattern

renorm.Pattern

  .search(text) -> Match | None

  .match(text) -> Match | None

  .fullmatch(text) -> Match | None

  .pattern -> str   (the compiled regex string)

renorm.Match

  .group(0) -> str   (full match)

  .group(i) -> T   (normalized value)

  .group(i, j) -> tuple[T, str, None, ...]   (normalized values)

  .groups() -> tuple[T, str, None, ...]   (all normalized values)

  .groupdict()   (disabled: reserved for internal use)

renorm.NormSpec

renorm.Num(dec, ths)

Installation#

pip install re-norm

Getting Started#

Basic example#

Instances of renorm.Num specify the thousand and decimal separators of plain number literals to be captured and, therefore, how they should be normalized.

import renorm as rn

eu = rn.Num(dec=",", ths=" ")
us = rn.Num(dec=".", ths="'")

pat = rn.compile(
    r"price=({@eu}); qty=({@us}); total=({@eu})",
    eu=eu,
    us=us,
)

m = pat.search("price=1 234,50; qty=2'000.0; total=2 469,00")
print(m.groups())  # (1234.5, 2000.0, 2469.0)

Using Specs in Regular Expressions#

Specs are embedded in patterns using placeholders:

  • {@name} for keyword specs

  • {@0}, {@1}, … for positional specs

Each placeholder is replaced by the regex fragment defined by the spec. If the placeholder appears inside a capturing group, the matched text is passed through the spec’s normalization function.

This is the same placeholder syntax used in the basic example above, where specs are passed as kwargs.

Below there is an example for a Num spec passed as a positional argument. Mind that the regular expression can be constructed with regular capturing groups and that renorm spec is only captured and normalized if inside a capturing group.

{@0}#
us = rn.Num(ths="'")
p = rn.compile(r"([A-Za-z]+):\s?({@0})", us)
m = p.search("total: 1'234.5\n")

print(m.groups()) # ('total', 1234.5)

Example for a Num spec passed as a keyword argument.

{@value}#
eu_num = rn.Num(dec=",", ths=" ")
p = rn.compile(r"([a-zA-z]+):\s?({@value})", value=eu_num)
m = p.search("total: 1 234,5\n")

print(m.groups()) # ('total', 1234.5)

Custom specs#

You can define your own normalization rules by subclassing rn.NormSpec. A custom spec must implement a pattern property (regex fragment) and a normalize method. This allows renorm to support arbitrary data types and formats.

class Hex(rn.NormSpec):
    @property
    def pattern(self):
        return r"[0-9A-Fa-f]+"

    def normalize(self, value: str):
        return int(value, 16)
addr = Hex()
size = rn.Num()

pat = rn.compile(
    r"addr=0x({@addr}), size=({@size}) bytes",
    addr=addr,
    size=size,
)

m = pat.search("addr=0x1A2B, size=64 bytes")
print(m.groups())   # (6699, 64.0)