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)

Piskvor saiu do prédio
fonte
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

Lukasz Szozda
fonte
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 .

Jeremy Stein
fonte
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
Benvorth
fonte
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
3
@Benvorth O MySQL 8.0 também suporta .
Lukasz Szozda
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.

Ryan Ward
fonte
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

Jay Patel
fonte
@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 ._,');
rasika godawatte
fonte
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:

Steve Chambers
fonte
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[[:>:]]'
Eddie B
fonte
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');
user3796869
fonte
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 ;
Nae
fonte
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/

Silambarasan RD
fonte
Obrigado :) É possível fazer isso facilmente desde a versão 8.
Piskvor saiu do prédio 31/07