2015-06-27 7 views
6

Ich benutze eine Regex, um einige BBCode zu analysieren, also muss die Regex rekursiv arbeiten, um auch Tags in anderen zu finden. Der Großteil des BBCode hat ein Argument, und manchmal wird es zitiert, wenn auch nicht immer.If-else in rekursiver Regex funktioniert nicht wie erwartet

Ein vereinfachtes Äquivalent der regex I (mit HTML-Style-Tags die entweichenden benötigt reduzieren) bin mit dieser ist:

'~<(\")?a(?(1)\1)> #Match the tag, and require a closing quote if an opening one provided 
    ([^<]+ | (?R))* #Match the contents of the tag, including recursively 
</a>~x' 

Allerdings, wenn ich einen Test-String haben, die wie folgt aussieht:

<"a">Content<a>Also Content</a></a> 

sie paßt nur die <a>Also Content</a>, weil, wenn er versucht, vom ersten Tag, die ersten passenden Gruppe zu entsprechen, \1, auf " gesetzt ist, und das ist nicht ov Er wird geschrieben, wenn die Regex rekursiv ausgeführt wird, um dem inneren Tag zu entsprechen, was bedeutet, dass es nicht übereinstimmt und dass Regex fehlschlägt, weil es nicht zitiert wird.

Wenn ich stattdessen konsistent entweder Anführungszeichen verwende oder nicht, funktioniert es gut, aber ich kann nicht sicher sein, dass das der Fall mit dem Inhalt sein wird, den ich analysieren muss. Gibt es eine Möglichkeit, dies zu umgehen? obwohl

"~\[spoiler\s*+ #Match the opening tag 
      (?:=\s*+(\"|\')?((?(1)(?!\\1).|[^\]]){0,100})(?(1)\\1))?+\s*\] #If an option exists, match that 
      (?:\ *(?:\n|<br />))?+ #Get rid of an extra new line before the start of the content if necessary 
      ((?:[^\[\n]++ #Capture all characters until the closing tag 
      |\n(?!\[spoiler]) Capture new line separately so backtracking doesn't run away due to above 
      |\[(?!/?spoiler(?:\s*=[^\]*])?) #Also match all tags that aren't spoilers 
      |(?R))*+) #Allow the pattern to recurse - we also want to match spoilers inside spoilers, 
        # without messing up nesting 
      \n? #Get rid of an extra new line before the closing tag if necessary 
      \[/spoiler] #match the closing tag 
     ~xi" 

Es gibt ein paar andere Fehler mit ihm auch


Der vollständige Regex, die ich verwende, ist [spoiler]content[/spoiler], [spoiler=option]content[/spoiler] und [spoiler="option"]content[/spoiler], anzupassen.

Antwort

3

Die einfachste Lösung ist stattdessen Alternativen zu verwenden:

<(?:a|"a")> 
    ([^<]++ | (?R))* 
</a> 

Aber wenn Sie wirklich nicht wiederholen wollen, dass a Teil, Sie Folgendes tun:

<("?)a\1> 
    ([^<]++ | (?R))* 
</a> 

Demo

Ich habe gerade die bedingte ? innerhalb der Gruppe. Diesmal passt die einfangende Gruppe immer, aber die Übereinstimmung kann leer sein, und die Bedingung ist nicht mehr notwendig.

Exkurs: Ich habe eine possessive quantifier zu [^<] angewendet catastrophic backtracking zu vermeiden.


In Ihrem Fall glaube ich, es ist besser, ein generisches Tag als ein bestimmtes Tag zu entsprechen. Passen Sie alle Tags an und entscheiden Sie dann in Ihrem Code, was mit dem Match zu tun ist.

Hier ist eine vollständige Regex:

\[ 
    (?<tag>\w+) \s* 
    (?:=\s* 
    (?: 
     (?<quote>["']) (?<arg>.{0,100}?) \k<quote> 
     | (?<arg>[^\]]+) 
    ) 
)? 
\] 

(?<content> 
    (?:[^[]++ | (?R))*+ 
) 

\[/\k<tag>\] 

Demo

Bitte beachte, dass ich die J Option hinzugefügt (PCRE_DUPNAMES) in der Lage sein (?<arg> ... ) zweimal zu verwenden.

+0

Die zweite Lösung funktioniert leider nicht richtig auf der komplexeren Regex, die ich für den tatsächlichen Code verwende, aber die erste Lösung sollte funktionieren. – JackW

+0

@StormDrive vielleicht sollten Sie Ihre volle Regex in diesem Fall posten. –

+0

Ich habe es zu der Frage hinzugefügt, obwohl es ziemlich hässlich ist. – JackW

1

(?(1)...) prüft nur, ob die Gruppe 1 definiert wurde, also ist die Bedingung wahr, sobald die Gruppe das erste Mal definiert wurde. Deshalb erhalten Sie dieses Ergebnis (es hängt nicht mit der Rekursionsebene oder was auch immer zusammen). Wenn <a> in der Rekursion erreicht wird, versucht die Regex-Engine <a"> und schlägt fehl Wenn Sie eine bedingte Anweisung verwenden möchten, können Sie statt dessen <("?)a(?(1)\1)> schreiben. Auf diese Weise wird die Gruppe 1 jedes Mal neu definiert.

Natürlich können Sie Ihr Muster auf eine effizientere Art und Weise wie folgt schreiben:

~<(?:a|"a")>[^<]*+(?:(?R)[^<]*)*+</a>~ 

Für Ihr spezielles Problem, werde ich diese Art von Muster verwenden, um alle Tags zu entsprechen:

$pattern = <<<'EOD' 
~ 
\[ (?<tag>\w+) \s* 
(?: 
    = \s* 
    (?| " (?<option>[^"]*) " | ' ([^']*) ' | ([^]\s]*)) # branch reset feature 
)? 
\s* ] 
(?<content> [^[]*+ (?: (?R) [^[]*)*+) 
\[/\g{tag}] 
~xi 
EOD; 

Wenn Wenn Sie ein bestimmtes Tag auf Bodenniveau festlegen möchten, können Sie (?(R)|(?=spoiler\b)) vor dem Tag-Namen hinzufügen.