正则表达式或 LIKE 模式的转义函数

Posted

技术标签:

【中文标题】正则表达式或 LIKE 模式的转义函数【英文标题】:Escape function for regular expression or LIKE patterns 【发布时间】:2011-07-05 20:49:51 【问题描述】:

为了放弃阅读整个问题,我的基本问题是:PostgreSQL 中有一个函数可以转义字符串中的正则表达式字符吗?

我查阅了文档,但找不到这样的功能。

这是完整的问题:

在 PostgreSQL 数据库中,我有一列具有唯一名称。我还有一个过程,它会定期将名称插入此字段,并且为了防止重复,如果它需要输入一个已经存在的名称,它会在末尾附加一个空格和括号。

即姓名、姓名(1)、姓名(2)、姓名(3)等

就目前而言,我使用以下代码来查找要添加到系列中的下一个数字(用 plpgsql 编写):

var_name_id := 1;

SELECT CAST(substring(a.name from E'\\((\\d+)\\)$') AS int)
INTO var_last_name_id
FROM my_table.names a
WHERE a.name LIKE var_name || ' (%)'
ORDER BY CAST(substring(a.name from E'\\((\\d+)\\)$') AS int) DESC
LIMIT 1;

IF var_last_name_id IS NOT NULL THEN
    var_name_id = var_last_name_id + 1;
END IF;

var_new_name := var_name || ' (' || var_name_id || ')';

var_name 包含我要插入的名称。)

这暂时可行,但问题在于WHERE 语句:

WHERE a.name LIKE var_name || ' (%)'

此检查无法验证所讨论的 % 是一个数字,并且它不考虑多个括号,例如“名称 ((1))”,如果任何一种情况都存在,则进行强制转换会抛出异常。

WHERE 声明确实需要更像:

WHERE a.r1_name ~* var_name || E' \\(\\d+\\)'

但是var_name 可能包含正则表达式字符,这导致了上面的问题:PostgreSQL 中是否有一个函数可以转义字符串中的正则表达式字符,所以我可以这样做:

WHERE a.r1_name ~* regex_escape(var_name) || E' \\(\\d+\\)'

非常感谢任何建议,包括可能对我的重名解决方案进行修改。

【问题讨论】:

我意识到这个问题很老了,但是在搜索 Postgres 函数以转义特殊的正则表达式字符时,它不断出现。所以我添加了一个提供 that 的答案。 【参考方案1】:

解决顶部的问题:

假设standard_conforming_strings = on,就像它自 Postgres 9.1 以来的默认值一样。

正则表达式转义函数

让我们从regular expression 模式中具有特殊含义的字符的完整列表开始:

!$()*+.:<=>?[\]^|-

包裹在bracket expression 中的大多数都失去了特殊意义——除了少数例外:

- 必须是第一个或最后一个,否则它表示 范围 个字符。 ]\ 必须用 \ 转义(在替换中也是如此)。

在下面添加capturing parentheses for the back reference 后,我们得到这个正则表达式模式:

([!$()*+.:<=>?[\\\]^|-])

使用它,此函数使用反斜杠 (\) 转义所有特殊字符 - 从而消除特殊含义:

CREATE OR REPLACE FUNCTION f_regexp_escape(text)
  RETURNS text
  LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS
$func$
SELECT regexp_replace($1, '([!$()*+.:<=>?[\\\]^|-])', '\\\1', 'g')
$func$;

在 Postgres 10 或更高版本中添加 PARALLEL SAFE(因为它)以允许使用它的查询的并行性。

演示

SELECT f_regexp_escape('test(1) > Foo*');

返回:

test\(1\) \> Foo\*

同时:

SELECT 'test(1) > Foo*' ~ 'test(1) > Foo*';

返回FALSE,这可能会让天真的用户大吃一惊,

SELECT 'test(1) > Foo*' ~ f_regexp_escape('test(1) > Foo*');

返回TRUE,就像现在一样。

LIKE转义函数

为了完整起见,LIKE 模式的挂件,其中只有三个字符是特殊的:

\%_

The manual:

默认转义字符是反斜杠,但可以使用ESCAPE 子句选择不同的转义字符。

此函数采用默认值:

CREATE OR REPLACE FUNCTION f_like_escape(text)
  RETURNS text
  LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS
$func$
SELECT replace(replace(replace($1
         , '\', '\\')  -- must come 1st
         , '%', '\%')
         , '_', '\_');
$func$;

我们也可以在这里使用更优雅的regexp_replace(),但对于少数字符,replace() 函数的级联会更快。

同样,PARALLEL SAFE 在 Postgres 10 或更高版本中。

演示

SELECT f_like_escape('20% \ 50% low_prices');

返回:

20\% \\ 50\% low\_prices

【讨论】:

感谢您的出色回答,也很好的解释。这对我帮助很大! 在 postgres 11 中尝试这个, f_regex_escape 的演示返回输入字符串,没有转义字符。 SELECT regexp_replace('test(1) &gt; Foo*', '([!$()*+.:&lt;=&gt;?[\\]^|-])', '\\1', 'g') 返回test(1) &gt; Foo* @zurbergram:在您的示例中,第三个 \ 在两个地方丢失了。 (可能是由于评论显示,\ 需要转义。)它的工作原理与宣传的一样:dbfiddle here 呃。很棒的答案,但很遗憾没有原生解决方案【参考方案2】:

您可以随意更改架构吗?我认为如果您可以使用复合主键,问题就会消失:

name text not null,
number integer not null,
primary key (name, number)

然后显示层的职责就是将 Fred #0 显示为“Fred”,将 Fred #1 显示为“Fred (1)”,等等。

如果您愿意,您可以为此职责创建一个视图。这是数据:

=> select * from foo;
  name  | number 
--------+--------
 Fred   |      0
 Fred   |      1
 Barney |      0
 Betty  |      0
 Betty  |      1
 Betty  |      2
(6 rows)

观点:

create or replace view foo_view as
select *,
case
  when number = 0 then
    name
  else
    name || ' (' || number || ')'
end as name_and_number
from foo;

结果:

=> select * from foo_view;
  name  | number | name_and_number 
--------+--------+-----------------
 Fred   |      0 | Fred
 Fred   |      1 | Fred (1)
 Barney |      0 | Barney
 Betty  |      0 | Betty
 Betty  |      1 | Betty (1)
 Betty  |      2 | Betty (2)
(6 rows)

【讨论】:

我考虑过这个解决方案,但由于插入数据确实是唯一存在问题的时间,因此大部分工作将是对所有显示查询进行更改。看起来修改插入查询会容易得多 @Benny,如果你愿意,你可以使用视图。请参阅修改后的答案。【参考方案3】:

尝试这样的事情怎么样,用var_name代替我硬编码的'John Bernard'

create table my_table(name text primary key);
insert into my_table(name) values ('John Bernard'), 
                                  ('John Bernard (1)'), 
                                  ('John Bernard (2)'), 
                                  ('John Bernard (3)');


select max(regexp_replace(substring(name, 13), ' |\(|\)', '', 'g')::integer+1) 
from my_table 
where substring(name, 1, 12)='John Bernard' 
      and substring(name, 13)~'^ \([1-9][0-9]*\)$';

 max
-----
   4
(1 row)

一个警告:我假设在此过程运行时单用户访问数据库(您的方法也是如此)。如果不是这样,那么max(n)+1 方法将不是一个好方法。

【讨论】:

数据库将被多个用户访问,但我的查询中的附加过滤器(在我的问题中省略)将限制它考虑一次只允许一个用户使用的行。感谢您的帮助,我会接受您拆分名称并使用 substring 和 char_length(字面上用 12 表示)分别比较这两个部分的解决方案。

以上是关于正则表达式或 LIKE 模式的转义函数的主要内容,如果未能解决你的问题,请参考以下文章

notepad++ 正则表达式引擎 (scintilla) 是不是支持子字符串转义(smth. like "\Q.*[escaped string]()+\E")?

Oracle 正则表达式函数-REGEXP_LIKE 使用例子

了解下C# 正则表达式

Oracle正则表达式

数据挖掘必备技能——正则表达式

C#--正则表达式