跳到内容

用于表达约束的 DSL

这个库提供了一个领域特定语言(DSL),以更直观和模块化的方式构建正则表达式。它允许你使用表示字面字符串、模式和各种量词的简单构建块来创建复杂的正则表达式。此外,这些自定义的正则表达式类型可以直接用作 Pydantic schema 中的类型,以便在文本生成期间强制执行模式约束。


为什么使用这个 DSL?

  1. 模块化和可读性:你不是编写晦涩的正则表达式字符串,而是将正则表达式组合成一个对象树。
  2. 增强调试:每个表达式都可以可视化为 ASCII 树,从而更容易理解和调试复杂的正则表达式。
  3. Pydantic 集成:在 Pydantic 模型中使用你通过 DSL 定义的正则表达式作为类型。DSL 可以无缝地转换为包含适当模式约束的 JSON Schema。
  4. 可扩展性:通过扩展提供的类,可以轻松添加或修改量词和其他正则表达式组件。

构建块

这个 DSL 中的每个正则表达式组件都是一个 Term。这里有两种主要类型:

  • String:表示字面字符串。它会转义在正则表达式中具有特殊含义的字符。
  • Regex:表示现有的正则表达式模式字符串。
from outlines.types import String, Regex

# A literal string "hello"
literal = String("hello")   # Internally represents "hello"

# A regex pattern to match one or more digits
digit = Regex(r"[0-9]+")     # Internally represents the pattern [0-9]+

# Converting to standard regex strings:
from outlines.types.dsl import to_regex

print(to_regex(literal))  # Output: hello
print(to_regex(digit))    # Output: [0-9]+

量词和组合项的早期介绍

该 DSL 支持在每个 Term 上使用常见的正则表达式量词作为方法。这些方法允许你指定模式应匹配多少次。它们包括:

  • exactly(count):精确匹配该项 count 次。
  • optional():匹配该项零次或一次。
  • one_or_more():匹配该项一次或多次(克莱尼加号)。
  • zero_or_more():匹配该项零次或多次(克莱尼星号)。
  • between(min_count, max_count):匹配该项 min_countmax_count 次(包括两者)。
  • at_least(count):匹配该项至少 count 次。
  • at_most(count):匹配该项最多 count 次。

这些量词也可以用作接受 Term 作为参数的函数。如果该项是普通字符串,它将自动转换为 String 对象。因此 String("foo").optional() 等同于 optional("foo")

让我们通过示例并排查看这些量词。

量词实战

exactly(count)

此方法限制该项精确出现 count 次。

# Example: exactly 5 digits
five_digits = Regex(r"\d").exactly(5)
print(to_regex(five_digits))  # Output: (\d){5}

你也可以使用 exactly 函数

from outlines.types import exactly

# Example: exactly 5 digits
five_digits = exactly(Regex(r"\d"), 5)
print(to_regex(five_digits))  # Output: (\d){5}

optional()

此方法使一项成为可选,意味着它可以出现零次或一次。

# Example: an optional "s" at the end of a word
maybe_s = String("s").optional()
print(to_regex(maybe_s))  # Output: (s)?

你也可以使用 optional 函数

from outlines.types import optional

# Example: an optional "s" at the end of a word
maybe_s = optional("s")
print(to_regex(maybe_s))  # Output: (s)?

one_or_more()

此方法指示该项必须至少出现一次。

# Example: one or more alphabetic characters
letters = Regex(r"[A-Za-z]").one_or_more()
print(to_regex(letters))  # Output: ([A-Za-z])+

你也可以使用 one_or_more 函数

from outlines.types import one_or_more

# Example: one or more alphabetic characters
letters = one_or_more(Regex(r"[A-Za-z]"))
print(to_regex(letters))  # Output: ([A-Za-z])+

zero_or_more()

此方法指示该项可以出现零次或多次。

# Example: zero or more spaces
spaces = String(" ").zero_or_more()
print(to_regex(spaces))  # Output: ( )*

你也可以使用 zero_or_more 函数

from outlines.types import zero_or_more

# Example: zero or more spaces
spaces = zero_or_more(" ")
print(to_regex(spaces))  # Output: ( )*

between(min_count, max_count)

此方法指示该项可以出现在 min_countmax_count 次之间(包括两者)。

# Example: Between 2 and 4 word characters
word_chars = Regex(r"\w").between(2, 4)
print(to_regex(word_chars))  # Output: (\w){2,4}

你也可以使用 between 函数

from outlines.types import between

# Example: Between 2 and 4 word characters
word_chars = between(Regex(r"\w"), 2, 4)
print(to_regex(word_chars))  # Output: (\w){2,4}

at_least(count)

此方法指示该项必须至少出现 count 次。

# Example: At least 3 digits
at_least_three = Regex(r"\d").at_least(3)
print(to_regex(at_least_three))  # Output: (\d){3,}

你也可以使用 at_least 函数

from outlines.types import at_least

# Example: At least 3 digits
at_least_three = at_least(Regex(r"\d"), 3)
print(to_regex(at_least_three))  # Output: (\d){3,}

at_most(count)

此方法指示该项最多可以出现 count 次。

# Example: At most 3 digits
up_to_three = Regex(r"\d").at_most(3)
print(to_regex(up_to_three))  # Output: (\d){0,3}

你也可以使用 at_most 函数

from outlines.types import at_most

# Example: At most 3 digits
up_to_three = at_most(Regex(r"\d"), 3)
print(to_regex(up_to_three))  # Output: (\d){0,3}

组合项

该 DSL 允许你使用串联和选择将基本项组合成更复杂的模式。

串联 (+)

+ 运算符(及其反向变体)将项串联起来,意味着这些项会按顺序匹配。

# Example: Match "hello world"
pattern = String("hello") + " " + Regex(r"\w+")
print(to_regex(pattern))  # Output: hello\ (\w+)

选择 (either())

either() 函数创建备选项,允许匹配多个模式中的一个。你可以提供任意数量的项。

# Example: Match either "cat" or "dog" or "mouse"
animal = either(String("cat"), "dog", "mouse")
print(to_regex(animal))  # Output: (cat|dog|mouse)

注意: 当使用 either() 处理普通字符串(例如 "dog")时,DSL 会自动将其包装在 String 对象中,该对象会转义在正则表达式中具有特殊含义的字符,这与量词函数类似。


自定义类型

该 DSL 内置了代表常见文本结构的类型:

  • integer 表示由 int 识别的整数
  • boolean 表示布尔值,即由 bool 识别的 "True" 或 "False"。
  • number 表示由 Python 的 float 识别的浮点数
  • date 表示由 datetime.date 理解的日期
  • time 表示由 datetime.time 理解的时间
  • datetime 表示由 datetime.datetime 理解的时间
  • digit 表示单个数字
  • char 表示单个字符
  • newline 表示换行符
  • whitespace 表示空白字符
  • sentence 表示一个句子
  • paragraph 表示一个段落(由一个或多个换行符分隔的一个或多个句子)

例如,你可以使用以下模式描述 GSM8K 数据集中的答案:

from outlines.types import sentence, digit

answer = "A: " + sentence.between(2,4) + " So the answer is: " + digit.between(1,4)

实际示例

示例 1:匹配自定义 ID 格式

假设你想创建一个正则表达式来匹配像 "ID-12345" 这样的 ID 格式,其中: - 字面量 "ID-" 必须在开头。 - 后面跟着恰好 5 个数字。

id_pattern = "ID-" + Regex(r"\d").exactly(5)
print(to_regex(id_pattern))  # Output: ID-(\d){5}

示例 2:使用 Pydantic 进行电子邮件验证

你可以定义一个用于电子邮件验证的正则表达式,并在 Pydantic 模型中将其用作类型。

from pydantic import BaseModel, ValidationError

# Define an email regex term (this is a simplified version)
email_regex = Regex(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")

class User(BaseModel):
    name: str
    email: email_regex  # Use our DSL regex as a field type

# Valid input
user = User(name="Alice", email="alice@example.com")
print(user)

# Invalid input (raises a ValidationError)
try:
    User(name="Bob", email="not-an-email")
except ValidationError as e:
    print(e)

当在 Pydantic 模型中使用时,电子邮件字段会根据正则表达式模式自动验证,并且其 JSON Schema 包含 pattern 约束。

示例 3:构建复杂模式

考虑一个匹配简单日期格式的模式:YYYY-MM-DD

year = Regex(r"\d").exactly(4)         # Four digits for the year
month = Regex(r"\d").exactly(2)        # Two digits for the month
day = Regex(r"\d").exactly(2)          # Two digits for the day

# Combine with literal hyphens
date_pattern = year + "-" + month + "-" + day
print(to_regex(date_pattern))
# Output: (\d){4}\-(\d){2}\-(\d){2}

可视化你的模式

这个 DSL 的一个独特之处在于,每个项都可以将其底层结构打印为 ASCII 树。这种可视化在处理复杂表达式时特别有用。

# A composite pattern using concatenation and quantifiers
pattern = "a" + String("b").one_or_more() + "c"
print(pattern)

预期输出

└── Sequence
    ├── String('a')
    ├── KleenePlus(+)
    │   └── String('b')
    └── String('c')

这种树状表示使得轻松查看正则表达式中的层级和操作顺序成为可能。


结语

这个 DSL 旨在简化正则表达式的创建和管理——无论你是在 Web API 中验证输入、约束 LLM 的输出,还是只是试验正则表达式模式。凭借针对常见量词和运算符的直观方法、清晰的视觉反馈以及与 Pydantic 的内置集成,你可以轻松构建强大且可维护的基于正则表达式的验证。

请随意进一步探索该库,并将示例应用于你的用例。祝你正则愉快!