Comment remplacer une expression régulière dans MySQL?

516

J'ai une table avec environ 500 000 lignes; varchar (255) La colonne UTF8 filenamecontient un nom de fichier;

J'essaie de retirer divers caractères étranges du nom de fichier - je pensais utiliser une classe de caractères: [^a-zA-Z0-9()_ .\-]

Maintenant, y a-t-il une fonction dans MySQL qui vous permet de remplacer via une expression régulière ? Je recherche une fonctionnalité similaire à la fonction REPLACE () - un exemple simplifié suit:

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

Output: "stackoverflow"

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

Output: "-tackover-low"

Je connais REGEXP / RLIKE , mais ceux-ci vérifient seulement s'il y a une correspondance, pas ce que la correspondance est.

(Je pourrais faire un " SELECT pkey_id,filename FROM foo WHERE filename RLIKE '[^a-zA-Z0-9()_ .\-]'" à partir d'un script PHP, faire un preg_replacepuis " UPDATE foo ... WHERE pkey_id=...", mais cela ressemble à un hack lent et laid de dernier recours)

Piskvor a quitté le bâtiment
la source
8
C'est une demande de fonctionnalité depuis 2007: bugs.mysql.com/bug.php?id=27389 . Si vous voulez vraiment cette fonctionnalité, connectez-vous et cliquez sur le bouton "Affecte-moi". Espérons qu'il obtiendra suffisamment de votes.
TMS
4
@Tomas: C'est ce que j'ai fait ... en 2009, quand je cherchais ça. Puisqu'il n'y a eu aucun progrès à ce sujet - apparemment, ce n'est pas une caractéristique si importante. (btw Postgres l'a: stackoverflow.com/questions/11722995/… )
Piskvor a quitté le bâtiment
1
Version connexe, plus simple, de cette question: stackoverflow.com/questions/6942973/…
Kzqai
2
J'ai créé regexp_split(fonction + procédure) & regexp_replace, qui sont implémentés avec l' REGEXPopérateur. Pour les recherches simples, il fera l'affaire. Vous pouvez le trouver ici - c'est donc le cas avec le code stocké MySQL, sans UDF. Si vous trouvez des bogues qui ne sont pas couverts par les limitations connues, n'hésitez pas à ouvrir le problème.
Alma Do
1
La bibliothèque trouvée à partir d'un autre thread SO: github.com/mysqludf/lib_mysqludf_preg fonctionne parfaitement.
Kyle

Réponses:

78

Avec MySQL 8.0+, vous pouvez utiliser la REGEXP_REPLACEfonction native .

12.5.2 Expressions régulières :

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

Remplace les occurrences de la chaîne expr qui correspondent à l'expression régulière spécifiée par le motif pat avec la chaîne de remplacement repl et renvoie la chaîne résultante. Si expr , pat ou repl est NULL, la valeur de retour est NULL.

et prise en charge des expressions régulières :

Auparavant, MySQL utilisait la bibliothèque d'expressions régulières Henry Spencer pour prendre en charge les opérateurs d'expressions régulières ( REGEXP, RLIKE).

La prise en charge des expressions régulières a été réimplémentée à l'aide de International Components for Unicode (ICU), qui fournit une prise en charge complète d'Unicode et est multi-octets. La REGEXP_LIKE()fonction effectue une correspondance d'expressions régulières à la manière des opérateurs REGEXPet RLIKE, qui sont désormais des synonymes de cette fonction. En outre, le REGEXP_INSTR(), REGEXP_REPLACE()et REGEXP_SUBSTR() les fonctions sont disponibles pour trouver des positions de match et d' effectuer la substitution et l' extraction sous - chaîne, respectivement.

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

DBFiddle Demo

Lukasz Szozda
la source
147

MySQL 8.0+ :

Vous pouvez utiliser le natif REGEXP_REPLACE fonction .

Versions plus anciennes:

Vous pouvez utiliser une fonction définie par l'utilisateur ( UDF ) comme mysql-udf-regexp .

Jeremy Stein
la source
3
REGEXP_REPLACE en tant que fonction définie par l'utilisateur? Semble prometteur, se penchera sur elle. Merci!
Piskvor a quitté le bâtiment le
15
Malheureusement, mysql-udf-regexp ne semble pas prendre en charge les caractères multi-octets. regexp_replace ('äöõü', 'ä', '') renvoie une longue chaîne numérique au lieu du texte réel.
lkraav
3
MySQL lui-même ne prend pas en charge les caractères multi-octets avec ses fonctionnalités RegEx.
Brad
4
Utilisateurs Windows: la bibliothèque UDF liée ici ne semble pas avoir un bon support Windows. La méthode d'installation de Windows décrite ne fonctionnait pas bien pour moi.
Jonathan
2
@lkraav vous devriez essayer la bibliothèque lib_mysqludf_preg ci-dessous car elle fonctionne très bien. C'est la version détaillée car elle retourne un blob par défaut et je ne sais pas si vous avez un jeu de caractères multi-octets par défaut: sélectionnez cast (TR comme char) COLLATE utf8_unicode_ci dans (sélectionnez preg_replace ('/ ä /', '', 'öõüä') R) T
gillyspy
124

Utilisez plutôt MariaDB. Il a une fonction

REGEXP_REPLACE(col, regexp, replace)

Voir les documents MariaDB et les améliorations des expressions régulières PCRE

Notez que vous pouvez également utiliser le regroupement d'expressions rationnelles (j'ai trouvé cela très utile):

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

Retour

over - stack - flow
Benvorth
la source
12
cela vient de mariadb 10
Nick
6
Pour la prochaine fois que j'en ai besoin, voici la syntaxe pour changer une colonne entière: UPDATE table SET Name = REGEXP_REPLACE(Name, "-2$", "\\1")Cela supprime -2 de abcxyz-2 d'une colonne entière à la fois.
Josiah
27
Changer une plateforme entière n'est pas une solution réaliste.
David Baucum
3
@DavidBaucum MariaDB est un remplacement direct pour MySQL. Ce n'est donc pas un "changement de plate-forme" mais plutôt un choix de compagnie aérienne différente pour le même voyage
Benvorth
3
@Benvorth MySQL 8.0 le prend également en charge .
Lukasz Szozda
113

Ma méthode de force brute pour que cela fonctionne était simplement:

  1. Vider la table - mysqldump -u user -p database table > dump.sql
  2. Trouvez et remplacez quelques modèles - find /path/to/dump.sql -type f -exec sed -i 's/old_string/new_string/g' {} \;, Il y a évidemment d'autres expressions regeular perl que vous pouvez également effectuer sur le fichier.
  3. Importez la table - mysqlimport -u user -p database table < dump.sql

Si vous voulez vous assurer que la chaîne n'est pas ailleurs dans votre ensemble de données, exécutez quelques expressions régulières pour vous assurer qu'elles se produisent toutes dans un environnement similaire. Il n'est également pas si difficile de créer une sauvegarde avant d'exécuter un remplacement, au cas où vous détruiriez accidentellement quelque chose qui perd de la profondeur des informations.

Ryan Ward
la source
33
D'accord, cela devrait aussi fonctionner; Je n'ai pas envisagé de remplacer hors ligne. Belle pensée prête à l'emploi là-bas!
Piskvor a quitté le bâtiment le
10
Cela me semble étrange que vous utilisiez find comme ça, je raccourcirais la commande à sed -i / old_string / new_string / g '/path/to/dump.sql
speshak
36
Très risqué et peu pratique avec des ensembles de données volumineux ou avec l'intégrité référentielle en place: pour supprimer les données, puis les réinsérer, vous devrez désactiver l'intégrité référentielle, laissant en pratique votre base de données également désactivée.
Raul Luna
5
Ayant utilisé cette méthode dans le passé, je suis d'accord avec Raul, c'est très risqué. Vous devez également être absolument certain que votre chaîne n'est pas ailleurs dans votre ensemble de données.
eggmatters
1
Des années de retard à la réponse @speshak, mais la raison pour laquelle j'ai choisi d'accéder au fichier comme celui-ci était parce que j'étais à l'origine très nerveux pour les mêmes raisons que celles mentionnées ci-dessus. À l'époque, il semblait que séparer la partie "trouver le fichier" de la partie "remplacer" rendrait le code plus facile à lire avant de le soumettre
Ryan Ward
42

nous résolvons ce problème sans utiliser l'expression régulière. Cette requête remplace uniquement la chaîne de correspondance exacte.

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

Exemple:

emp_id employee_firstname

1 geai

2 jay ajay

3 geais

Après avoir exécuté le résultat de la requête:

emp_id employee_firstname

1 abc

2 abc ajay

3 abc

Jay Patel
la source
@yellowmelon à quoi servent les deux paires de guillemets doubles?
codecowboy
5
Il remplit l'employé avec des espaces avant et après. Cela lui permet de rechercher-remplacer (espace) employeename (espace), ce qui évite d'attraper le employeename "jay" s'il fait partie d'une chaîne plus grande "ajay". Ensuite, il coupe les espaces une fois terminé.
Slam
42

J'ai récemment écrit une fonction MySQL pour remplacer les chaînes à l'aide d'expressions régulières. Vous pouvez trouver mon message à l'emplacement suivant:

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

Voici le code de fonction:

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 ;

Exemple d'exécution:

mysql> select regex_replace('[^a-zA-Z0-9\-]','','2my test3_text-to. check \\ my- sql (regular) ,expressions ._,');
rasika godawatte
la source
25
Je vais juste renforcer le point ci-dessus: cette fonction remplace les caractères qui correspondent à une expression à un seul caractère. Il dit ci-dessus qu'il est utilisé "pour répéter les chaînes en utilisant des expressions régulières", et cela peut être un peu trompeur. Il fait son travail, mais ce n'est pas le travail demandé. (Pas une plainte - c'est juste pour sauver les gens qui mènent sur le mauvais chemin)
Jason
2
Il serait plus utile d'inclure du code dans votre réponse au lieu de publier un lien nu.
phobie
2
Bien - mais ne traite malheureusement pas des références comme select regex_replace('.*(abc).*','\1','noabcde')(renvoie 'noabcde', pas 'abc').
Izzy
@phobie quelqu'un d'autre a fait cela dans cette réponse - juste comme référence au cas où le lien mourrait;)
Izzy
J'ai modifié cette méthode pour tenter de résoudre certaines des limitations mentionnées ci-dessus et plus encore. Veuillez voir cette réponse .
Steve Chambers
13

MISE À JOUR 2: Un ensemble utile de fonctions d' expression régulière, y compris REGEXP_REPLACE, a maintenant été fourni dans MySQL 8.0. Cela rend la lecture inutile sauf si vous êtes contraint d'utiliser une version antérieure.


MISE À JOUR 1: Je l'ai maintenant transformé en article de blog: http://stevettt.blogspot.co.uk/2018/02/a-mysql-regular-expression-replace.html


Ce qui suit développe la fonction fournie par Rasika Godawatte mais parcourt toutes les sous-chaînes nécessaires plutôt que de simplement tester des caractères uniques:

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

Démo

Démo Rextester

Limites

  1. Cette méthode va bien sûr prendre un certain temps lorsque la chaîne du sujet est grande. Mise à jour: ont maintenant ajouté des paramètres de longueur de correspondance minimale et maximale pour une efficacité améliorée lorsqu'ils sont connus (zéro = inconnu / illimité).
  2. Il ne permettra pas la substitution de références arrières (par exemple \1, \2 etc.) pour remplacer les groupes de capture. Si cette fonctionnalité est nécessaire, veuillez voir cette réponse qui tente de fournir une solution de contournement en mettant à jour la fonction pour permettre une recherche et un remplacement secondaires dans chaque correspondance trouvée (au détriment d'une complexité accrue).
  3. Si ^et / ou $sont utilisés dans le modèle, ils doivent être respectivement au tout début et à la toute fin - par exemple, les modèles tels que (^start|end$)ne sont pas pris en charge.
  4. Il existe un indicateur "gourmand" pour spécifier si la correspondance globale doit être gourmande ou non gourmande. La combinaison de correspondances gourmandes et paresseuses dans une seule expression régulière (par exemple a.*?b.*) n'est pas prise en charge.

Exemples d'utilisation

La fonction a été utilisée pour répondre aux questions StackOverflow suivantes:

Steve Chambers
la source
7

Vous 'pouvez' le faire ... mais ce n'est pas très sage ... c'est à peu près aussi audacieux que j'essaierai ... dans la mesure où RegEx complet prend en charge votre bien meilleure utilisation de perl ou similaire.

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
la source
1
Non, ça ne marchera pas. Imaginez que votre colonne contienne "asdfWORD_TO_REPLACE WORD_TO_REPLACE". Votre méthode aboutirait à "asdfREPLACEMENT REPLACEMENT" où la bonne réponse serait "asdfWORD_TO_REPLACE REPLACEMENT".
Ryan Shillington
1
@Ryan ... c'est exactement pourquoi j'ai déclaré que ce n'était pas très sage ... dans le cas d'utilisation que vous fournissez, cela échouerait très certainement. En bref, c'est une mauvaise idée d'utiliser une structure de type regex. Pire encore ... si vous supprimez la clause where, toutes vos valeurs seront NULL ...
Eddie B
1
En fait, Ryan dans ce cas, vous vous trompez car les marqueurs ne trouveront que des correspondances pour le mot de longueur nulle `` limites '', donc seuls les mots avec des limites avant et après le mot correspondraient ... C'est quand même une mauvaise idée ...
Eddie B
6

Nous pouvons utiliser la condition IF dans la requête SELECT comme ci-dessous:

Supposons que pour tout ce qui a "ABC", "ABC1", "ABC2", "ABC3", ..., nous voulons remplacer par "ABC" puis en utilisant la condition REGEXP et IF () dans la requête SELECT, nous pouvons y parvenir .

Syntaxe:

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

Exemple:

SELECT IF('ABC1' REGEXP 'ABC[0-9]$','ABC','ABC1');
user3796869
la source
Bonjour, merci pour la suggestion. J'ai essayé quelque chose de similaire, mais les performances de mes ensembles de données n'ont pas été satisfaisantes. Pour les petits ensembles, cela peut être viable.
Piskvor a quitté le bâtiment
3

Celui ci-dessous trouve essentiellement la première correspondance à partir de la gauche, puis en remplace toutes les occurrences (testé dans ).

Usage:

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

La mise en oeuvre:

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
la source
3

Je pense qu'il existe un moyen facile d'y parvenir et cela fonctionne bien pour moi.

Pour sélectionner des lignes à l'aide de REGEX

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

Pour METTRE À JOUR des lignes à l'aide de 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'

Référence REGEXP: https://www.geeksforgeeks.org/mysql-regular-expressions-regexp/

Silambarasan RD
la source
Merci :) Il est possible de le faire facilement depuis la version 8.
Piskvor a quitté le bâtiment le