s9e/regexp-builder

一个生成匹配字符串列表的正则表达式的库。

2.1.0 2023-02-21 20:42 UTC

README

s9e\RegexpBuilder 是一个生成匹配给定字符串列表的正则表达式的库。它最适合高效地在文本中查找关键词列表。

在实践中,给定 ['foo', 'bar', 'baz'] 作为输入,该库将生成 ba[rz]|foo,一个可以匹配字符串 foobarbaz 的正则表达式。它可以生成用于不同编程语言的正则表达式引擎(如 PHP、JavaScript 等)的正则表达式。

Build status

安装

s9e/regexp-builder 添加到您的 Composer 依赖项。

composer require s9e/regexp-builder

使用方法

使用该库最简单的方法是从现有的工厂之一获取一个 Builder 实例。构建器的 build() 方法接受一个字符串列表作为输入,并返回一个匹配它们的正则表达式。

// Use the PHP factory to generate a PHP regexp
$builder = s9e\RegexpBuilder\Factory\PHP::getBuilder();
echo '/', $builder->build(['foo', 'bar', 'baz']), '/';
/ba[rz]|foo/

工厂

工厂是一个静态类,用于为特定用例创建一个配置好的 Builder 实例。所有工厂都有一个静态的 getBuilder() 方法。其中一些接受可选参数。

以下工厂可用于生成对应编程语言的正则表达式。生成的 Builder 实例将仅使用可打印的 ASCII 字符生成正则表达式,而其他字符将根据正则表达式引擎的语法进行转义。以下列出了工厂及其可选参数(及其默认值):

  • PHP
    • modifiers: '' - 用于正则表达式的 模式修饰符,例如 isu
    • delimiter: '/' - 用于正则表达式的 定界符,例如 #()
  • Java
  • JavaScript
  • RE2

此外,还存在两个工厂 RawBytesRawUTF8。它们可以分别使用字节和 UTF-8 字符作为基本单元生成更小的正则表达式,而不会对使用的字符有任何限制。生成的正则表达式应作为二进制数据处理,不推荐在可读代码中使用。

示例

创建 PHP (PCRE2) 正则表达式

以下示例展示了如何创建一个匹配 (U+263A)或 (U+2639),带或不带 u 标志的 PHP 正则表达式。

// Without any modifiers, PCRE operates on bytes
$builder = s9e\RegexpBuilder\Factory\PHP::getBuilder();
echo '/', $builder->build(['', '']), "/\n";

// The 'u' flag enables Unicode mode in PCRE
$builder = s9e\RegexpBuilder\Factory\PHP::getBuilder(modifiers: 'u');
echo '/', $builder->build(['', '']), '/u';
/\xE2\x98[\xB9\xBA]/
/[\x{2639}\x{263A}]/u

创建 JavaScript 正则表达式

以下示例表明可以使用 JavaScript 工厂来创建带或不带 u 标志的 JavaScript 正则表达式。

$builder = s9e\RegexpBuilder\Factory\JavaScript::getBuilder();
echo '/', $builder->build(['😁', '😂']), "/\n";

// The 'u' flag enables Unicode mode in JavaScript RegExp
$builder = s9e\RegexpBuilder\Factory\JavaScript::getBuilder(flags: 'u');
echo '/', $builder->build(['😁', '😂']), '/u';
/\uD83D[\uDE01\uDE02]/
/[\u{1F601}\u{1F602}]/u

使用元序列

用户定义的序列可以用于在输入字符串中表示任意表达式。该序列可以由一个或多个字符组成。它所表示的表达式必须是有效的。例如,.* 是有效的,但 + 不是。

在以下示例中,我们通过将 ? 映射到 . 和将 * 映射到 .* 来模拟 Bash 风格的通配符。

$builder = new s9e\RegexpBuilder\Builder;
$builder->meta->set('?', '.');
$builder->meta->set('*', '.*');

echo '/', $builder->build(['foo?', 'bar*']), '/';
/bar.*|foo./

以下示例中,我们将输入中的\d映射到输出中的\d,以模拟正则表达式的转义序列。请注意,它们不必相同,我们可以选择将*映射到\d或将\d映射到[0-9]

$builder = new s9e\RegexpBuilder\Builder;
$builder->meta->set('\\d', '\\d');

echo '/', $builder->build(['a', 'b', '\\d']), '/';
/[ab\d]/

另外,也可以将meta属性设置为提升的构造参数,如下所示。

$builder = new s9e\RegexpBuilder\Builder(
	meta: new s9e\RegexpBuilder\Meta(['?' => '.', '*' => '.*'])
);

echo '/', $builder->build(['foo?', 'bar*']), '/';
/bar.*|foo./

在输入中使用正则表达式

作为元序列的替代,可以识别输入中作为正则表达式而不是字面值解释的部分。这是通过在构建正则表达式时传递一个数组而不是字符串字面值来完成的。该数组必须包含0个或多个字符串字面值作为字面值部分,以及0个或多个s9e\RegexpBuilder\Expression实例作为正则表达式。

以下示例中,我们构建一个用于URL路由的正则表达式。我们想匹配以下路由,这里以正则表达式形式表示

  • /(*:home)
  • /admin(*:admin_index)
  • /admin/login(*:admin_login)
  • /admin/logout(*:admin_logout)
  • /admin/product(*:admin_product_store)
  • /admin/product/(\d+)(*:admin_product_show)
  • /admin/product/(\d+)/edit(*:admin_product_edit)
  • /shop(*:shop_index)
  • /shop/product(*:shop_product_index)
  • /shop/product/(\d+)(*:shop_product_show)
// Use the PHP factory to generate a PHP regexp with ~ as a delimiter
$delimiter = '~';
$builder   = s9e\RegexpBuilder\Factory\PHP::getBuilder(delimiter: $delimiter);

// In this example, we want to use non-capture groups that reset group numbers
// As per https://www.pcre.org/current/doc/html/pcre2syntax.html#TOC1
$builder->serializer->groupType = s9e\RegexpBuilder\GroupType::NonCaptureReset;

// Syntactic sugar
function expr(string $expr)
{
	return new s9e\RegexpBuilder\Expression($expr);
}

// Here we split each route into a mix of literals and regular expressions
$regexp = $builder->build([
	['/', expr('(*:home)')],
	['/admin', expr('(*:admin_index)')],
	['/admin/login', expr('(*:admin_login)')],
	['/admin/logout', expr('(*:admin_logout)')],
	['/admin/product', expr('(*:admin_product_store)')],
	['/admin/product/', expr('(\d+)'), expr('(*:admin_product_show)')],
	['/admin/product/', expr('(\d+)'), '/edit', expr('(*:admin_product_edit)')],
	['/shop', expr('(*:shop_index)')],
	['/shop/product', expr('(*:shop_product_index)')],
	['/shop/product/', expr('(\d+)'), expr('(*:shop_product_show)')]
]);
$regexp = $delimiter . '^' . $regexp . '$' . $delimiter;

// Let's see what the regexp looks like
echo "$regexp\n\n";

// Let's test our new regexp
preg_match($regexp, '/admin/product/123', $m);
print_r($m);
~^/(?|admin(?|/(?|log(?|in(*:admin_login)|out(*:admin_logout))|product(?|/(\d+)(?|/edit(*:admin_product_edit)|(*:admin_product_show))|(*:admin_product_store)))|(*:admin_index))|shop(?|/product(?|/(\d+)(*:shop_product_show)|(*:shop_product_index))|(*:shop_index))|(*:home))$~

Array
(
    [0] => /admin/product/123
    [1] => 123
    [MARK] => admin_product_show
)