Як замінити регулярний вираз у MySQL?


515

У мене є таблиця з ~ 500k рядками; 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

4
@Tomas: Я це зробив ... у 2009 році, коли я шукав це навколо. Оскільки на цьому досягнуто нульового прогресу - очевидно, це не така важлива особливість. (До речі Postgres є це: stackoverflow.com/questions/11722995 / ... )
Piskvor покинув будівлю

1
Пов'язані, простіше, версія цього питання: stackoverflow.com/questions/6942973 / ...
Kzqai

2
Я створив regexp_split(функція + процедура) & regexp_replace, які реалізовані разом з REGEXPоператором. Для простих пошуку, це зробить трюк. Ви можете знайти його тут, так що це шлях із збереженим кодом MySQL, без UDF. Якщо ви знайдете помилки, які не охоплені відомими обмеженнями, не соромтесь відкривати проблему.
Alma Do

1
Знайдена ця бібліотека з іншого потоку SO: github.com/mysqludf/lib_mysqludf_preg прекрасно працює.
Кайл

Відповіді:


77

З MySQL 8.0+ ви можете користуватися REGEXP_REPLACEфункцією.

12.5.2 Регулярні вирази :

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

Замінює входження в рядку висловлю , які відповідають регулярному виразу , певним шаблоном погладити з заміною рядки реплєї і повертає отриманий рядок. Якщо expr , pat або repl є NULL, повертається значення NULL.

та підтримка регулярного вираження :

Раніше MySQL використовував бібліотеку регулярних виразів Генрі Спенсера для підтримки операторів регулярних виразів ( REGEXP, RLIKE).

Підтримка регулярних виразів була повторно доповнена за допомогою International Components for Unicode (ICU), яка забезпечує повну підтримку Unicode та є багатобайтовою безпекою. REGEXP_LIKE()Функція виконує регулярні вирази в манері REGEXPі RLIKEоператорів, які в даний час є синонімами для цієї функції. Крім того, 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 як визначена користувачем функція? Виглядає перспективно, загляне в це. Дякую!
Пісквор вийшов з будівлі

15
На жаль, схоже, що mysql-udf-regexp не підтримує багатобайтові символи. regexp_replace ('äöõü', 'ä', '') повертає довгий числовий рядок замість реального тексту.
lkraav

3
Сам MySQL не підтримує багатобайтові символи зі своїми функціями RegEx.
Бред

4
Користувачі Windows: Бібліотека UDF, зв'язана тут, не має хорошої підтримки Windows. Окреслений спосіб установки вікон для мене не працював добре.
Джонатан

2
@lkraav слід спробувати бібліотеку lib_mysqludf_preg нижче, оскільки вона чудово працює. Це багатослівна версія, оскільки вона повертає крапку за замовчуванням, і я не знаю, чи є у вас багатобайтовий набір за замовчуванням: виберіть cast (TR як char) COLLATE utf8_unicode_ci (виберіть preg_replace ('/ ä /', '', 'öõüä') R) T
gillyspy

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
Нік

6
Наступного разу, коли мені це потрібно, ось синтаксис зміни цілого стовпця: UPDATE table SET Name = REGEXP_REPLACE(Name, "-2$", "\\1")Це вилучає -2 з abcxyz-2 з цілого стовпця одразу.
Йосія

27
Зміна цілої платформи навряд чи є реалістичним рішенням.
Девід Баукум

3
@DavidBaucum MariaDB - це замінна версія для MySQL. Тож це не «зміна платформи», а більше, як вибір іншої авіакомпанії для тієї ж поїздки
Бенворт


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
Гаразд, це теж має працювати; Я не вважав заміну офлайн. Приємного нестандартного мислення там!
Пісквор вийшов з будівлі

10
Мені здається дивним, що ви використовуєте такий пошук, я скорочу команду до sed -i 's / old_string / new_string / g'
/path/to/dump.sql

36
Дуже ризиковано і непрактично з великими наборами даних або з встановленою референтною цілісністю: для того, щоб видалити ці дані та знову вставити їх, вам доведеться вимкнути референтну цілісність, залишаючи на практиці і вашу базу даних.
Рауль Луна

5
Використовуючи цей метод у минулому, я збирався з Раулем, це дуже ризиковано. Ви також повинні бути абсолютно впевнені, що ваша рядок не стоїть у вашому наборі даних.
яєчники

1
Роки запізнилися на відповідь @speshak, але я вирішив отримати доступ до файлу, як це було, тому що я спочатку дуже нервував з тих же причин, що були згадані вище. У той час, як здавалося, відокремлення частини "знайти файл" від частини "замінити" полегшить читання коду до того, як я його
Райан Уорд,

42

ми вирішуємо цю проблему, не використовуючи регулярний вираз, цей запит замінить лише рядок точної відповідності.

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

Приклад:

emp_id співробітник_ім'я

1 джей

2 jay ajay

3 джей

Після виконання результату запиту:

emp_id співробітник_ім'я

1 абс

2 abc ajay

3 абс


@yellowmelon для чого дві пари подвійних лапок?
codecowboy

5
Він оббиває ім'я співробітника пробілами до і після. Це дозволяє йому шукати-замінювати (space) ім'я співробітника (space), що дозволяє уникнути лову імені співробітника "jay", якщо його частина більшої рядки "ajay". Потім він обрізає пробіли, коли закінчиться.
Слам

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
Я просто підкріплюю вищезазначене: ця функція замінює символи, які відповідають односимвольним виразом. Вище сказано, що він використовується "для повторного використання рядків за допомогою регулярних виразів", і це може бути трохи оманливим. Він робить свою роботу, але це не та робота, яку просять. (Не скарга - це просто врятувати провідних людей на неправильний шлях)
Джейсон

2
Було б корисніше насправді включити код у свою відповідь, а не публікувати відкрите посилання.
фобій

2
Добре - але, на жаль, не стосується таких посилань select regex_replace('.*(abc).*','\1','noabcde')(повертає 'noabcde', не 'abc').
Іззі

@phobie хтось інший зробив це у цій відповіді - просто як посилання на випадок, якщо посилання вмирає;)
Izzy

Я змінив цей метод, щоб спробувати вирішити деякі обмеження, згадані вище та багато іншого. Будь ласка, дивіться цю відповідь .
Стів Чемберс

14

Я радий повідомити, що оскільки було задано це запитання, зараз є задовільна відповідь! Погляньте на цей надзвичайний пакет:

https://github.com/mysqludf/lib_mysqludf_preg

Зразок SQL:

SELECT PREG_REPLACE('/(.*?)(fox)/' , 'dog' , 'the quick brown fox' ) AS demo;

Я знайшов пакет із цієї публікації в блозі як пов’язаний з цим питанням .


13

ОНОВЛЕННЯ 2: Корисний набір функцій регулярних виразів, включаючи REGEXP_REPLACE , тепер надано в MySQL 8.0. Це робить читання непотрібним, якщо ви не обмежені у використанні більш ранньої версії.


ОНОВЛЕННЯ 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

Ви можете це зробити ... але це не дуже розумно ... це приблизно так сміливо, як я спробую ... наскільки повна RegEx підтримує ваші набагато краще, використовуючи 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", де правильною відповіддю буде "ЗАМОВЛЕННЯ asdfWORD_TO_REPLACE".
Райан Шиллінгтон

1
@Ryan ... саме тому я заявив, що це було не дуже розумно ... у випадку використання ви надаєте це, безумовно, не вдасться. Коротше кажучи, погана ідея використовувати структуру, схожу на регулярні виразки. Ще гірше ... якщо ви скасуєте пункт де всі ваші значення будуть НУЛЬНІ ...
Едді Б

1
Насправді, Райан у цьому випадку ви неправі, оскільки маркери знайдуть відповідність лише слова "межі" з нульовою довжиною, тому відповідатимуть лише слова з межами до і після слова ... Але все-таки погана ідея, хоча ...
Едді Б

6

Ми можемо використовувати умову IF у запиті SELECT, як показано нижче:

Припустимо, що для чого завгодно з "ABC", "ABC1", "ABC2", "ABC3", ..., ми хочемо замінити на "ABC", використовуючи умову REGEXP та IF () у SELECT запиті, ми можемо досягти цього .

Синтаксис:

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

Привіт, дякую за пропозицію. Я намагався щось подібне, але ефективність моїх наборів даних була незадовільною. Для невеликих наборів це може бути життєздатним.
Пісквор вийшов з будинку

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
ї
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.