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


515

我有一张约有50万行的表格;varchar(255)UTF8列filename包含一个文件名;

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

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

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

Output: "stackoverflow"

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

Output: "-tackover-low"

我知道REGEXP / RLIKE,但那些只检查是否有匹配,没有什么比赛是。

(我可以做一个“ SELECT pkey_id,filename FROM foo WHERE filename RLIKE '[^a-zA-Z0-9()_ .\-]'”从PHP脚本,做了preg_replace,然后“ UPDATE foo ... WHERE pkey_id=...”,但看起来像一个不得已的缓慢和丑陋的黑客)


8
自2007年以来,这是一项功能请求:bugs.mysql.com/bug.php?id=27389。如果您确实需要此功能,请登录并单击“影响我”按钮。希望它将获得足够的选票。
TMS 2014年

4
@Tomas:我已经在2009年这样做了,当时我正在寻找它。由于进展为零-显然这不是一个重要的功能。(btw Postgres拥有它:stackoverflow.com/questions/11722995/…
Piskvor在

1
相关的,简单的,版本这个问题:stackoverflow.com/questions/6942973/...
Kzqai

2
我创建了regexp_split(函数+过程)&regexp_replace,并通过REGEXPoperator 来实现。对于简单的查找,它可以解决问题。您可能会在这里找到它-因此,这是MySQL存储代码(没有UDF)的方法。如果您发现一些错误,而这些错误没有被已知的限制所覆盖,请随时打开问题。
Alma Do

1
从另一个SO线程找到了这个库:github.com/mysqludf/lib_mysqludf_preg可以完美地工作。
凯尔(Kyle)

Answers:


77

使用MySQL 8.0+,您可以使用本机REGEXP_REPLACE功能。

12.5.2正则表达式

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

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

正则表达式支持

此前,MySQL的使用的亨利斯宾塞正则表达式库来支持正则表达式运算符(REGEXPRLIKE)。

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

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

DBFiddle演示


146

MySQL 8.0+

您可以使用本机REGEXP_REPLACE功能。

旧版本:

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


3
REGEXP_REPLACE是用户定义的函数吗?看起来很有希望,会调查一下。谢谢!
Piskvor于

15
不幸的是,mysql-udf-regexp似乎不支持多字节字符。regexp_replace('äöõü','ä','')返回一个长数字字符串,而不是真实文本。
lkraav

3
MySQL本身的RegEx功能不支持多字节字符。
布拉德

4
Windows用户:此处链接的UDF库似乎没有很好的Windows支持。概述的Windows安装方法对我来说效果不佳。
乔纳森(Jonathan)

2
@lkraav,您应该尝试下面的lib_mysqludf_preg库,因为它很好用。这个冗长的版本默认情况下会返回一个blob,并且我不知道您是否将多字节字符集设置为默认字符集:选择cast(TR as char)COLLUT utf8_unicode_ci from(select preg_replace('/ä/',``, 'öõüä')R)T
gillyspy 2014年

124

请改用MariaDB。具有功能

REGEXP_REPLACE(col, regexp, replace)

请参阅MariaDB文档PCRE正则表达式增强功能

请注意,您也可以使用regexp分组(我发现这非常有用):

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

退货

over - stack - flow

12
这是来自mariadb 10
Nick

6
下次我需要它时,这里是更改整个列的语法:UPDATE table SET Name = REGEXP_REPLACE(Name, "-2$", "\\1")这会从整个列中一次删除abcxyz-2中的-2。
乔西亚

27
更改整个平台几乎不是一个现实的解决方案。
David Baucum '17

3
@DavidBaucum MariaDB是MySQL的直接替代品。因此,这不是“更换平台”,而更像是选择同一趟飞机的另一家航空公司
Benvorth


113

我的暴力破解方法就是:

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

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


33
好吧,那也应该起作用;我没有考虑离线替换。很好的开箱即用的思维!
Piskvor在

10
对您来说,使用find这样的命令似乎令我感到奇怪,我将命令缩短为sed -i's / old_string / new_string / g'/path/to/dump.sql
speshak 2012年

36
风险很大,对于大数据集或适当的参照完整性不切实际:要删除数据然后再次插入,必须关闭参照完整性,实际上也要关闭数据库。
劳尔·卢纳

5
过去使用过这种方法后,我对劳尔(Raul)表示反对,这非常冒险。您还需要绝对确定,您的字符串不在数据集中。
eggmatters 2015年

1
@speshak的答案已经晚了几年,但是我之所以选择像这样访问文件,是因为出于与上述相同的原因,我最初非常紧张。当时看来,将“查找文件”部分与“替换”部分分开会使代码在我提交之前更容易阅读
Ryan Ward

42

我们无需使用正则表达式即可解决此问题,此查询仅替换完全匹配的字符串。

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

例:

emp_id employee_firstname

1个杰伊

2杰伊·阿杰

3周杰伦

执行查询结果后:

emp_id employee_firstname

1 abc

2 abc阿杰

3 abc


@yellowmelon两对双引号分别代表什么?
codecowboy16年

5
他在职员名前后加上空格。这样,他就可以搜索替换(space)employeename(空格),从而避免在大字符串“ ajay”的一部分中捕获雇员姓名“ jay”。然后,他在完成后将空间修剪掉。
Slam 2016年

42

我最近写了一个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 ._,');

25
我就加强上述观点:这个函数替换字符匹配单个字符的表达式。上面说过,它被用来“使用正则表达式替换字符串”,这可能会引起误解。它完成了它的工作,但不是要的工作。(不是投诉-只是为了让领导者走错路)
杰森(Jason

2
在答案中实际包含代码,而不是发布裸露的链接会更有用。
phobie 2015年

2
很好-但不幸的是,它不处理类似引用select regex_replace('.*(abc).*','\1','noabcde')(返回'noabcde',而不是'abc')。
Izzy

@phobie有人在此答案中进行了此操作 – 仅供参考,以防链接消失;)
Izzy

我已经修改了此方法,以尝试解决上面提到的一些限制以及更多限制。请看这个答案
史蒂夫·钱伯斯


13

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


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


以下内容对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 < 1, 1, minMatchLen);
    SET maxMatchLen = IF(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演示

局限性

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

使用范例

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


7

您可以“做到”……但这并不是很明智的选择……这就像我将要尝试的那样大胆……只要RegReg完全支持您使用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[[:>:]]'

1
不,那行不通。假设您的列包含“ asdfWORD_TO_REPLACE WORD_TO_REPLACE”。您的方法将导致“ asdfREPLACEMENT REPLACEMENT”,其中正确答案为“ asdfWORD_TO_REPLACE REPLACEMENT”。
瑞安·希灵顿

1
@Ryan ...这就是为什么我说这不是很明智的原因...在您提供的用例中,这肯定会失败。简而言之,使用“类似于正则表达式”的结构是个坏主意。更糟的是...如果删除where子句,则所有值都将为NULL ...
Eddie B

1
实际上,Ryan在这种情况下是不正确的,因为标记只能找到零长度单词“ boundaries”的匹配项,因此只有在单词前后都有边界的单词才可以匹配……但这仍然不是一个好主意……
Eddie B

6

我们可以在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');

您好,谢谢您的建议。我一直在尝试类似的方法,但是数据集的性能却不尽人意。对于较小的布景,这可能是可行的。
Piskvor于

3

下面的一个基本上是从左侧找到第一个匹配项,然后替换所有匹配项(在 )。

用法:

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 ;

3

我认为有一个简单的方法可以实现这一目标,并且对我来说很好。

使用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版本很容易做到
Piskvor离开大楼
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.