如何在 MySQL 中进行正则表达式替换?

Posted

技术标签:

【中文标题】如何在 MySQL 中进行正则表达式替换?【英文标题】:How to do a regular expression replace in MySQL? 【发布时间】:2010-11-02 11:27:00 【问题描述】:

我有一个大约 500k 行的表; varchar(255) UTF8 列filename 包含一个文件名;

我正在尝试从文件名中去除各种奇怪的字符 - 我想我会使用一个字符类:[^a-zA-Z0-9()_ .\-]

现在,MySQL 中是否有一个函数可以让您通过正则表达式进行替换?我正在寻找与 REPLACE() 函数类似的功能 - 简化示例如下:

SELECT REPLACE('stackowerflow', 'ower', 'over');

Output: "***"

/* does something like this exist? */
SELECT X_REG_REPLACE('***','/[A-Zf]/','-'); 

Output: "-tackover-low"

我知道REGEXP/RLIKE,但那些只检查如果有匹配,而不是什么匹配。

(我可以php 脚本中执行“SELECT pkey_id,filename FROM foo WHERE filename RLIKE '[^a-zA-Z0-9()_ .\-]'”,然后执行 preg_replace,然后执行“UPDATE foo ... WHERE pkey_id=...”,但这看起来像是最后的手段,缓慢而丑陋的 hack )

【问题讨论】:

这是自 2007 年以来的功能请求:bugs.mysql.com/bug.php?id=27389。如果您真的想要此功能,请登录并单击“影响我”按钮。希望它能获得足够的选票。 @Tomas:我已经做到了......在 2009 年,当时我正在四处寻找它。由于它的进展为零 - 显然它不是一个重要的功能。 (顺便说一句 Postgres 有它:***.com/questions/11722995/…) 这个问题的相关,更简单的版本:***.com/questions/6942973/… 我创建了regexp_split(函数+过程)和regexp_replace,它们是用REGEXP操作符实现的。对于简单的查找,它可以解决问题。你可能会发现它here - 所以,这是 MySQL 存储代码的方式,没有 UDF。如果您发现一些已知限制未涵盖的错误,请随时打开问题。 从另一个 SO 线程找到这个库:github.com/mysqludf/lib_mysqludf_preg 完美运行。 【参考方案1】:

MySQL 8.0+

你可以使用原生的REGEXP_REPLACE函数。

旧版本:

您可以使用用户定义的函数 (UDF),例如 mysql-udf-regexp。

【讨论】:

REGEXP_REPLACE 作为用户定义的函数?看起来很有希望,会研究它。谢谢! 不幸的是 mysql-udf-regexp 似乎不支持多字节字符。 regexp_replace('äöõü', 'ä', '') 返回一个长数字字符串而不是实际文本。 MySQL 本身的 RegEx 特性不支持多字节字符。 Windows 用户:此处链接的 UDF 库似乎没有良好的 Windows 支持。概述的 Windows 安装方法对我来说效果不佳。 @lkraav 你应该试试下面的 lib_mysqludf_preg 库,因为它很好用。这是详细版本,因为它默认返回一个 blob,我不知道您是否有一个多字节字符集作为默认值: select cast( TR as char) COLLATE utf8_unicode_ci from (select preg_replace('/ä/', '', 'öõüä') R) T【参考方案2】:

我最近编写了一个 MySQL 函数来使用正则表达式替换字符串。您可以在以下位置找到我的帖子:

http://techras.wordpress.com/2011/06/02/regex-replace-for-mysql/

这里是功能代码:

DELIMITER $$

CREATE FUNCTION  `regex_replace`(pattern VARCHAR(1000),replacement VARCHAR(1000),original VARCHAR(1000))
RETURNS VARCHAR(1000)
DETERMINISTIC
BEGIN 
 DECLARE temp VARCHAR(1000); 
 DECLARE ch VARCHAR(1); 
 DECLARE i INT;
 SET i = 1;
 SET temp = '';
 IF original REGEXP pattern THEN 
  loop_label: LOOP 
   IF i>CHAR_LENGTH(original) THEN
    LEAVE loop_label;  
   END IF;
   SET ch = SUBSTRING(original,i,1);
   IF NOT ch REGEXP pattern THEN
    SET temp = CONCAT(temp,ch);
   ELSE
    SET temp = CONCAT(temp,replacement);
   END IF;
   SET i=i+1;
  END LOOP;
 ELSE
  SET temp = original;
 END IF;
 RETURN temp;
END$$

DELIMITER ;

示例执行:

mysql> select regex_replace('[^a-zA-Z0-9\-]','','2my test3_text-to. check \\ my- sql (regular) ,expressions ._,');

【讨论】:

我只是强调上面的一点:这个函数替换匹配单个字符表达式的characters。上面说它用于“使用正则表达式替换字符串”,这可能有点误导。它完成了它的工作,但它不是被要求的工作。 (不是抱怨 - 只是为了让引导人们走上错误的道路) 很好——但不幸的是不处理像select regex_replace('.*(abc).*','\1','noabcde')这样的引用(返回'noabcde',而不是'abc')。 我已经修改了这个方法来尝试解决上面提到的一些限制以及更多。请参阅this answer。 @Izzy MySQL 8+ REGEXP_REPLACE 内置函数也没有,对吧?我正在尝试,它似乎不起作用 @golimar 我不知道。而且我也无法测试,因为我没有 MySQL 8 或更高版本。【参考方案3】:

我让这个工作的蛮力方法只是:

    转储表格 - mysqldump -u user -p database table > dump.sql 查找并替换几个模式 - find /path/to/dump.sql -type f -exec sed -i 's/old_string/new_string/g' \;,显然您还可以对文件执行其他 perl 正则表达式。 导入表 - mysqlimport -u user -p database table < dump.sql

如果您想确保字符串不在数据集中的其他位置,请运行一些正则表达式以确保它们都出现在相似的环境中。在运行替换之前创建备份也不是那么难,以防您不小心破坏了丢失信息深度的东西。

【讨论】:

好的,应该也可以;我没有考虑离线替换。很好的开箱即用的想法! 对我来说似乎很奇怪你会这样使用 find,我会将命令缩短为 sed -i 's/old_string/new_string/g' /path/to/dump.sql 非常危险,并且对于大数据集或引用完整性不切实际:要删除数据然后再次插入,您必须关闭引用完整性,实际上您的数据库也会关闭. 以前用过这种方法,我不同意Raul,这是非常危险的。您还需要绝对确定,您的字符串不在数据集中的其他位置。 @speshak 的答案迟到了几年,但我选择这样访问文件的原因是因为我最初非常紧张,原因与上述相同。当时似乎将“查找文件”部分与“替换”部分分开会使代码在我提交之前更易于阅读【参考方案4】:

您“可以”做到...但这不是很明智...这是我将尝试的最大胆...只要完整的 RegEx 支持您最好使用 perl 或类似的东西。

UPDATE db.tbl
SET column = 
CASE 
WHEN column REGEXP '[[:<:]]WORD_TO_REPLACE[[:>:]]' 
THEN REPLACE(column,'WORD_TO_REPLACE','REPLACEMENT')
END 
WHERE column REGEXP '[[:<:]]WORD_TO_REPLACE[[:>:]]'

【讨论】:

不,这行不通。假设您的列包含“asdfWORD_TO_REPLACE WORD_TO_REPLACE”。您的方法将导致“asdfREPLACEMENT REPLACEMENT”,其中正确答案是“asdfWORD_TO_REPLACE REPLACEMENT”。 @Ryan ...这正是我说这不是很明智的原因...在您提供的用例中,这肯定会失败。简而言之,使用“类似正则表达式”的结构是个坏主意。更糟糕的是......如果你放弃 where 子句,你的所有值都将是 NULL ... 实际上 Ryan 在这种情况下你是不正确的,因为标记只会找到零长度单词“边界”的匹配项,所以只有在单词前后有边界的单词才会匹配......它仍然虽然是个坏主意...【参考方案5】:

我很高兴地报告,自从提出这个问题以来,现在有一个令人满意的答案!看看这个很棒的包:

https://github.com/mysqludf/lib_mysqludf_preg

示例 SQL:

SELECT PREG_REPLACE('/(.*?)(fox)/' , 'dog' , 'the quick brown fox' ) AS demo;

我在this question 上找到了来自this blog post 的包。

【讨论】:

【参考方案6】:

如果您使用的是 MariaDB 或 MySQL 8.0,它们有一个功能

REGEXP_REPLACE(col, regexp, replace)

见MariaDB docs和PCRE Regular expression enhancements

请注意,您也可以使用正则表达式分组(我发现这非常有用):

SELECT REGEXP_REPLACE("***", "(stack)(over)(flow)", '\\2 - \\1 - \\3')

返回

over - stack - flow

【讨论】:

这是来自 mariadb 10 下次我需要它时,这里是更改整列的语法:UPDATE table SET Name = REGEXP_REPLACE(Name, "-2$", "\\1") 这会立即从整列中删除 abcxyz-2 中的 -2。 改变整个平台几乎不是一个现实的解决方案。 @DavidBaucum MariaDB 是 MySQL 的替代品。所以这不是“换平台”,更像是为同一次旅行选择不同的航空公司 @Benvorth MySQL 8.0 supports it too.【参考方案7】:

我们可以在SELECT查询中使用IF条件如下:

假设对于任何带有“ABC”、“ABC1”、“ABC2”、“ABC3”、...的东西,我们想用“ABC”替换,然后在 SELECT 查询中使用 REGEXP 和 IF() 条件,我们可以做到这一点。

语法:

SELECT IF(column_name REGEXP 'ABC[0-9]$','ABC',column_name)
FROM table1 
WHERE column_name LIKE 'ABC%';

例子:

SELECT IF('ABC1' REGEXP 'ABC[0-9]$','ABC','ABC1');

【讨论】:

您好,谢谢您的建议。我一直在尝试类似的东西,但我的数据集的性能并不令人满意。对于较小的集合,这可能是可行的。【参考方案8】:

我们在不使用正则表达式的情况下解决了这个问题 此查询仅替换完全匹配的字符串。

update employee set
employee_firstname = 
trim(REPLACE(concat(" ",employee_firstname," "),' jay ',' abc '))

例子:

emp_id 员工名字

1 杰

2 周杰伦

3 杰

执行查询结果后:

emp_id 员工名字

1 abc

2 abc ajay

3 abc

【讨论】:

@yellowmelon 两对双引号是干什么用的? 他在员工姓名前后用空格填充。这允许他搜索-替换 (space)employeename(space),如果它是较大字符串“ajay”的一部分,则可以避免捕获员工名称“jay”。然后他在完成后修剪空间。【参考方案9】:

更新 2: MySQL 8.0 现在提供了一组有用的正则表达式函数,包括 REGEXP_REPLACE。除非您被限制使用较早的版本,否则这会导致不必要的阅读。


更新 1: 现在已将其写入博客文章:http://stevettt.blogspot.co.uk/2018/02/a-mysql-regular-expression-replace.html


以下内容扩展了function provided by Rasika Godawatte,但会搜索所有必要的子字符串,而不仅仅是测试单个字符:

-- ------------------------------------------------------------------------------------
-- USAGE
-- ------------------------------------------------------------------------------------
-- SELECT reg_replace(<subject>,
--                    <pattern>,
--                    <replacement>,
--                    <greedy>,
--                    <minMatchLen>,
--                    <maxMatchLen>);
-- where:
-- <subject> is the string to look in for doing the replacements
-- <pattern> is the regular expression to match against
-- <replacement> is the replacement string
-- <greedy> is TRUE for greedy matching or FALSE for non-greedy matching
-- <minMatchLen> specifies the minimum match length
-- <maxMatchLen> specifies the maximum match length
-- (minMatchLen and maxMatchLen are used to improve efficiency but are
--  optional and can be set to 0 or NULL if not known/required)
-- Example:
-- SELECT reg_replace(txt, '^[Tt][^ ]* ', 'a', TRUE, 2, 0) FROM tbl;
DROP FUNCTION IF EXISTS reg_replace;
DELIMITER //
CREATE FUNCTION reg_replace(subject VARCHAR(21845), pattern VARCHAR(21845),
  replacement VARCHAR(21845), greedy BOOLEAN, minMatchLen INT, maxMatchLen INT)
RETURNS VARCHAR(21845) DETERMINISTIC BEGIN 
  DECLARE result, subStr, usePattern VARCHAR(21845); 
  DECLARE startPos, prevStartPos, startInc, len, lenInc INT;
  IF subject REGEXP pattern THEN
    SET result = '';
    -- Sanitize input parameter values
    SET minMatchLen = IF(minMatchLen IS NULL OR minMatchLen < 1, 1, minMatchLen);
    SET maxMatchLen = IF(maxMatchLen IS NULL OR maxMatchLen < 1
                         OR maxMatchLen > CHAR_LENGTH(subject),
                         CHAR_LENGTH(subject), maxMatchLen);
    -- Set the pattern to use to match an entire string rather than part of a string
    SET usePattern = IF (LEFT(pattern, 1) = '^', pattern, CONCAT('^', pattern));
    SET usePattern = IF (RIGHT(pattern, 1) = '$', usePattern, CONCAT(usePattern, '$'));
    -- Set start position to 1 if pattern starts with ^ or doesn't end with $.
    IF LEFT(pattern, 1) = '^' OR RIGHT(pattern, 1) <> '$' THEN
      SET startPos = 1, startInc = 1;
    -- Otherwise (i.e. pattern ends with $ but doesn't start with ^): Set start pos
    -- to the min or max match length from the end (depending on "greedy" flag).
    ELSEIF greedy THEN
      SET startPos = CHAR_LENGTH(subject) - maxMatchLen + 1, startInc = 1;
    ELSE
      SET startPos = CHAR_LENGTH(subject) - minMatchLen + 1, startInc = -1;
    END IF;
    WHILE startPos >= 1 AND startPos <= CHAR_LENGTH(subject)
      AND startPos + minMatchLen - 1 <= CHAR_LENGTH(subject)
      AND !(LEFT(pattern, 1) = '^' AND startPos <> 1)
      AND !(RIGHT(pattern, 1) = '$'
            AND startPos + maxMatchLen - 1 < CHAR_LENGTH(subject)) DO
      -- Set start length to maximum if matching greedily or pattern ends with $.
      -- Otherwise set starting length to the minimum match length.
      IF greedy OR RIGHT(pattern, 1) = '$' THEN
        SET len = LEAST(CHAR_LENGTH(subject) - startPos + 1, maxMatchLen), lenInc = -1;
      ELSE
        SET len = minMatchLen, lenInc = 1;
      END IF;
      SET prevStartPos = startPos;
      lenLoop: WHILE len >= 1 AND len <= maxMatchLen
                 AND startPos + len - 1 <= CHAR_LENGTH(subject)
                 AND !(RIGHT(pattern, 1) = '$' 
                       AND startPos + len - 1 <> CHAR_LENGTH(subject)) DO
        SET subStr = SUBSTRING(subject, startPos, len);
        IF subStr REGEXP usePattern THEN
          SET result = IF(startInc = 1,
                          CONCAT(result, replacement), CONCAT(replacement, result));
          SET startPos = startPos + startInc * len;
          LEAVE lenLoop;
        END IF;
        SET len = len + lenInc;
      END WHILE;
      IF (startPos = prevStartPos) THEN
        SET result = IF(startInc = 1, CONCAT(result, SUBSTRING(subject, startPos, 1)),
                        CONCAT(SUBSTRING(subject, startPos, 1), result));
        SET startPos = startPos + startInc;
      END IF;
    END WHILE;
    IF startInc = 1 AND startPos <= CHAR_LENGTH(subject) THEN
      SET result = CONCAT(result, RIGHT(subject, CHAR_LENGTH(subject) + 1 - startPos));
    ELSEIF startInc = -1 AND startPos >= 1 THEN
      SET result = CONCAT(LEFT(subject, startPos), result);
    END IF;
  ELSE
    SET result = subject;
  END IF;
  RETURN result;
END//
DELIMITER ;

演示

Rextester Demo

限制

    这个方法当然要花点时间当主题 字符串很大。 更新:现在添加了最小和最大匹配长度参数,以在这些参数已知时提高效率(零 = 未知/无限)。不会允许替换反向引用(例如\1\2 等)来替换捕获组。如果需要此功能,请参阅 this answer,它尝试通过更新该功能以允许在每个找到的匹配项中进行二次查找和替换(以增加复杂性为代价)来提供解决方法。 如果在模式中使用^和/或$,它们必须分别位于最开始和最结束 - 例如不支持 (^start|end$) 等模式。 有一个“贪婪”标志来指定整体匹配是贪婪还是非贪婪。不支持在单个正则表达式(例如 a.*?b.*)中结合贪婪和惰性匹配。

用法示例

该函数已用于回答以下 *** 问题:

How to count words in MySQL / regular expression replacer? How to extract the nth word and count word occurrences in a MySQL string? How to extract two consecutive digits from a text field in MySQL? How to remove all non-alpha numeric characters from a string in MySQL? How to replace every other instance of a particular character in a MySQL string? How to get all distinct words of a specified minimum length from multiple columns in a MySQL table?

【讨论】:

与此函数的文档所建议的不同,null 不能用于代替 maxMatchLengthminMatchLength 参数中的 0,至少在 mariadb 5.5.60 中 好地方 - 现在已更新 SQL 以允许 NULL【参考方案10】:

使用 MySQL 8.0+,您可以使用本机 REGEXP_REPLACE 函数。

12.5.2 Regular Expressions:

REGEXP_REPLACE(expr, pat, repl[, pos[, occurrence[, match_type]]])

用替换字符串 repl 替换字符串 expr 中匹配由模式 pat 指定的正则表达式的匹配项,并返回结果细绳。如果 exprpatreplNULL,则返回值为 NULL

和Regular expression support:

以前,MySQL 使用 Henry Spencer 正则表达式库来支持正则表达式运算符(REGEXPRLIKE)。

使用 Unicode 国际组件 (ICU) 重新实现了正则表达式支持,它提供完整的 Unicode 支持并且是多字节安全的。 REGEXP_LIKE() 函数以REGEXPRLIKE 运算符的方式执行正则表达式匹配,它们现在是该函数的同义词。 此外, REGEXP_INSTR() REGEXP_REPLACE() REGEXP_SUBSTR() 函数可用于查找匹配位置并执行子字符串分别替换和提取。

SELECT REGEXP_REPLACE('***','[A-Zf]','-',1,0,'c'); 
-- Output:
-tackover-low

DBFiddle Demo

【讨论】:

【参考方案11】:

下面的基本上从左边找到第一个匹配,然后替换它的所有出现(在mysql-5.6中测试)。

用法:

SELECT REGEX_REPLACE('dis ambiguity', 'dis[[:space:]]*ambiguity', 'disambiguity');

实施:

DELIMITER $$
CREATE FUNCTION REGEX_REPLACE(
  var_original VARCHAR(1000),
  var_pattern VARCHAR(1000),
  var_replacement VARCHAR(1000)
  ) RETURNS
    VARCHAR(1000)
  COMMENT 'Based on https://techras.wordpress.com/2011/06/02/regex-replace-for-mysql/'
BEGIN
  DECLARE var_replaced VARCHAR(1000) DEFAULT var_original;
  DECLARE var_leftmost_match VARCHAR(1000) DEFAULT
    REGEX_CAPTURE_LEFTMOST(var_original, var_pattern);
    WHILE var_leftmost_match IS NOT NULL DO
      IF var_replacement <> var_leftmost_match THEN
        SET var_replaced = REPLACE(var_replaced, var_leftmost_match, var_replacement);
        SET var_leftmost_match = REGEX_CAPTURE_LEFTMOST(var_replaced, var_pattern);
        ELSE
          SET var_leftmost_match = NULL;
        END IF;
      END WHILE;
  RETURN var_replaced;
END $$
DELIMITER ;

DELIMITER $$
CREATE FUNCTION REGEX_CAPTURE_LEFTMOST(
  var_original VARCHAR(1000),
  var_pattern VARCHAR(1000)
  ) RETURNS
    VARCHAR(1000)
  COMMENT '
  Captures the leftmost substring that matches the [var_pattern]
  IN [var_original], OR NULL if no match.
  '
BEGIN
  DECLARE var_temp_l VARCHAR(1000);
  DECLARE var_temp_r VARCHAR(1000);
  DECLARE var_left_trim_index INT;
  DECLARE var_right_trim_index INT;
  SET var_left_trim_index = 1;
  SET var_right_trim_index = 1;
  SET var_temp_l = '';
  SET var_temp_r = '';
  WHILE (CHAR_LENGTH(var_original) >= var_left_trim_index) DO
    SET var_temp_l = LEFT(var_original, var_left_trim_index);
    IF var_temp_l REGEXP var_pattern THEN
      WHILE (CHAR_LENGTH(var_temp_l) >= var_right_trim_index) DO
        SET var_temp_r = RIGHT(var_temp_l, var_right_trim_index);
        IF var_temp_r REGEXP var_pattern THEN
          RETURN var_temp_r;
          END IF;
        SET var_right_trim_index = var_right_trim_index + 1;
        END WHILE;
      END IF;
    SET var_left_trim_index = var_left_trim_index + 1;
    END WHILE;
  RETURN NULL;
END $$
DELIMITER ;

【讨论】:

【参考方案12】:

我认为有一种简单的方法可以实现这一点,而且对我来说效果很好。

使用 REGEX 选择行

SELECT * FROM `table_name` WHERE `column_name_to_find` REGEXP 'string-to-find'

使用 REGEX 更新行

UPDATE `table_name` SET column_name_to_find=REGEXP_REPLACE(column_name_to_find, 'string-to-find', 'string-to-replace') WHERE column_name_to_find REGEXP 'string-to-find'

REGEXP 参考: https://www.geeksforgeeks.org/mysql-regular-expressions-regexp/

【讨论】:

谢谢 :) 从版本 8 开始就可以轻松完成了。【参考方案13】:

是的,你可以。

UPDATE table_name 
  SET column_name = 'seach_str_name'
  WHERE column_name REGEXP '[^a-zA-Z0-9()_ .\-]';

【讨论】:

以上是关于如何在 MySQL 中进行正则表达式替换?的主要内容,如果未能解决你的问题,请参考以下文章

MySQL正则表达式搜索和替换[重复]

正则表达式高级替换,匹配后进行运算,然后使用结果替换,怎么实现?

mysql 正则表达式 如何截取字符串中指定格式的字符

如何使用REPLACE和正则表达式替换MYSQL中字符串中的多个关键字[重复]

如何进行正则表达式替换,在日期字符串中添加字符?

UE中如何使用正则替换行中部分字符串