Como substituir uma expressão regular no MySQL?


516

Eu tenho uma tabela com ~ 500k linhas; A coluna varchar (255) UTF8 filenamecontém um nome de arquivo;

Estou tentando remover vários caracteres estranhos do nome do arquivo - pensei em usar uma classe de caracteres: [^a-zA-Z0-9()_ .\-]

Agora, existe uma função no MySQL que permite substituir por uma expressão regular ? Estou procurando uma funcionalidade semelhante à função REPLACE () - exemplo simplificado a seguir:

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

Output: "stackoverflow"

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

Output: "-tackover-low"

Eu sei sobre o REGEXP / RLIKE , mas eles apenas verificam se há uma correspondência, não qual é a correspondência.

(Eu poderia fazer um " SELECT pkey_id,filename FROM foo WHERE filename RLIKE '[^a-zA-Z0-9()_ .\-]'" a partir de um script PHP, fazer um preg_replacee depois " UPDATE foo ... WHERE pkey_id=...", mas isso parece um hack lento e feio de último recurso)


8
É uma solicitação de recurso desde 2007: bugs.mysql.com/bug.php?id=27389 . Se você realmente deseja esse recurso, faça login e clique no botão "Afeta-me". Espero que ele receba votos suficientes.
— TMS

4
@ Tomas: Eu fiz isso ... em 2009, quando eu estava procurando por isso. Como não houve progresso nisto - aparentemente não é uma característica tão importante. (entre Postgres tem: stackoverflow.com/questions/11722995/… )
— Piskvor saiu do prédio 9/14

1
Relacionado, mais simples, versão desta pergunta: stackoverflow.com/questions/6942973/...
— Kzqai

2
Eu criei regexp_split(function + procedure) & regexp_replace, que são implementados com o REGEXPoperador. Para pesquisas simples, ele fará o truque. Você pode encontrá-lo aqui - então, este é o caminho com o código armazenado do MySQL, sem UDF. Se você encontrar alguns erros, que não são cobertos por limitações conhecidas, fique à vontade para abrir o problema.
— Alma Do

1
Encontrei esta biblioteca em outro thread do SO: github.com/mysqludf/lib_mysqludf_preg funciona perfeitamente.
— Kyle

Respostas:


78

Com o MySQL 8.0+, você pode usar a REGEXP_REPLACEfunção nativamente .

12.5.2 Expressões regulares :

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

Substitui ocorrências na cadeia expr que correspondem à expressão regular especificada pelo padrão pat pela cadeia de substituição repl e retorna a cadeia resultante. Se expr , pat ou repl for NULL, o valor de retorno é NULL.

e suporte à expressão regular :

Anteriormente, o MySQL usava a biblioteca de expressões regulares Henry Spencer para suportar operadores de expressões regulares ( REGEXP, RLIKE).

O suporte à expressão regular foi reimplementado usando o International Components for Unicode (ICU), que fornece suporte completo a Unicode e é seguro para multibytes. A REGEXP_LIKE()função executa correspondência de expressão regular da maneira dos operadores REGEXPe RLIKE, que agora são sinônimos para essa função. Além disso, a REGEXP_INSTR(), REGEXP_REPLACE()e REGEXP_SUBSTR() funções estão disponíveis para encontrar posições de jogo e realizar substring substituição e extração, respectivamente.

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

DBFiddle Demo


147

MySQL 8.0 ou superior :

Você pode usar a REGEXP_REPLACEfunção nativa .

Versões mais antigas:

Você pode usar uma função definida pelo usuário ( UDF ) como mysql-udf-regexp .


3
REGEXP_REPLACE como uma função definida pelo usuário? Parece promissor, vai olhar para ele. Obrigado!
— Piskvor saiu do prédio

15
Infelizmente, o mysql-udf-regexp não parece ter suporte para caracteres multibyte. regexp_replace ('äöõü', 'ä', '') retorna uma cadeia numérica longa em vez de texto real.
— lkraav

3
O próprio MySQL não suporta caracteres de vários bytes com seus recursos RegEx.
— 22413 Brad

4
Usuários do Windows: a Biblioteca UDF vinculada aqui não parece ter um bom suporte para o Windows. O método de instalação do Windows descrito não funcionou bem para mim.
— Jonathan

2
@lkraav, você deve experimentar a biblioteca lib_mysqludf_preg abaixo, pois ela funciona muito bem. Esta versão detalhada, pois retorna um blob por padrão e não sei se você tem um conjunto de caracteres de vários bytes como padrão: selecione converter (TR como caractere) COLLATE utf8_unicode_ci from (selecione preg_replace ('/ ä /', '', '' öõüä) R) T
— gillyspy

124

Use MariaDB em vez disso. Tem uma função

REGEXP_REPLACE(col, regexp, replace)

Vejo documentos do MariaDB e aprimoramentos de expressão regular do PCRE

Observe que você também pode usar o agrupamento regexp (achei muito útil):

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

retorna

over - stack - flow

12
Isto é do mariadb 10
— Nick

6
Para a próxima vez que precisar, aqui está a sintaxe para alterar uma coluna inteira: UPDATE table SET Name = REGEXP_REPLACE(Name, "-2$", "\\1")Isso remove -2 de abcxyz-2 de uma coluna inteira de uma só vez.
— 1937 Josiah

27
Alterar uma plataforma inteira dificilmente é uma solução realista.
— David Baucum

3
@DavidBaucum O MariaDB é um substituto para o MySQL. Portanto, não é uma "mudança de plataforma", mas sim como escolher uma companhia aérea diferente para a mesma viagem
— Benvorth 30/11


113

Meu método de força bruta para fazer isso funcionar era apenas:

  1. Despejar a mesa - mysqldump -u user -p database table > dump.sql
  2. Encontre e substitua alguns padrões - find /path/to/dump.sql -type f -exec sed -i 's/old_string/new_string/g' {} \; , Obviamente, existem outras expressões perl regeulares que você também pode executar no arquivo.
  3. Importar a tabela - mysqlimport -u user -p database table < dump.sql

Se você deseja garantir que a sequência não esteja em outro lugar no seu conjunto de dados, execute algumas expressões regulares para garantir que elas ocorram em um ambiente semelhante. Também não é tão difícil criar um backup antes de executar uma substituição, caso você destrua acidentalmente algo que perde a profundidade das informações.


33
Ok, isso deve funcionar também; Não considerei uma substituição offline. Bom pensamento pronto para uso lá!
— Piskvor saiu do prédio

10
Parece-me estranho que você usaria encontrar como esse, eu iria encurtar a comando para sed -i 's / old_string / new_string / g' /path/to/dump.sql
— speshak

36
Muito arriscado e pouco prático com grandes conjuntos de dados ou com integridade referencial em vigor: para remover os dados e inseri-los novamente, será necessário desativar a integridade referencial, deixando na prática também o banco de dados.
— Raul Luna

5
Tendo usado esse método no passado, concordo com Raul, isso é muito arriscado. Você também precisa ter certeza absoluta de que sua string não está no seu conjunto de dados.
— eggmatters

1
Anos atrasado para a resposta @speshak, mas a razão pela qual escolhi acessar o arquivo dessa maneira foi porque originalmente estava muito nervoso pelos mesmos motivos mencionados acima. Na época, parecia que separa a parte "encontrar o arquivo" do "substituir" peças tornar o código mais fácil de ler antes de eu submeteu
— Ryan Ward

42

resolvemos esse problema sem usar regex. Essa consulta substitui apenas a string de correspondência exata.

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

Exemplo:

emp_id employee_firstname

1 jay

2 jay ajay

3 jay

Depois de executar o resultado da consulta:

emp_id employee_firstname

1 abc

2 abc ajay

3 abc


@yellowmelon para que servem os dois pares de aspas duplas?
— Codecowboy 4/03/16

5
Ele está preenchendo o nome do funcionário com espaços antes e depois. Isso permite que ele procure e substitua por (espaço) nome do empregador (espaço), o que evita capturar o nome do empregador "jay" se faz parte de uma string maior "ajay". Então ele apara os espaços quando terminar.
— Slam

42

Recentemente, escrevi uma função MySQL para substituir seqüências de caracteres usando expressões regulares. Você pode encontrar minha postagem no seguinte local:

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

Aqui está o código da função:

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 ;

Execução de exemplo:

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

25
Vou apenas reforçar o ponto acima: esta função substitui caracteres que correspondem a uma expressão de caractere único. Ele diz acima que é usado "para repalcar seqüências de caracteres usando expressões regulares", e isso pode ser um pouco enganador. Ele faz o seu trabalho, mas não é o trabalho que está sendo solicitado. (Não uma reclamação - é apenas para salvar levando as pessoas para o caminho errado)
— Jason

2
Seria mais útil incluir código na sua resposta, em vez de postar um link nu.
— Phobie

2
Bom - mas infelizmente não lida com referências como select regex_replace('.*(abc).*','\1','noabcde')(retorna 'noabcde', não 'abc').
— Izzy

@phobie alguém fez isso nesta resposta - apenas como uma referência, no caso o link morre;)
— Izzy

Modifiquei esse método para tentar abordar algumas das limitações mencionadas acima e mais. Por favor, veja esta resposta .
— 9788 Steve Chambers


13

ATUALIZAÇÃO 2: Um conjunto útil de funções regex incluindo REGEXP_REPLACE foi fornecido no MySQL 8.0. Isso torna desnecessária a leitura, a menos que você seja obrigado a usar uma versão anterior.


ATUALIZAÇÃO 1: transformou isso em uma postagem de blog: http://stevettt.blogspot.co.uk/2018/02/a-mysql-regular-expression-replace.html


O seguinte expande a função fornecida por Rasika Godawatte, mas percorre todas as substrings necessárias, em vez de apenas testar caracteres únicos:

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

Demo

Rextester Demo

Limitações

  1. É claro que esse método vai demorar um pouco quando a sequência de assunto for grande. Atualizar: agora adicionamos parâmetros de comprimento mínimo e máximo de correspondência para maior eficiência quando estes são conhecidos (zero = desconhecido / ilimitado).
  2. Ele não irá permitir a substituição de referências anteriores (por exemplo \1, \2 etc.) para substituir grupos de captura. Se essa funcionalidade for necessária, consulte esta resposta que tenta fornecer uma solução alternativa, atualizando a função para permitir uma localização secundária e substituição dentro de cada correspondência encontrada (às custas de maior complexidade).
  3. Se ^e / ou $for usado no padrão, eles deverão estar no início e no final, respectivamente - por exemplo, padrões como os que (^start|end$)não são suportados.
  4. Há um sinalizador "ganancioso" para especificar se a correspondência geral deve ser gananciosa ou não gananciosa. A combinação de correspondências gananciosas e preguiçosas em uma única expressão regular (por exemplo a.*?b.*) não é suportada.

Exemplos de uso

A função foi usada para responder às seguintes perguntas sobre o StackOverflow:


7

Você 'pode' fazer isso ... mas não é muito sábio ... isso é tão ousado quanto tentarei ... na medida em que o RegEx completo ofereça suporte muito melhor ao usar perl ou algo semelhante.

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
Não, isso não vai funcionar. Imagine que sua coluna contém 'asdfWORD_TO_REPLACE WORD_TO_REPLACE ". Seu método resultaria em' asdfREPLACEMENT REPLACEMENT", onde a resposta correta seria "asdfWORD_TO_REPLACE REPLACEMENT".
— Ryan Shillington

1
@ Ryan ... foi exatamente por isso que afirmei que não era muito sábio ... no caso de uso que você fornecer, isso definitivamente falharia. Em resumo, é uma má idéia usar a estrutura 'semelhante a regex'. Ainda pior ... se você deixar cair a cláusula WHERE todos os seus valores serão NULL ...
— Eddie B

1
Na verdade, Ryan, neste caso, você está incorreto, pois os marcadores encontrarão apenas correspondências para a palavra de comprimento zero 'limites'; portanto, apenas as palavras com limites antes e depois da palavra corresponderão ... Ainda é uma má idéia ...
— Eddie B

6

Podemos usar a condição SE na consulta SELECT como abaixo:

Suponha que, para qualquer coisa com "ABC", "ABC1", "ABC2", "ABC3", ..., desejemos substituir por "ABC" e, em seguida, usando a condição REGEXP e IF () na consulta SELECT, podemos conseguir isso .

Sintaxe:

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

Exemplo:

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

Olá, obrigado pela sugestão. Eu tenho tentado algo semelhante, mas o desempenho em meus conjuntos de dados foi insatisfatório. Para conjuntos pequenos, isso pode ser viável.
— Piskvor saiu do prédio

3

O abaixo encontra basicamente a primeira correspondência da esquerda e substitui todas as ocorrências dela (testadas em )

Uso:

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

Implementação:

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

Eu acho que existe uma maneira fácil de conseguir isso e está funcionando bem para mim.

Para selecionar linhas usando REGEX

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

Para atualizar linhas 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'

Referência do REGEXP: https://www.geeksforgeeks.org/mysql-regular-expressions-regexp/


Obrigado :) É possível fazer isso facilmente desde a versão 8.
— Piskvor saiu do prédio 31/07
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.