¿Cómo reemplazar una expresión regular en MySQL?

516

Tengo una tabla con ~ 500k filas; La columna varchar (255) UTF8 filenamecontiene un nombre de archivo;

Estoy tratando de eliminar varios caracteres extraños del nombre de archivo, pensé que usaría una clase de caracteres: [^a-zA-Z0-9()_ .\-]

Ahora, ¿hay una función en MySQL que le permita reemplazar a través de una expresión regular ? Estoy buscando una funcionalidad similar a la función REPLACE (): a continuación se muestra un ejemplo simplificado:

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

Output: "stackoverflow"

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

Output: "-tackover-low"

Sé acerca de REGEXP / RLIKE , pero esos solo comprueban si hay una coincidencia, no cuál es la coincidencia.

( Podría hacer un " SELECT pkey_id,filename FROM foo WHERE filename RLIKE '[^a-zA-Z0-9()_ .\-]'" desde un script PHP, hacer un preg_replacey luego " UPDATE foo ... WHERE pkey_id=...", pero parece un truco lento y feo de último recurso)

Piskvor salió del edificio
fuente
8
Es una solicitud de funciones desde 2007: bugs.mysql.com/bug.php?id=27389 . Si realmente desea esta función, inicie sesión y haga clic en el botón "Me afecta". Esperemos que obtenga suficientes votos.
TMS
44
@Tomas: Lo hice ... en 2009, cuando lo estaba buscando. Dado que no ha habido ningún progreso, aparentemente no es una característica tan importante. (por cierto Postgres lo tiene: stackoverflow.com/questions/11722995/… )
Piskvor salió del edificio el
1
Versión más simple y relacionada de esta pregunta: stackoverflow.com/questions/6942973/…
Kzqai
2
He creado regexp_split(función + procedimiento) & regexp_replace, que se implementan con el REGEXPoperador. Para búsquedas simples, hará el truco. Puede encontrarlo aquí , así que esta es la forma con el código almacenado de MySQL, sin UDF. Si encuentra algunos errores, que no están cubiertos por limitaciones conocidas, no dude en abrir el problema.
Alma Do
1
Encontré esta biblioteca desde otro hilo SO: github.com/mysqludf/lib_mysqludf_preg funciona perfectamente.
Kyle

Respuestas:

78

Con MySQL 8.0+ puede usar la REGEXP_REPLACEfunción nativa .

12.5.2 Expresiones regulares :

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

Reemplaza las ocurrencias en la cadena expr que coinciden con la expresión regular especificada por el patrón pat con la respuesta de la cadena de reemplazo , y devuelve la cadena resultante. Si expr , pat o repl es NULL, el valor de retorno es NULL.

y soporte de expresiones regulares :

Anteriormente, MySQL usaba la biblioteca de expresiones regulares de Henry Spencer para admitir operadores de expresiones regulares ( REGEXP, RLIKE).

El soporte de expresiones regulares se ha reimplementado utilizando International Components for Unicode (ICU), que proporciona soporte completo de Unicode y es seguro para múltiples bytes. La REGEXP_LIKE()función realiza una coincidencia de expresiones regulares a la manera deREGEXPRLIKE operadores y , que ahora son sinónimos de esa función. Además, el REGEXP_INSTR(), REGEXP_REPLACE()y REGEXP_SUBSTR() las funciones están disponibles para encontrar las posiciones de los partidos y llevar a cabo la sustitución de subcadenas y extracción, respectivamente.

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

DBFiddle Demo

Lukasz Szozda
fuente
147

MySQL 8.0+ :

Puedes usar la REGEXP_REPLACEfunción nativa .

Versiones anteriores:

Puede usar una función definida por el usuario ( UDF ) como mysql-udf-regexp .

Jeremy Stein
fuente
3
¿REGEXP_REPLACE como una función definida por el usuario? Parece prometedor, lo investigará. ¡Gracias!
Piskvor salió del edificio el
15
Desafortunadamente, mysql-udf-regexp no parece tener soporte para caracteres multibyte. regexp_replace ('äöõü', 'ä', '') devuelve una larga cadena numérica en lugar de texto real.
lkraav
3
MySQL en sí mismo no admite caracteres de varios bytes con sus características RegEx.
Brad
44
Usuarios de Windows: la biblioteca UDF vinculada aquí no parece tener un buen soporte de Windows. El método de instalación de Windows descrito no funcionó bien para mí.
Jonathan
2
@lkraav deberías probar la biblioteca lib_mysqludf_preg a continuación, ya que funciona muy bien. Esta es la versión detallada, ya que devuelve un blob por defecto y no sé si tiene un juego de caracteres multibyte como predeterminado: seleccione cast (TR como char) COLLATE utf8_unicode_ci from (select preg_replace ('/ ä /', '', 'öõüä') R) T
gillyspy
124

Utilice MariaDB en su lugar. Tiene una funcion

REGEXP_REPLACE(col, regexp, replace)

Ver MariaDB docs y PCRE Mejoras de expresión regular

Tenga en cuenta que también puede usar la agrupación regexp (lo encontré muy útil):

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

devoluciones

over - stack - flow
Benvorth
fuente
12
esto es de mariadb 10
Nick
66
Para la próxima vez que lo necesite, aquí está la sintaxis para cambiar una columna completa: UPDATE table SET Name = REGEXP_REPLACE(Name, "-2$", "\\1")Esto elimina -2 de abcxyz-2 de una columna completa a la vez.
Josiah
27
Cambiar una plataforma completa no es una solución realista.
David Baucum
3
@DavidBaucum MariaDB es un reemplazo directo para MySQL. Por lo tanto, no es un "cambio de plataforma" sino más bien como elegir una aerolínea diferente para el mismo viaje
Benvorth,
3
@Benvorth MySQL 8.0 también lo admite .
Lukasz Szozda
113

Mi método de fuerza bruta para hacer que esto funcione fue simplemente:

  1. Volcar la mesa mysqldump -u user -p database table > dump.sql
  2. Encuentre y reemplace un par de patrones find /path/to/dump.sql -type f -exec sed -i 's/old_string/new_string/g' {} \;, obviamente, también hay otras expresiones regeulares perl que podría realizar en el archivo.
  3. Importar la tabla - mysqlimport -u user -p database table < dump.sql

Si desea asegurarse de que la cadena no esté en otra parte de su conjunto de datos, ejecute algunas expresiones regulares para asegurarse de que todas ocurran en un entorno similar. Tampoco es tan difícil crear una copia de seguridad antes de ejecutar un reemplazo, en caso de que destruya accidentalmente algo que pierde profundidad de información.

Ryan Ward
fuente
33
De acuerdo, eso también debería funcionar; No consideré un reemplazo fuera de línea. ¡Un pensamiento original listo para usar!
Piskvor salió del edificio
10
Me parece extraño que usarías encontrar así, acortaría el comando a sed -i / s_ old_string / new_string / g '/path/to/dump.sql
speshak
36
Muy arriesgado y poco práctico con grandes conjuntos de datos, o con integridad referencial en su lugar: para eliminar los datos y luego insertarlos nuevamente, tendrá que desactivar la integridad referencial, dejando en la práctica también su base de datos.
Raul Luna
55
Habiendo usado este método en el pasado, agrego con Raúl, esto es muy arriesgado. También debe estar absolutamente seguro de que su cadena no está en otra parte de su conjunto de datos.
Eggmatters
1
Años después de la respuesta @speshak, pero la razón por la que elegí acceder al archivo de esta manera fue porque originalmente estaba muy nervioso por las mismas razones mencionadas anteriormente. En ese momento parecía que separar la parte de "buscar el archivo" de la parte de "reemplazar" haría que el código fuera más fácil de leer antes de enviarlo
Ryan Ward
42

resolvemos este problema sin usar regex, esta consulta reemplaza solo la cadena de coincidencia exacta.

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

Ejemplo:

emp_id employee_firstname

1 jay

2 jay ajay

3 jay

Después de ejecutar el resultado de la consulta:

emp_id employee_firstname

1 abc

2 abc ajay

3 abc

Jay Patel
fuente
@yellowmelon ¿para qué son los dos pares de comillas dobles?
codecowboy
55
Está rellenando el nombre del empleado con espacios antes y después. Esto le permite buscar-reemplazar por (espacio) nombre de empleado (espacio), lo que evita atrapar el nombre de empleado "jay" si es parte de una cadena más grande "ajay". Luego recorta los espacios cuando termina.
Slam
42

Recientemente escribí una función MySQL para reemplazar cadenas usando expresiones regulares. Puedes encontrar mi publicación en la siguiente ubicación:

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

Aquí está el código de función:

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 ;

Ejecución de ejemplo:

mysql> select regex_replace('[^a-zA-Z0-9\-]','','2my test3_text-to. check \\ my- sql (regular) ,expressions ._,');
rasika godawatte
fuente
25
Solo reforzaré el punto anterior: esta función reemplaza los caracteres que coinciden con una expresión de un solo carácter. Dice anteriormente que se usa "para volver a unir cadenas usando expresiones regulares", y eso puede ser un poco engañoso. Hace su trabajo, pero no es el trabajo que se solicita. (No es una queja, es solo para salvar a las personas líderes por el camino equivocado)
Jason
2
Sería más útil incluir el código en su respuesta en lugar de publicar un enlace desnudo.
phobie
2
Agradable, pero desafortunadamente no trata con referencias como select regex_replace('.*(abc).*','\1','noabcde')(devuelve 'noabcde', no 'abc').
Izzy
@phobie alguien más lo hizo en esta respuesta , solo como referencia en caso de que el enlace muera;)
Izzy
He modificado este método para intentar abordar algunas de las limitaciones mencionadas anteriormente y más. Por favor vea esta respuesta .
Steve Chambers
13

ACTUALIZACIÓN 2: Un conjunto útil de funciones regex que incluye REGEXP_REPLACE ahora se ha proporcionado en MySQL 8.0. Esto hace que la lectura sea innecesaria a menos que esté limitado a usar una versión anterior.


ACTUALIZACIÓN 1: Ahora he convertido esto en una publicación de blog: http://stevettt.blogspot.co.uk/2018/02/a-mysql-regular-expression-replace.html


Lo siguiente se expande sobre la función proporcionada por Rasika Godawatte, pero rastrea todas las subcadenas necesarias en lugar de solo probar caracteres individuales:

-- ------------------------------------------------------------------------------------
-- 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 ;

Manifestación

Demo de Rextester

Limitaciones

  1. Por supuesto, este método llevará un tiempo cuando la cadena de asunto es grande. Actualización: ahora se han agregado parámetros de longitud de coincidencia mínima y máxima para mejorar la eficiencia cuando se conocen (cero = desconocido / ilimitado).
  2. Es no permitir sustitución de referencias hacia atrás (por ejemplo \1, \2 etc.) para sustituir a los grupos de captura. Si se necesita esta funcionalidad, consulte esta respuesta que intenta proporcionar una solución alternativa actualizando la función para permitir una búsqueda y reemplazo secundario dentro de cada coincidencia encontrada (a expensas de una mayor complejidad).
  3. Si ^y / o $se usa en el patrón, deben estar al principio y al final respectivamente, por ejemplo, patrones como los que (^start|end$)no son compatibles.
  4. Hay un indicador "codicioso" para especificar si la coincidencia general debe ser codiciosa o no codiciosa. a.*?b.*No se admite la combinación de coincidencia codiciosa y perezosa dentro de una sola expresión regular (p . Ej. ).

Ejemplos de uso

La función se ha utilizado para responder las siguientes preguntas de StackOverflow:

Steve Chambers
fuente
7

Puede 'hacerlo' ... pero no es muy sabio ... esto es tan atrevido como lo intentaré ... en la medida en que RegEx completo lo respalde mucho mejor usando perl o similares.

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[[:>:]]'
Eddie B
fuente
1
No, eso no funcionará. Imagine que su columna contiene 'asdfWORD_TO_REPLACE WORD_TO_REPLACE ". Su método daría como resultado' asdfREPLACEMENT REPLACEMENT" donde la respuesta correcta sería "asdfWORD_TO_REPLACE REPLACEMENT".
Ryan Shillington
1
@ Ryan ... es exactamente por eso que dije que no era muy sabio ... en el caso de uso que proporcione, esto definitivamente fallará. En resumen, es una mala idea usar una estructura 'tipo regex'. Peor aún ... si abandonas la cláusula where, todos tus valores serán NULOS ...
Eddie B
1
En realidad, Ryan en este caso es incorrecto ya que los marcadores solo encontrarán coincidencias para la palabra 'límites' de longitud cero, por lo que solo las palabras con límites antes y después de la palabra coincidirán ... Sin embargo, sigue siendo una mala idea ...
Eddie B
6

Podemos usar la condición IF en la consulta SELECT de la siguiente manera:

Supongamos que para cualquier cosa con "ABC", "ABC1", "ABC2", "ABC3", ..., queremos reemplazar con "ABC" y luego usar la condición REGEXP e IF () en la consulta SELECT, podemos lograr esto .

Sintaxis:

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

Ejemplo:

SELECT IF('ABC1' REGEXP 'ABC[0-9]$','ABC','ABC1');
usuario3796869
fuente
Hola, gracias por la sugerencia. He intentado algo similar, pero el rendimiento de mis conjuntos de datos no ha sido satisfactorio. Para conjuntos pequeños, esto puede ser viable.
Piskvor dejó el edificio el
3

Básicamente, el siguiente encuentra la primera coincidencia desde la izquierda y luego reemplaza todas las ocurrencias (probada en )

Uso:

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

Implementación:

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 ;
Nae
fuente
3

Creo que hay una manera fácil de lograr esto y está funcionando bien para mí.

Para SELECCIONAR filas usando REGEX

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

Para ACTUALIZAR filas usando 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'

Referencia de REGEXP: https://www.geeksforgeeks.org/mysql-regular-expressions-regexp/

Silambarasan RD
fuente
Gracias :) Es posible hacerlo fácilmente desde la versión 8.
Piskvor dejó el edificio el