Regex dla wszystkich 10-literowych słów z unikalnymi literami

23

Próbuję napisać wyrażenie regularne, które wyświetli wszystkie słowa o długości 10 znaków i żadna z liter nie będzie się powtarzać.

Do tej pory mam

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

Która jest pierwszą częścią pytania. Jak miałbym przejść do sprawdzania „wyjątkowości”? Naprawdę nie mam pojęcia, poza tym muszę użyć referencji.

Dylan Meeus
źródło
1
Trzeba to zrobić za pomocą wyrażenia regularnego?
Hauke ​​Laging
Ćwiczę wyrażenia regularne, więc najlepiej tak :)
Dylan Meeus
3
Nie wierzę, że można to zrobić za pomocą wyrażenia regularnego w stylu informatycznym: to, czego chcesz, wymaga „pamięci” tego, co poprzedzają dopasowane znaki, a wyrażeń regularnych po prostu tego nie ma. To powiedziawszy, możesz to zrobić z referencjami wstecznymi i rzeczami, które nie są wyrażeniami regularnymi, które może zrobić dopasowanie w stylu PCRE.
Bruce Ediger
3
@BruceEdiger, o ile istnieje skończona liczba znaków w języku (26) i liter w ciągu (10), jest to całkiem możliwe. To tylko wiele stanów, ale nic, co nie uczyniłoby go zwykłym językiem.
1
Masz na myśli „Wszystkie angielskie słowa ...”? Czy masz na myśli te, które zostały zapisane łącznikami i apostrofami, czy nie (teściowie, nie?) Czy masz na myśli takie słowa, jak kawiarnia, naiwna, fasada?
hippietrail

Odpowiedzi:

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

nie obejmuje słów, które mają dwa identyczne znaki.

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

nie obejmuje tych, które mają powtarzające się postacie.

POSIXly:

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

trumieszcza słowa we własnej linii, konwertując dowolną srówność znaków niebędących wyrazami ( cuzupełnienie znaków alfanumerycznych i podkreślników) na znak nowej linii.

Lub z jednym grep:

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

(z wyłączeniem wierszy zawierających mniej niż 10 i więcej niż 10 znaków oraz wiersze o znaku pojawiającym się co najmniej dwa razy).

grepTylko jeden (GNU grep z obsługą PCRE lub pcregrep):

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

Oznacza to, że granica słowa ( \b), po której następuje sekwencja 10 znaków słów (pod warunkiem, że po każdym nie następuje sekwencja znaków słowa i samych siebie, przy użyciu operatora PCRE o przeczącej przyszłości (?!...)).

Mamy szczęście, że tutaj działa, ponieważ niewiele silników wyrażeń regularnych działa z odwołaniami wstecznymi w powtarzających się częściach.

Zauważ, że (przynajmniej z moją wersją GNU grep)

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

Nie działa, ale

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

robi (as echo aa | grep -Pw '(.)\2') co brzmi jak błąd.

Może chcesz:

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

jeśli chcesz \wlub \brozważasz dowolną literę jako składnik słowa, a nie tylko ASCII w ustawieniach regionalnych innych niż ASCII.

Inna alternatywa:

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

Jest to granica słów (taka, po której nie następuje ciąg znaków, z których jeden się powtarza), a następnie 10 znaków.

Rzeczy, które mogą mieć na myśli:

  • W porównaniu rozróżniana jest Babylonishwielkość liter, więc na przykład pasują, ponieważ wszystkie znaki są różne, mimo że są dwie litery Bs, jedna mała i jedna duża (użyj, -iaby to zmienić).
  • o -w, \wa \b, słowo jest literą (ASCII tylko te, dla GNU grep teraz The [:alpha:]klasa znaków w danym regionie czy korzystania -Pi (*UCP)), cyfry dziesiętne lub podkreślenia .
  • oznacza to, że c'est(dwa słowa zgodnie z francuską definicją słowa) lub it's(jedno słowo zgodnie z niektórymi angielskimi definicjami słowa) lub rendez-vous(jedno słowo zgodnie z francuską definicją słowa) nie są uważane za jedno słowo.
  • Mimo to (*UCP)znaki łączące Unicode nie są uważane za składniki słowa, więc téléphone( $'t\u00e9le\u0301phone') jest uważane za 10 znaków, z których jeden nie jest alfa. défavorisé( $'d\u00e9favorise\u0301') byłby dopasowany, mimo że ma dwa, éponieważ to 10 różnych znaków alfanumerycznych, po których następuje łączący akcent ostry (inny niż alfa, więc granica między tym ea jego akcentem jest ograniczona).
Stéphane Chazelas
źródło
1
Niesamowite. \wnie pasuje -jednak.
Graeme
@Stephane Czy możesz zamieścić krótkie wyjaśnienie dwóch ostatnich wyrażeń.
mkc
Czasami wydaje się, że spojrzenia są rozwiązaniem wszystkich rzeczy, które kiedyś były niemożliwe z RE.
Barmar
1
@Barmar są nadal niemożliwe dzięki wyrażeniom regularnym. „Wyrażenie regularne” jest konstrukcją matematyczną, która wyraźnie dopuszcza tylko niektóre konstrukcje, mianowicie znaki literalne, klasy znaków oraz operatory „|”, „(...)”, „?”, „+” I „*”. Każde tak zwane „wyrażenie regularne”, które używa operatora, który nie jest jednym z powyższych, nie jest w rzeczywistości wyrażeniem regularnym.
Jules
1
@Jules To jest unix.stackexchange.com, a nie math.stackexchange.com. Matematyczne RE są nieistotne w tym kontekście, mówimy o rodzajach RE, których używasz z grep, PCRE itp.
Barmar
12

Okej ... oto niezręczny sposób na pięcioznakowy ciąg:

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

Ponieważ nie możesz umieścić referencji wstecz w klasie postaci (np. [^\1|\2]), Musisz zastosować przeczące spojrzenie - (?!foo). Jest to funkcja PCRE, więc potrzebujesz -Pprzełącznika.

Wzorzec ciągu 10 znaków będzie oczywiście o wiele dłuższy, ale istnieje krótsza metoda wykorzystująca zmienną długość cokolwiek pasuje ('. *') W lookahead:

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

Po przeczytaniu pouczającej odpowiedzi Stephane'a Chazelasa, zdałem sobie sprawę, że istnieje podobny prosty wzór dla tego użytecznego za pomocą -vprzełącznika grep :

    (.).*\1

Ponieważ sprawdzanie odbywa się po jednym znaku na raz, zobaczysz, czy po danym znaku następuje zero lub więcej znaków ( .*), a następnie dopasowanie dla odwołania wstecznego. -vodwraca, drukując tylko rzeczy, które nie pasują do tego wzoru. To sprawia, że ​​referencje wsteczne są bardziej użyteczne, ponieważ nie można ich zanegować za pomocą klasy znaków, a znacznie:

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

będzie działać, aby zidentyfikować ciąg dowolnej długości za pomocą unikalnych znaków, podczas gdy:

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

nie będzie, ponieważ będzie pasować do dowolnego sufiksu unikatowymi znakami (np. abcabcpasuje ze względu abcna koniec, a aaaaze względu ana koniec - stąd dowolny ciąg znaków). Jest to komplikacja spowodowana tym, że spojrzenia mają zerową szerokość (nic nie zużywają).

Złotowłosa
źródło
Dobra robota! Działa to jednak tylko w połączeniu z tym z Q.
Graeme
1
Wierzę, że możesz uprościć pierwszy, jeśli twój silnik regex pozwala na negatywne (.)(?!.*\1)(.)(?!.*\2)(.)(?!.*\3)(.)(?!\4).
spojrzenie w przyszłość
@ChristopherCreutzig: Absolutnie fajny telefon. Dodałem to w.
goldilocks
6

Jeśli nie musisz robić wszystkiego w wyrażeniu regularnym, zrobiłbym to w dwóch krokach: najpierw dopasuj wszystkie 10-literowe słowa, a następnie odfiltruj je pod kątem wyjątkowości. Najkrótszym sposobem, w jaki wiem, jak to zrobić, jest Perl:

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

Zwróć uwagę na dodatkowe \Wkotwice, aby zapewnić dopasowanie tylko słów o długości dokładnie 10 znaków.

Joseph R.
źródło
Dziękuję, ale chciałbym, żeby to był regex oneliner :)
Dylan Meeus
4

Inni sugerują, że nie jest to możliwe bez różnych rozszerzeń niektórych systemów wyrażeń regularnych, które w rzeczywistości nie są regularne. Ponieważ jednak język, który chcesz dopasować, jest skończony, jest on wyraźnie regularny. W przypadku 3 liter z 4-literowego alfabetu byłoby to łatwe:

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

Oczywiście wymyka się to w pośpiechu z większą ilością liter i większych alfabetów. :-)

R ..
źródło
Musiałem głosować za tym, ponieważ tak naprawdę odpowiedź by zadziałała. Chociaż może to być najmniej efektywny sposób, w jaki ktokolwiek napisał regex: P
Dylan Meeus
4

Opcja --perl-regexp(krótka -P) GNU grepużywa bardziej wydajnych wyrażeń regularnych, które zawierają wzorce wybiegające w przyszłość. Poniższy wzór wyszukuje każdą literę, której ta litera nie pojawia się w pozostałej części słowa:

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

Jednak zachowanie w czasie wykonywania jest dość złe, ponieważ \w*może mieć prawie nieskończoną długość. Można go ograniczyć do \w{,8}, ale to także sprawdza poza limitem słów 10 liter. Dlatego następujący wzorzec najpierw sprawdza poprawną długość słowa:

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

Jako plik testowy wykorzystałem duży plik ≈ 500 MB:

  • Pierwszy wzór: ≈ 43 s
  • Późny wzór: ≈ 15 s

Aktualizacja:

Nie mogłem znaleźć znaczącej zmiany w zachowaniu w czasie wykonywania dla niewdzięcznego operatora ( \w*?) lub operatora dzierżawczego ( (...){10}+). Trochę szybciej wydaje się zastąpienie opcji -w:

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

Aktualizacja grep z wersji 2.13 do 2.18 była znacznie bardziej skuteczna. Plik testowy zajął tylko ≈ 6 sekund.

Heiko Oberdiek
źródło
Wydajność będzie w dużej mierze zależeć od charakteru danych. Podczas przeprowadzania testów na moim stwierdziłem, że użycie niepochodnych operatorów ( \w{,8}?) pomogło dla pewnego rodzaju danych wejściowych (choć niezbyt znacząco). Niezłe wykorzystanie \g{-1}do obejścia błędu GNU grep.
Stéphane Chazelas
@StephaneChazelas: Dzięki za opinie. Próbowałem także nie chciwych i zaborczych operatorów i nie znalazłem znaczącej zmiany w zachowaniu w czasie wykonywania (wersja 2.13). Wersja 2.18 jest znacznie szybsza i mogłem zobaczyć choć odrobinę poprawy. Błąd GNU grep występuje w obu wersjach. W każdym razie wolę odniesienie względne \g{-1}, ponieważ sprawia, że ​​wzorzec jest bardziej niezależny od lokalizacji. W tej formie można go wykorzystać jako część większego wzoru.
Heiko Oberdiek
0

Rozwiązanie Perla:

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

ale to nie działa

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

lub

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

testowane z perl v5.14.2 i v5.18.2


źródło
1. i 3. nic nie robi, 2. wypisuje dowolną linię 10 lub więcej znaków, nie więcej niż 2 kolejne spacje. pastebin.com/eEDcy02D
manatwork
prawdopodobnie jest to wersja perla. testowany z wersją 14.14.2 i wersją 5.18.2
Próbowałem ich z wersją 5.1.14.1 na Linuksie i wersją 5.1.14.2 na Cygwin. Oba zachowywały się jak w próbce pastebin, którą wcześniej podłączyłem.
manatwork
pierwsza linia działa dla mnie z zapisanymi wersjami perla. te dwa ostatnie powinny działać, ponieważ są takie same, ale nie działały. Perlre często zauważają, że niektóre zachłanne wyrażenia są wysoce eksperymentalne.
Przetestowano z najnowszymi aktualizacjami. Tylko drugi z nich działa poprawnie. (Jednak słowo musi znajdować się w jednym wierszu, a pytanie dotyczy dopasowania słów, a nie całych wierszy.)
manatwork