Regex для всех 10 буквенных слов, с уникальными буквами


23

Я пытаюсь написать регулярное выражение, которое будет отображать все слова длиной 10 символов, и ни одна из букв не повторяется.

Пока у меня есть

grep --colour -Eow '(\w{10})'

Что является самой первой частью вопроса. Как бы я проверил «уникальность»? Я действительно понятия не имею, кроме этого мне нужно использовать обратные ссылки.


1
Это должно быть сделано с помощью регулярного выражения?
Хауке Лагинг

Я практикую регулярные выражения, поэтому желательно да :)
Дилан Мееус

3
Я не верю, что вы можете сделать это с помощью регулярного выражения в компьютерном стиле: для того, что вам нужно, требуется «память» о том, что представляют собой предшествующие совпадающие символы, а в регулярных выражениях этого просто нет. Тем не менее, вы можете сделать это с помощью обратных ссылок и вещей, не связанных с регулярными выражениями, которые может выполнять сопоставление в стиле PCRE.
Брюс Эдигер

3
@BruceEdiger, если в языке (26) и букв в строке (10) есть конечное число символов, это вполне возможно сделать. Просто много состояний, но ничего такого, что не сделало бы это не обычным языком.

1
Вы имеете в виду "все английские слова ..."? Вы хотите включить те, которые написаны с дефисами и апострофами или нет (в законе, не)? Вы хотите включить такие слова, как кафе, наивный, фасад?
hippietrail

Ответы:


41
grep -Eow '\w{10}' | grep -v '\(.\).*\1'

исключает слова, которые имеют два одинаковых символа.

grep -Eow '\w{10}' | grep -v '\(.\)\1'

исключая те, которые имеют повторяющиеся символы.

POSIXly:

tr -cs '[:alnum:]_' '[\n*]' |
   grep -xE '.{10}' |
   grep -v '\(.\).*\1'

trпомещает слова в отдельную строку, преобразовывая все sсимволы, не входящие в состав слова ( cпропуска букв, цифр и подчеркивания), в символ новой строки.

Или с одним grep:

tr -cs '[:alnum:]_' '[\n*]' |
   grep -ve '^.\{0,9\}$' -e '.\{11\}' -e '\(.\).*\1'

(за исключением строк длиной менее 10 и более 10 символов, а также строк, в которых символы появляются как минимум дважды).

grepТолько с одним (GNU grep с поддержкой PCRE или pcregrep):

grep -Po '\b(?:(\w)(?!\w*\1)){10}\b'

Таким образом, за границей слова ( \b) следует последовательность из 10 символов слова (при условии, что за каждым не следует последовательность символов слова и сами по себе, используя оператор PCRE с отрицательным прогнозом (?!...)).

Нам повезло, что это работает здесь, так как не многие движки регулярных выражений работают с обратными ссылками внутри повторяющихся частей.

Обратите внимание, что (по крайней мере, с моей версией GNU grep)

grep -Pow '(?:(\w)(?!\w*\1)){10}'

Не работает, но

grep -Pow '(?:(\w)(?!\w*\2)){10}'

делает (как echo aa | grep -Pw '(.)\2'), что звучит как ошибка.

Вы можете хотеть:

grep -Po '(*UCP)\b(?:(\w)(?!\w*\1)){10}\b'

если вы хотите \wили \bсчитаете любую букву компонентом слова, а не только буквы ASCII в не-ASCII локалях.

Другая альтернатива:

grep -Po '\b(?!\w*(\w)\w*\1)\w{10}\b'

Это граница слова (за которой не следует последовательность символов слова, один из которых повторяется), за которой следуют 10 символов слова.

Вещи, которые могут быть в глубине души:

  • Сравнение чувствительно к регистру, так Babylonishчто, например, будет совпадать, так как все символы различны, даже если есть два Bs, один нижний и один верхний регистр (используйте -iдля изменения этого).
  • для -w, \wи \b, слово это буква (ASCII те только для GNU grep сейчас , то [:alpha:]класс символов в вашей местности при использовании -Pи (*UCP)), десятичных цифр или подчеркивания .
  • это означает, что c'est(два слова согласно французскому определению слова) или it's(одно слово согласно некоторым английским определениям слова) или rendez-vous(одно слово согласно французскому определению слова) не считаются одним словом.
  • Даже при этом (*UCP)символы объединения Юникод не рассматриваются как компоненты слова, поэтому téléphone( $'t\u00e9le\u0301phone') считается как 10 символов, один из которых не альфа. défavorisé( $'d\u00e9favorise\u0301') будет совпадать, даже если у него два, éпотому что это 10 разных буквенных символов, за которыми следует сочетание острого акцента (не альфа, поэтому между словом eи его акцентом есть граница слова ).

1
Потрясающе. \wне совпадает, -хотя
Грэм,

@Stephane Можете ли вы опубликовать краткое объяснение двух последних выражений.
MKC

Иногда кажется, что обходные пути - это решение всех вещей, которые раньше были невозможны с RE.
Бармар

1
@ Barmar они все еще невозможны с регулярными выражениями. «Регулярное выражение» - это математическая конструкция, которая явно допускает только определенные конструкции, а именно: литеральные символы, классы символов и операторы '|', '(...)', '?', '+' И '*'. Любое так называемое «регулярное выражение», в котором используется оператор, не являющийся одним из вышеперечисленных, на самом деле не является регулярным выражением.
Жюль

1
@Jules Это unix.stackexchange.com, а не math.stackexchange.com. В этом контексте математические RE не имеют значения, мы говорим о типах RE, которые вы используете с grep, PCRE и т. Д.
Barmar

12

Хорошо ... вот неуклюжий способ для строки из пяти символов:

grep -P '^(.)(?!\1)(.)(?!\1|\2)(.)(?!\1|\2|\3)(.)(?!\1|\2|\3|\4).$'

Поскольку вы не можете поместить обратную ссылку в класс символов (например [^\1|\2]), вы должны использовать отрицательный прогноз - (?!foo). Это функция PCRE, поэтому вам нужен -Pпереключатель.

Конечно, шаблон для строки из 10 символов будет намного длиннее, но есть более короткий метод, использующий переменную длину, совпадающую с любым ('. *') В запросе:

grep -P '^(.)(?!.*\1)(.)(?!.*\2)(.)(?!.*\3)(.)(?!.*\4)(.)(?!.*\5).$'

Прочитав поучительный ответ Стефана Чазела, я понял, что есть аналогичный простой шаблон для использования с помощью -vпереключателя grep :

    (.).*\1

Так как проверка выполняется по одному символу за раз, будет видно, сопровождается ли после любого заданного символа ноль или более символов ( .*), а затем совпадение для обратной ссылки. -vинвертирует, печатая только то, что не соответствует этому шаблону. Это делает обратные ссылки более полезными, так как они не могут быть отменены классом символов, и значительно:

grep -v '\(.\).*\1'

будет работать для идентификации строки любой длины с уникальными символами, тогда как:

grep -P '(.)(?!.*\1)'

не будет, так как он будет сопоставлять любой суффикс с уникальными символами (например, abcabcсовпадения из-за abcконца и aaaaиз-за aконца - отсюда и любая строка). Это осложнение вызвано тем, что обходные пути имеют нулевую ширину (они ничего не потребляют).


Отлично сработано! Это будет работать только в сочетании с тем в Q.
Грэм,

1
Я полагаю, что вы можете упростить первый, если ваш движок регулярных выражений допускает отрицательный (.)(?!.*\1)(.)(?!.*\2)(.)(?!.*\3)(.)(?!\4).
прогноз

@ChristopherCreutzig: Абсолютно, хороший звонок. Я добавил это в.
Златовласка

6

Если вам не нужно делать все это в регулярном выражении, я бы сделал это в два этапа: сначала сопоставьте все 10-буквенные слова, затем отфильтруйте их по уникальности. Самый короткий способ, которым я знаю, как это сделать, в Perl:

perl -nle 'MATCH:while(/\W(\w{10})\W/g){
             undef %seen;
             for(split//,$1){next MATCH if ++$seen{$_} > 1}
             print
           }' your_file

Обратите внимание на дополнительные \Wпривязки, чтобы гарантировать, что сопоставляются только слова длиной ровно 10 символов.


Спасибо, но я бы хотел, чтобы он был регулярным выражением :)
Dylan Meeus

4

Другие предположили, что это невозможно без различных расширений некоторых систем регулярных выражений, которые на самом деле не являются регулярными. Тем не менее, поскольку язык, который вы хотите использовать, является конечным, он явно регулярный. Для 3 букв из 4-буквенного алфавита это будет легко:

(abc|abd|acb|acd|bac|bad|bcd|bdc|cab|cad|cbd|cdb|dab|dac|dbc|dcb)

Очевидно, что это выходит из-под контроля в спешке с большим количеством букв и больших алфавитов. :-)


Я должен был поддержать это, потому что это на самом деле ответ, который будет работать. Хотя на самом деле это может быть наименее эффективным способом написания регулярных выражений из всех, что когда-либо были: P
Dylan Meeus

4

Опция --perl-regexp(краткая -P) GNU grepиспользует более мощные регулярные выражения, которые включают в себя шаблоны заблаговременного просмотра. Следующий шаблон ищет каждую букву, которую эта буква не встречает в оставшейся части слова:

grep -Pow '((\w)(?!\w*\g{-1})){10}'

Однако поведение во время выполнения довольно плохое, потому что \w*может иметь почти бесконечную длину. Это может быть ограничено \w{,8}, но это также проверяет превышение предела в 10 букв. Поэтому следующий шаблон сначала проверяет правильную длину слова:

grep -Pow '(?=\w{10}\b)((\w)(?!\w*\g{-1})){10}'

В качестве тестового файла я использовал большой файл размером 500 МБ:

  • Первый шаблон: ≈ 43 с
  • Последний рисунок: ≈ 15 с

Обновить:

Я не смог найти существенного изменения в поведении во время выполнения для не жадного оператора ( \w*?) или притяжательного оператора ( (...){10}+). Немного быстрее кажется замена опции -w:

grep -Po '\b(?=\w{10}\b)((\w)(?!\w*\g{-1})){10}\b'

Обновление grep с версии 2.13 до 2.18 было намного более эффективным. Тестовый файл занял ≈ 6 с.


Производительность во многом зависит от характера данных. Выполняя тесты на моем, я обнаружил, что использование некожадных операторов ( \w{,8}?) помогло для некоторого типа ввода (хотя и не очень значительно). Хорошее использование, \g{-1}чтобы обойти ошибку GNU grep.
Стефан Шазелас

@StephaneChazelas: Спасибо за отзыв. Я также пробовал не жадные и притяжательные операторы и не обнаружил значительных изменений в поведении во время выполнения (версия 2.13). Версия 2.18 намного быстрее, и я мог видеть хотя бы небольшое улучшение. Ошибка GNU grep присутствует в обеих версиях. В любом случае я предпочитаю относительную ссылку \g{-1}, потому что это делает шаблон более независимым от местоположения. В этой форме это может использоваться как часть большего образца.
Хайко Обердик,

0

Решение Perl:

perl -lne 'print if (!/(.)(?=$1)/g && /^\w{10}$/)' file

но это не работает с

perl -lne 'print if (!/(.)(?=\1)/g && /^\w{10}$/)' file

или

perl -lne 'print if ( /(.)(?!$1)/g && /^\w{10}$/)' file

протестировано с Perl v5.14.2 и v5.18.2


1-й и 3-й ничего не делают, 2-й выводит любую строку из 10 или более символов, не более 2-х последовательных пробелов. pastebin.com/eEDcy02D
manatwork

это, вероятно, версия Perl. протестировано с v5.14.2 и v5.18.2

Я попробовал их с v5.14.1 на Linux и v5.14.2 на Cygwin. Оба вели себя как в образце пастбина, который я связал ранее.
manatwork

у меня первая строка работает с отмеченными версиями perl. два последних должны работать, потому что они одинаковы, но не работают. Perlre часто отмечают, что некоторые жадные выражения очень экспериментальны.

Проверено с вашими последними обновлениями. Только 2-й выводит правильно. (Однако слово должно быть одним в строке, в то время как вопрос касается сопоставления слов, а не целых строк.)
manatwork
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.