Wzorzec wyrażenia regularnego do dopasowania, z wyłączeniem kiedy… / Z wyjątkiem między


108

--Edytuj-- Obecne odpowiedzi zawierają kilka przydatnych pomysłów, ale chcę czegoś bardziej kompletnego, co mogę w 100% zrozumieć i ponownie wykorzystać; dlatego wyznaczyłem nagrodę. Również pomysły, które działają wszędzie, są dla mnie lepsze niż niestandardowa składnia, taka jak\K

To pytanie dotyczy tego, jak mogę dopasować wzorzec z wyjątkiem niektórych sytuacji s1 s2 s3. Podaję konkretny przykład, aby pokazać swoje znaczenie, ale wolę ogólną odpowiedź, którą mogę w 100% zrozumieć, aby móc jej użyć ponownie w innych sytuacjach.

Przykład

Chcę dopasować pięć cyfr, używając, \b\d{5}\bale nie w trzech sytuacjach s1 s2 s3:

s1: Nie w wierszu, który kończy się kropką jak to zdanie.

s2: Nigdzie wewnątrz parens.

s3: Nie wewnątrz bloku, który zaczyna się if(i kończy na//endif

Wiem, jak rozwiązać każdy z s1 s2 s3 z lookahead i lookbehind, szczególnie w C # lookbehind lub \Kw PHP.

Na przykład

s1 (?m)(?!\d+.*?\.$)\d+

s3 z C # lookbehind (?<!if\(\D*(?=\d+.*?//endif))\b\d+\b

s3 z PHP \ K (?:(?:if\(.*?//endif)\D*)*\K\d+

Ale mieszanka warunków sprawia, że ​​moja głowa eksploduje. Jeszcze bardziej zła wiadomość jest taka, że ​​być może będę musiał dodać inne warunki s4 s5 w innym czasie.

Dobra wiadomość jest taka, że ​​nie obchodzi mnie, czy przetwarzam pliki przy użyciu najpopularniejszych języków, takich jak PHP, C #, Python lub pralka mojego sąsiada. :) Jestem prawie początkującym w Pythonie i Javie, ale jestem zainteresowany, aby dowiedzieć się, czy ma rozwiązanie.

Więc przyjechałem tutaj, żeby sprawdzić, czy ktoś wymyśli elastyczny przepis.

Podpowiedzi są w porządku: nie musisz podawać mi pełnego kodu. :)

Dziękuję Ci.


1
\Knie ma specjalnej składni php. Proszę rozwinąć i wyjaśnić, co chcesz powiedzieć. Jeśli chcesz nam powiedzieć, że nie potrzebujesz „skomplikowanego” rozwiązania, musisz powiedzieć, co jest dla Ciebie skomplikowane i dlaczego.
hakre

@hakre Masz na myśli to, że Ruby teraz go używa i zaczął się w Perlu?
Hans Schindler

1
Nie, ponieważ to PCRE nie jest PHP (ani Ruby). Perl jest inny, jednak PCRE ma być kompatybilny z Perl Regex.
hakre

Twoje wymagania s2 i s3 wydają się być sprzeczne. s2 oznacza, że ​​nawiasy są zawsze dopasowane i mogą być zagnieżdżane, ale s3 wymaga, aby: "if("otwarty paren był zamknięty, nie za pomocą a ")", ale raczej za "//endif":? A jeśli dla s3 naprawdę miałeś na myśli, że klauzula if powinna być zamknięta "//endif)":, to wymaganie s3 jest podzbiorem s2.
ridgerunner

@hakre Tak, znam PCRE, ale żeby wyjaśnić, pytanie dotyczy języka programowania ... jest napisane especially in C# lookbehind or \K in PHP... Ale C # lookbehind nie tylko C # to .NET, więc możesz też narzekać, mówię C # nie .NET :) A w odpowiedzi mówię Ruby, a nie Onigurama, to też jest złe ... Czy jest inny język, który używa PCRE? Nie mówię o Notepad ++ lub narzędziach serwera, to jest pytanie o użycie funkcji w języku Mam nadzieję, że wyjaśnię i przepraszam, jeśli wygląda źle
Hans Schindler

Odpowiedzi:


205

Hans, wezmę przynętę i wypoleruję moją wcześniejszą odpowiedź. Powiedziałeś, że chcesz „czegoś bardziej kompletnego”, więc mam nadzieję, że nie masz nic przeciwko długiej odpowiedzi - po prostu próbując zadowolić. Zacznijmy od pewnego tła.

Po pierwsze, to doskonałe pytanie. Często pojawiają się pytania dotyczące dopasowywania pewnych wzorców, z wyjątkiem określonych kontekstów (na przykład w bloku kodu lub w nawiasach). Te pytania często prowadzą do dość niewygodnych rozwiązań. Twoje pytanie dotyczące wielu kontekstów jest więc szczególnym wyzwaniem.

Niespodzianka

Zaskakujące jest, że istnieje co najmniej jedno wydajne rozwiązanie, które jest ogólne, łatwe do wdrożenia i przyjemne w utrzymaniu. To działa ze wszystkimi smakami regex , które pozwalają kontrolować grupy przechwytywania w kodzie. I zdarza się, że odpowiada na wiele typowych pytań, które na pierwszy rzut oka mogą brzmieć inaczej niż twoje: „dopasuj wszystko oprócz pączków”, „zamień wszystko oprócz ...”, „dopasuj wszystkie słowa oprócz tych z czarnej listy mojej mamy”, „ignoruj tagi "," dopasuj temperaturę, chyba że są zapisane kursywą "...

Niestety, technika ta nie jest dobrze znana: szacuję, że na dwadzieścia pytań SO, które mogłyby jej użyć, tylko jedno ma odpowiedź, która o niej wspomina - co oznacza, że ​​może jedna na pięćdziesiąt lub sześćdziesiąt odpowiedzi. Zobacz moją wymianę z Kobi w komentarzach. Technika ta jest szczegółowo opisana w tym artykule, który nazywa ją (optymistycznie) „najlepszą sztuczką wyrażeń regularnych wszechczasów”. Nie wdając się w szczegóły, spróbuję dokładnie zrozumieć, jak działa ta technika. Aby uzyskać więcej szczegółów i przykłady kodu w różnych językach, zachęcam do zapoznania się z tym źródłem.

Lepiej znana odmiana

Istnieje odmiana składni specyficzna dla Perla i PHP, która zapewnia to samo. Zobaczysz to w SO w rękach mistrzów regex, takich jak CasimiretHippolyte i HamZa . Powiem ci więcej na ten temat poniżej, ale skupiam się tutaj na ogólnym rozwiązaniu, które działa ze wszystkimi odmianami wyrażeń regularnych (o ile możesz sprawdzić grupy przechwytywania w swoim kodzie).

Dzięki za całe tło, zx81 ... Ale jaki jest przepis?

Kluczowy fakt

Metoda zwraca dopasowanie w przechwyceniu grupy 1. W ogóle nie dba o ogólny mecz.

W rzeczywistości sztuczka polega na dopasowaniu różnych kontekstów, których nie chcemy (łącząc te konteksty w łańcuch za pomocą |OR / alternacji), aby je „zneutralizować”. Po dopasowaniu wszystkich niechcianych kontekstów ostatnia część naprzemienności dopasowuje to, czego chcemy i przechwytuje to do grupy 1.

Ogólny przepis to

Not_this_context|Not_this_either|StayAway|(WhatYouWant)

To będzie pasować Not_this_context, ale w pewnym sensie mecz trafia do kosza, ponieważ nie będziemy patrzeć na ogólne mecze: patrzymy tylko na przechwyty w grupie 1.

W twoim przypadku, z twoimi cyframi i trzema kontekstami do zignorowania, możemy zrobić:

s1|s2|s3|(\b\d+\b)

Zauważ, że ponieważ w rzeczywistości dopasowujemy s1, s2 i s3 zamiast próbować ich uniknąć za pomocą lookarounds, poszczególne wyrażenia dla s1, s2 i s3 mogą pozostać jasne jak dzień. (Są to podwyrażenia po każdej stronie a |)

Całe wyrażenie można zapisać w ten sposób:

(?m)^.*\.$|\([^\)]*\)|if\(.*?//endif|(\b\d+\b)

Zobacz to demo (ale skup się na grupach przechwytywania w prawym dolnym panelu).

Jeśli spróbujesz w myślach podzielić to wyrażenie regularne w każdym |separatorze, w rzeczywistości jest to tylko seria czterech bardzo prostych wyrażeń.

W przypadku smaków obsługujących wolne odstępy jest to szczególnie dobre.

(?mx)
      ### s1: Match line that ends with a period ###
^.*\.$  
|     ### OR s2: Match anything between parentheses ###
\([^\)]*\)  
|     ### OR s3: Match any if(...//endif block ###
if\(.*?//endif  
|     ### OR capture digits to Group 1 ###
(\b\d+\b)

Jest to wyjątkowo łatwe do odczytania i utrzymania.

Rozszerzanie wyrażenia regularnego

Jeśli chcesz zignorować więcej sytuacji s4 i s5, dodaj je w większej liczbie naprzemienności po lewej stronie:

s4|s5|s1|s2|s3|(\b\d+\b)

Jak to działa?

Konteksty, których nie chcesz, są dodawane do listy wariantów po lewej stronie: będą pasować, ale te ogólne dopasowania nigdy nie są sprawdzane, więc dopasowanie ich jest sposobem na umieszczenie ich w „koszu na śmieci”.

Jednak żądana zawartość jest przechwytywana do grupy 1. Następnie musisz programowo sprawdzić, czy grupa 1 jest ustawiona, a nie pusta. Jest to trywialne zadanie programistyczne (a później porozmawiamy o tym, jak to się robi), zwłaszcza biorąc pod uwagę, że pozostawia ono proste wyrażenie regularne, które można zrozumieć na pierwszy rzut oka i zmienić lub rozszerzyć zgodnie z wymaganiami.

Nie zawsze jestem fanem wizualizacji, ale ta dobrze pokazuje, jak prosta jest metoda. Każda „linia” odpowiada potencjalnemu dopasowaniu, ale tylko dolna linia jest uwzględniana w grupie 1.

Wizualizacja wyrażeń regularnych

Debuggex Demo

Odmiana Perl / PCRE

W przeciwieństwie do powyższego ogólnego rozwiązania, istnieje odmiana Perla i PCRE, która jest często widoczna w SO, przynajmniej w rękach regex Gods, takich jak @CasimiretHippolyte i @HamZa. To jest:

(?:s1|s2|s3)(*SKIP)(*F)|whatYouWant

W Twoim przypadku:

(?m)(?:^.*\.$|\([^()]*\)|if\(.*?//endif)(*SKIP)(*F)|\b\d+\b

Ta odmiana jest nieco łatwiejsza w użyciu, ponieważ treść dopasowana w kontekstach s1, s2 i s3 jest po prostu pomijana, więc nie musisz sprawdzać przechwyceń grupy 1 (zwróć uwagę, że nawiasy zniknęły). Zapałki zawierają tylkowhatYouWant

Należy zauważyć, że (*F), (*FAIL)i (?!)są tym samym. Jeśli chcesz być bardziej niejasny, możesz użyć(*SKIP)(?!)

demo dla tej wersji

Aplikacje

Oto kilka typowych problemów, które ta technika często może łatwo rozwiązać. Zauważysz, że wybór słów może sprawić, że niektóre z tych problemów brzmią inaczej, podczas gdy w rzeczywistości są one praktycznie identyczne.

  1. Jak mogę dopasować foo z wyjątkiem dowolnego miejsca w tagu <a stuff...>...</a>?
  2. Jak mogę dopasować foo poza <i>tagiem lub fragmentem kodu javascript (więcej warunków)?
  3. Jak mogę dopasować wszystkie słowa, których nie ma na tej czarnej liście?
  4. Jak mogę zignorować cokolwiek wewnątrz bloku SUB ... END SUB?
  5. Jak mogę dopasować wszystko oprócz ... s1 s2 s3?

Jak zaprogramować przechwyty w grupie 1

Nie zrobiłeś tego, jeśli chodzi o kod, ale do ukończenia ... Kod do inspekcji grupy 1 będzie oczywiście zależał od wybranego języka. W każdym razie nie powinien dodawać więcej niż kilka wierszy do kodu, którego używałbyś do sprawdzania dopasowań.

W razie wątpliwości polecam zajrzeć do sekcji z przykładami kodu we wspomnianym wcześniej artykule, która przedstawia kod dla kilku języków.

Alternatywy

W zależności od złożoności pytania i używanego silnika wyrażeń regularnych istnieje kilka alternatyw. Oto dwa, które można zastosować w większości sytuacji, w tym w wielu warunkach. Moim zdaniem żadna z nich nie jest tak atrakcyjna jak s1|s2|s3|(whatYouWant)przepis, choćby dlatego, że zawsze zwycięża przejrzystość.

1. Wymień, a następnie dopasuj.

Dobrym rozwiązaniem, które brzmi hakersko, ale działa dobrze w wielu środowiskach, jest dwuetapowa praca. Pierwsze wyrażenie regularne neutralizuje kontekst, który chcesz zignorować, zastępując potencjalnie sprzeczne ciągi. Jeśli chcesz tylko dopasować, możesz zastąpić pustym ciągiem, a następnie uruchomić dopasowanie w drugim kroku. Jeśli chcesz zamienić, możesz najpierw zamienić ciągi, które mają być ignorowane, na coś charakterystycznego, na przykład otoczenie cyfr łańcuchem o stałej szerokości @@@. Po tej zamianie możesz zamienić to, czego naprawdę chciałeś, a następnie będziesz musiał przywrócić swoje charakterystyczne @@@struny.

2. Lookarounds.

Twój oryginalny post pokazał, że wiesz, jak wykluczyć pojedynczy warunek za pomocą obejrzeń. Powiedziałeś, że C # jest do tego świetny i masz rację, ale nie jest to jedyna opcja. Formy wyrażeń regularnych .NET znalezione na przykład w C #, VB.NET i Visual C ++, a także wciąż eksperymentalny regexmoduł do zastąpienia rew Pythonie, to jedyne dwa znane mi silniki, które obsługują lookbehind o nieskończonej szerokości. Dzięki tym narzędziom jeden warunek w jednym spojrzeniu wstecz może zająć się patrzeniem nie tylko za mecz, ale także na mecz i poza mecz, unikając potrzeby koordynacji z patrzeniem w przód. Więcej warunków? Więcej obejść.

Recykling wyrażenia regularnego, które miałeś dla s3 w C #, cały wzorzec wyglądałby tak.

(?!.*\.)(?<!\([^()]*(?=\d+[^)]*\)))(?<!if\(\D*(?=\d+.*?//endif))\b\d+\b

Ale teraz już wiesz, że tego nie polecam, prawda?

Usunięcia

@HamZa i @Jerry zasugerowali, żebym wspomniał o dodatkowej sztuczce w przypadku, gdy chcesz po prostu usunąć WhatYouWant. Pamiętasz, że przepis na dopasowanie WhatYouWant(przechwycenie go do grupy 1) był s1|s2|s3|(WhatYouWant), prawda? Aby usunąć wszystkie wystąpienia WhatYouWant, należy zmienić wyrażenie regularne na

(s1|s2|s3)|WhatYouWant

W przypadku ciągu zastępczego używasz $1. To, co się tutaj dzieje, to to, że dla każdego s1|s2|s3dopasowanego wystąpienia , zamiana $1zastępuje to wystąpienie sobą (do którego odwołuje się $1). Z drugiej strony, gdy WhatYouWantjest dopasowany, jest zastępowany przez pustą grupę i nic więcej - i dlatego jest usuwany. Obejrzyj to demo , dziękuję @HamZa i @Jerry za zasugerowanie tego wspaniałego dodatku.

Części zamienne

To prowadzi nas do zamienników, o których pokrótce się poruszę.

  1. Jeśli zastępujesz go niczym, zobacz powyższą sztuczkę „Usunięcia”.
  2. Podczas wymiany, jeśli używasz Perla lub PCRE, użyj (*SKIP)(*F)wariantu wspomnianego powyżej, aby dokładnie dopasować to, co chcesz, i wykonaj prostą wymianę.
  3. W innych odmianach, w ramach wywołania funkcji zamiany, sprawdź dopasowanie za pomocą wywołania zwrotnego lub lambda i zamień, jeśli ustawiono grupę 1. Jeśli potrzebujesz pomocy, artykuł już przywoływany zawiera kod w różnych językach.

Baw się dobrze!

Nie, czekaj, jest więcej!

Ach, nie, zachowam to dla moich wspomnień w dwudziestu tomach, które zostaną wydane wiosną przyszłego roku.


2
@Kobi Dwuczęściowa odpowiedź. Tak, wczoraj wieczorem dałem się ponieść pisaniu i napisałem na dole, że będę na nim spał i posprzątam później. :) Tak, sztuczka jest prosta, ale nie podzielam twojego przekonania, że ​​jest „podstawowa”, ponieważ nie wydaje się być częścią powszechnych narzędzi używanych przez ludzi do rozwiązywania problemów związanych z wykluczeniem. Kiedy szukałem w Google problemów „z wyjątkiem” lub „chyba” lub „nie wewnątrz” problemów w SO, tylko jedna odpowiedź (bez głosów) sugerowała to, żadna z pozostałych nie. Nawiasem mówiąc, nie widziałem twoich odpowiedzi, które są wspaniałe. :)
zx81

2
Przepraszamy, ale „Najlepsza sztuczka” Rexa po prostu nie działa ( niezawodnie ). Powiedz, że chcesz dopasować Tarzan, ale nie w żadnym miejscu w cudzysłowie. Wyrażenie /no|no|(yes)/regularne : trick wyglądałoby następująco: /"[^"]*"|Tarzan/(ignorując znaki ucieczki). To będzie działać na wielu przypadkach, ale nie całkowicie, gdy stosuje się do następnego ważnego tekstu javascript: var bug1 = 'One " quote here. Should match this Tarzan'; var bug2 = "Should not match this Tarzan";. Sztuczka Rexa działa tylko wtedy, gdy WSZYSTKIE możliwe struktury są dopasowane - innymi słowy - musisz w pełni przeanalizować tekst, aby zagwarantować 100% dokładność.
ridgerunner 07.07.14

1
Przepraszam, jeśli brzmiałem szorstko - z pewnością nie był to mój zamiar. Chodzi mi o to (podobnie jak w moim drugim komentarzu do pierwotnego pytania powyżej), że prawidłowe rozwiązanie w dużym stopniu zależy od przeszukiwanego tekstu docelowego. Mój przykład ma kod źródłowy JavaScript jako tekst docelowy, który ma jeden podwójny cudzysłów ujęty w pojedynczy cudzysłów. Równie dobrze mógł to być dosłowny RegExp, taki jak: var bug1 = /"[^"]*"|(Tarzan)/gi;i miał ten sam efekt (a ten drugi przykład z pewnością nie jest przypadkiem skrajnym). Mógłbym przytoczyć wiele innych przykładów, w których ta technika nie działa niezawodnie.
ridgerunner

1
@ridgerunner Zawsze lubię słyszeć od Ciebie, po prostu brzmi to dla mnie nieuzasadniona szorstka. Kiedy wiemy, że nasze ciągi znaków mogą zawierać „fałszywe alerty”, wszyscy dostosowujemy nasze wzorce. Na przykład, aby dopasować ciąg, który może zawierać cudzysłowy, które mogą wyrzucić dopasowanie do łańcucha, możesz użyć (?<!\\)"(?:\\"|[^"\r\n])*+" Nie wyciągaj dużych dział, chyba że masz ku temu powód. Zasada rozwiązania jest nadal aktualna. Jeśli nie jesteśmy w stanie wyrazić wzoru do umieszczenia po lewej stronie, to inna historia, potrzebujemy innego rozwiązania. Ale rozwiązanie robi to, co reklamuje.
zx81,

1
Ta odpowiedź została dodana do często zadawanych pytań dotyczących wyrażeń regularnych przepełnienia stosu przez użytkownika @funkwurm.
aliteralmind

11

Wykonaj trzy różne dopasowania i obsłuż kombinację trzech sytuacji, używając logiki warunkowej w programie. Nie musisz obsługiwać wszystkiego w jednym gigantycznym wyrażeniu regularnym.

EDYCJA: pozwól mi trochę rozszerzyć, ponieważ pytanie stało się bardziej interesujące :-)

Ogólną ideą, którą próbujesz tutaj uchwycić, jest dopasowanie do określonego wzorca wyrażenia regularnego, ale nie wtedy, gdy istnieją pewne inne (może to być dowolna liczba) wzorce obecne w ciągu testowym. Na szczęście możesz skorzystać ze swojego języka programowania: zachowaj proste wyrażenia regularne i po prostu użyj złożonego warunku. Najlepszą praktyką byłoby uchwycenie tego pomysłu w składniku wielokrotnego użytku, więc stwórzmy klasę i metodę, która ją implementuje:

using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

public class MatcherWithExceptions {
  private string m_searchStr;
  private Regex m_searchRegex;
  private IEnumerable<Regex> m_exceptionRegexes;

  public string SearchString {
    get { return m_searchStr; }
    set {
      m_searchStr = value;
      m_searchRegex = new Regex(value);
    }
  }

  public string[] ExceptionStrings {
    set { m_exceptionRegexes = from es in value select new Regex(es); }
  }

  public bool IsMatch(string testStr) {
    return (
      m_searchRegex.IsMatch(testStr)
      && !m_exceptionRegexes.Any(er => er.IsMatch(testStr))
    );
  }
}

public class App {
  public static void Main() {
    var mwe = new MatcherWithExceptions();

    // Set up the matcher object.
    mwe.SearchString = @"\b\d{5}\b";
    mwe.ExceptionStrings = new string[] {
      @"\.$"
    , @"\(.*" + mwe.SearchString + @".*\)"
    , @"if\(.*" + mwe.SearchString + @".*//endif"
    };

    var testStrs = new string[] {
      "1." // False
    , "11111." // False
    , "(11111)" // False
    , "if(11111//endif" // False
    , "if(11111" // True
    , "11111" // True
    };

    // Perform the tests.
    foreach (var ts in testStrs) {
      System.Console.WriteLine(mwe.IsMatch(ts));
    }
  }
}

Tak więc powyżej ustawiliśmy ciąg wyszukiwania (pięć cyfr), wiele ciągów wyjątków (twoje s1 , s2 i s3 ), a następnie spróbujemy dopasować je do kilku ciągów testowych. Wydrukowane wyniki powinny być takie, jak pokazano w komentarzach obok każdego testu.


2
Masz na myśli może dopasuj trzy wyrażenia regularne z rzędu? Regex 1 eliminuje sytuację 1 (może po prostu usuwa złą cyfrę), r2 usuwa s2, r3 usuwa s3 i dopasowuje pozostałe cyfry? To ciekawy pomysł.
Hans Schindler

Ha, jasne, dlatego cię zagłosowałem. :) Nie zrozum mnie źle, nadal uważam, że w tym konkretnym przypadku moja odpowiedź jest bardziej wydajna i łatwa do utrzymania. Czy widziałeś wersję z wolnym odstępem, którą dodałem wczoraj? Jest to jednoprzebiegowe i wyjątkowo łatwe do odczytania i utrzymania. Ale podoba mi się twoja praca i twoja rozszerzona odpowiedź. Przepraszam, że nie mogę ponownie zagłosować, inaczej bym to zrobił. :)
zx81

2

Twoje wymaganie, aby nie było w środku parens, jest niemożliwe do spełnienia we wszystkich przypadkach. Mianowicie, jeśli w jakiś sposób możesz znaleźć (po lewej i )po prawej stronie, nie zawsze oznacza to, że jesteś w środku parens. Na przykład.

(....) + 55555 + (.....)- nie wewnątrz parenów jeszcze są (i )na lewo i na prawo

Teraz możesz uważać się za sprytnego i szukać (w lewo tylko wtedy, gdy nie napotkasz )wcześniej i odwrotnie, po prawej. To nie zadziała w tym przypadku:

((.....) + 55555 + (.....))- pareny wewnętrzne, mimo że są zamykane )i (na lewo i na prawo.

Nie można dowiedzieć się, czy jesteś wewnątrz parenów, używając wyrażenia regularnego, ponieważ wyrażenie regularne nie może policzyć, ile parenów zostało otwartych i ile zamkniętych.

Rozważ to łatwiejsze zadanie: używając wyrażenia regularnego, dowiedz się, czy wszystkie (prawdopodobnie zagnieżdżone) pareny w łańcuchu są zamknięte, czyli dla każdego (, co musisz znaleźć ). Dowiesz się, że jest to niemożliwe do rozwiązania, a jeśli nie możesz tego rozwiązać za pomocą wyrażenia regularnego, nie możesz dowiedzieć się, czy słowo znajduje się wewnątrz parens dla wszystkich przypadków, ponieważ nie możesz znaleźć w jakiejś pozycji w ciągu, jeśli wszystkie poprzedzające (mają odpowiednik ).


2
Nikt nie powiedział nic o zagnieżdżonych nawiasach, a Twój przypadek # 1 jest dobrze obsługiwany przez odpowiedź zx81.
Dan Bechard

Dziękuję za miłe przemyślenia :) ale zagnieżdżony nawias nie martwi mnie o to pytanie, bardziej chodzi o ideę złych sytuacji s1 s2 s3
Hans Schindler

Oczywiście nie jest to niemożliwe! Właśnie dlatego musisz śledzić poziom parsów, w których obecnie analizujesz.
MrWonderful

Cóż, jeśli parsujesz jakiś rodzaj CFG, jak wydaje się robić OP, lepiej będzie, jeśli wygenerujesz parser LALR lub podobny, który nie ma z tym problemów.
RokL

2

Hans, jeśli nie masz nic przeciwko, użyłem pralki twojego sąsiada o nazwie perl :)

Edytowano: poniżej pseudokodu:

  loop through input
  if line contains 'if(' set skip=true
        if skip= true do nothing
        else
           if line match '\b\d{5}\b' set s0=true
           if line does not match s1 condition  set s1=true
           if line does not match s2 condition  set s2=true
           if s0,s1,s2 are true print line 
  if line contains '//endif' set skip=false

Biorąc pod uwagę plik input.txt:

tiago@dell:~$ cat input.txt 
this is a text
it should match 12345
if(
it should not match 12345
//endif 
it should match 12345
it should not match 12345.
it should not match ( blabla 12345  blablabla )
it should not match ( 12345 )
it should match 12345

Oraz skrypt validator.pl:

tiago@dell:~$ cat validator.pl 
#! /usr/bin/perl
use warnings;
use strict;
use Data::Dumper;

sub validate_s0 {
    my $line = $_[0];
    if ( $line =~ \d{5/ ){
        return "true";
    }
    return "false";
}

sub validate_s1 {
    my $line = $_[0];
    if ( $line =~ /\.$/ ){
        return "false";
    }
    return "true";
}

sub validate_s2 {
    my $line = $_[0];
    if ( $line =~ /.*?\(.*\d{5.*?\).*/ ){
        return "false";
    }
    return "true";
}

my $skip = "false";
while (<>){
    my $line = $_; 

    if( $line =~ /if\(/ ){
       $skip = "true";  
    }

    if ( $skip eq "false" ) {
        my $s0_status = validate_s0 "$line"; 
        my $s1_status = validate_s1 "$line";
        my $s2_status = validate_s2 "$line";

        if ( $s0_status eq "true"){
            if ( $s1_status eq "true"){
                if ( $s2_status eq "true"){
                    print "$line";
                }
            }
        }
    } 

    if ( $line =~ /\/\/endif/) {
        $skip="false";
    }
}

Wykonanie:

tiago @ dell: ~ $ cat input.txt | perl validator.pl
powinien pasować do 12345
powinien pasować do 12345
powinien pasować do 12345

2

Nie jestem pewien, czy to ci pomoże, czy nie, ale zapewniam rozwiązanie, biorąc pod uwagę następujące założenia -

  1. Potrzebujesz eleganckiego rozwiązania, aby sprawdzić wszystkie warunki
  2. Warunki mogą ulec zmianie w przyszłości iw dowolnym czasie.
  3. Jeden warunek nie powinien zależeć od innych.

Jednak wziąłem pod uwagę również następujące -

  1. Podany plik zawiera minimalne błędy. Jeśli tak się stanie, mój kod może wymagać pewnych modyfikacji, aby sobie z tym poradzić.
  2. Użyłem stosu do śledzenia if(bloków.

OK, oto rozwiązanie -

Użyłem C #, a wraz z nim MEF (Microsoft Extensibility Framework), aby zaimplementować konfigurowalne parsery. Pomysł polega na tym, że do analizy składniowej należy użyć jednego parsera oraz listy konfigurowalnych klas walidatora, aby sprawdzić poprawność wiersza i zwrócić wartość true lub false na podstawie walidacji. Następnie możesz dodać lub usunąć dowolny walidator w dowolnym momencie lub dodać nowy, jeśli chcesz. Do tej pory zaimplementowałem już dla S1, S2 i S3, o których wspominałeś, sprawdź klasy w punkcie 3. Musisz dodać klasy dla s4, s5, jeśli będziesz potrzebować w przyszłości.

  1. Najpierw utwórz interfejsy -

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace FileParserDemo.Contracts
    {
        public interface IParser
        {
            String[] GetMatchedLines(String filename);
        }
    
        public interface IPatternMatcher
        {
            Boolean IsMatched(String line, Stack<string> stack);
        }
    }
  2. Następnie pojawia się czytnik plików i sprawdzanie -

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using FileParserDemo.Contracts;
    using System.ComponentModel.Composition.Hosting;
    using System.ComponentModel.Composition;
    using System.IO;
    using System.Collections;
    
    namespace FileParserDemo.Parsers
    {
        public class Parser : IParser
        {
            [ImportMany]
            IEnumerable<Lazy<IPatternMatcher>> parsers;
            private CompositionContainer _container;
    
            public void ComposeParts()
            {
                var catalog = new AggregateCatalog();
                catalog.Catalogs.Add(new AssemblyCatalog(typeof(IParser).Assembly));
                _container = new CompositionContainer(catalog);
                try
                {
                    this._container.ComposeParts(this);
                }
                catch
                {
    
                }
            }
    
            public String[] GetMatchedLines(String filename)
            {
                var matched = new List<String>();
                var stack = new Stack<string>();
                using (StreamReader sr = File.OpenText(filename))
                {
                    String line = "";
                    while (!sr.EndOfStream)
                    {
                        line = sr.ReadLine();
                        var m = true;
                        foreach(var matcher in this.parsers){
                            m = m && matcher.Value.IsMatched(line, stack);
                        }
                        if (m)
                        {
                            matched.Add(line);
                        }
                     }
                }
                return matched.ToArray();
            }
        }
    }
  3. Potem pojawia się implementacja poszczególnych warcabów, nazwy klas są oczywiste, więc nie sądzę, aby potrzebowały więcej opisów.

    using FileParserDemo.Contracts;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Threading.Tasks;
    
    namespace FileParserDemo.PatternMatchers
    {
        [Export(typeof(IPatternMatcher))]
        public class MatchAllNumbers : IPatternMatcher
        {
            public Boolean IsMatched(String line, Stack<string> stack)
            {
                var regex = new Regex("\\d+");
                return regex.IsMatch(line);
            }
        }
    
        [Export(typeof(IPatternMatcher))]
        public class RemoveIfBlock : IPatternMatcher
        {
            public Boolean IsMatched(String line, Stack<string> stack)
            {
                var regex = new Regex("if\\(");
                if (regex.IsMatch(line))
                {
                    foreach (var m in regex.Matches(line))
                    {
                        //push the if
                        stack.Push(m.ToString());
                    }
                    //ignore current line, and will validate on next line with stack
                    return true;
                }
                regex = new Regex("//endif");
                if (regex.IsMatch(line))
                {
                    foreach (var m in regex.Matches(line))
                    {
                        stack.Pop();
                    }
                }
                return stack.Count == 0; //if stack has an item then ignoring this block
            }
        }
    
        [Export(typeof(IPatternMatcher))]
        public class RemoveWithEndPeriod : IPatternMatcher
        {
            public Boolean IsMatched(String line, Stack<string> stack)
            {
                var regex = new Regex("(?m)(?!\\d+.*?\\.$)\\d+");
                return regex.IsMatch(line);
            }
        }
    
    
        [Export(typeof(IPatternMatcher))]
        public class RemoveWithInParenthesis : IPatternMatcher
        {
            public Boolean IsMatched(String line, Stack<string> stack)
            {
                var regex = new Regex("\\(.*\\d+.*\\)");
                return !regex.IsMatch(line);
            }
        }
    }
  4. Program -

    using FileParserDemo.Contracts;
    using FileParserDemo.Parsers;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace FileParserDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                var parser = new Parser();
                parser.ComposeParts();
                var matches = parser.GetMatchedLines(Path.GetFullPath("test.txt"));
                foreach (var s in matches)
                {
                    Console.WriteLine(s);
                }
                Console.ReadLine();
            }
        }
    }

Do testów wziąłem przykładowy plik @ Tiago, Test.txtktóry miał następujące linie -

this is a text
it should match 12345
if(
it should not match 12345
//endif 
it should match 12345
it should not match 12345.
it should not match ( blabla 12345  blablabla )
it should not match ( 12345 )
it should match 12345

Daje wynik -

it should match 12345
it should match 12345
it should match 12345

Nie wiem, czy to ci pomogło, czy nie, bawiłem się dobrze, bawiąc się tym .... :)

Najlepsze w tym jest to, że aby dodać nowy warunek, wystarczy podać implementację IPatternMatcher, zostanie on automatycznie wywołany, a tym samym zostanie zweryfikowany.


2

To samo, co @ zx81, (*SKIP)(*F)ale z użyciem negatywnego potwierdzenia antycypowania.

(?m)(?:if\(.*?\/\/endif|\([^()]*\))(*SKIP)(*F)|\b\d+\b(?!.*\.$)

PRÓBNY

W Pythonie zrobiłbym to z łatwością,

import re
string = """cat 123 sat.
I like 000 not (456) though 111 is fine
222 if(  //endif if(cat==789 stuff  //endif   333"""
for line in string.split('\n'):                                  # Split the input according to the `\n` character and then iterate over the parts.
    if not line.endswith('.'):                                   # Don't consider the part which ends with a dot.
        for i in re.split(r'\([^()]*\)|if\(.*?//endif', line):   # Again split the part by brackets or if condition which endswith `//endif` and then iterate over the inner parts.
            for j in re.findall(r'\b\d+\b', i):                  # Then find all the numbers which are present inside the inner parts and then loop through the fetched numbers.
                print(j)                                         # Prints the number one ny one.

Wynik:

000
111
222
333
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.