2009-04-02 9 views
39

Ich habe begonnen zu fühlen, dass die Verwendung von regulären Ausdrücken die Wartbarkeit des Codes verringert. Es gibt etwas Böses an der Kürze und Kraft regulärer Ausdrücke. Perl verbindet dies mit Nebenwirkungen wie Standardoperatoren.Wie schreibe ich mehr wartbare reguläre Ausdrücke?

Ich habe die Angewohnheit, reguläre Ausdrücke mit mindestens einem Satz zu dokumentieren, der die grundlegende Absicht und mindestens ein Beispiel dafür angibt, was zusammenpassen würde.

Da reguläre Ausdrücke aufgebaut sind, ist es meiner Meinung nach unbedingt erforderlich, die größten Komponenten jedes Elements im Ausdruck zu kommentieren. Trotzdem kratze mich sogar mein eigener regulärer Gesichtsausdruck, als würde ich Klingonisch lesen.

Wollen Sie Ihre regulären Ausdrücke absichtlich verdummen? Zerlegen Sie möglicherweise kürzere und stärkere in einfachere Schritte? Ich habe es aufgegeben, reguläre Ausdrücke zu verschachteln. Gibt es reguläre Expressionskonstrukte, die Sie aufgrund von Mainability-Problemen vermeiden?

Lassen Sie dieses Beispiel die Frage nicht trüben.

Wenn das folgende von Michael Ash eine Art Bug in ihm hatte, würden Sie irgendwelche Aussichten haben, irgendetwas zu tun, aber es gänzlich wegzuwerfen?

^(?:(?:(?:0?[13578]|1[02])(\/|-|\.)31)\1|(?:(?:0?[13-9]|1[0-2])(\/|-|\.)(?:29|30)\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:0?2(\/|-|\.)29\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:(?:0?[1-9])|(?:1[0-2]))(\/|-|\.)(?:0?[1-9]|1\d|2[0-8])\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$ 

Auf Anfrage kann der genaue Zweck mit dem obigen Link von Herrn Ash gefunden werden.

Übereinstimmungen 01.1.02 | 11-30-2001 | 2/29/2000

Nicht übereinstimmungen 02/29/01 | 13/01/2002 | 11/00/02

+0

Ich freue mich auf die Antworten zu diesem Thema. Ich tendiere zu denken, dass es keine gute Antwort gibt, so sehr ich Regex liebe;). – RedBlueThing

+4

Um Ihre Ausdrücke regelmäßig und pflegeleicht zu halten, empfehle ich Pflaumensaft. – jerebear

+0

In Bezug auf große haarige Regex: Ja, würde ich. Einige Aussichten zumindest. Es ist nicht mehr unleserlich als das durchschnittliche Kapitel von Dostojewski. – chaos

Antwort

17

Normalerweise versuche ich einfach, alle meine Regular Expression-Aufrufe innerhalb ihrer eigenen Funktion mit einem aussagekräftigen Namen und einigen grundlegenden Kommentaren zu umhüllen. Reguläre Ausdrücke halte ich für eine schreibgeschützte Sprache, die nur von demjenigen gelesen werden kann, der sie geschrieben hat (es sei denn, es ist wirklich einfach). Ich erwarte, dass jemand den Ausdruck wahrscheinlich komplett neu schreiben müsste, wenn er seine Absicht ändern müsste, und das ist wahrscheinlich zum Besseren, um das Regular Expression Training am Leben zu erhalten.

+0

Verkapsele, was es mit funktionaler Bedeutung macht. Das ist eine gute Praxis, die ich nicht angestellt habe. – ojblass

+0

Ja, es hat sich für viele der größeren Projekte, an denen ich beteiligt war, gut entwickelt. – James

+1

Eigentlich ist das ein guter Ansatz. Richtig benannt und dokumentiert, könnten Sie dann eine Nicht-RE-Option eingeben, wenn die RE nicht mehr erreichbar ist. Ich stimme nicht zu, dass sie nur schreiben sollten, aber +1 trotzdem. – paxdiablo

0

Ich erwarte nicht, dass reguläre Ausdrücke lesbar sind, also lasse ich sie einfach so, wie sie sind, und schreibe sie bei Bedarf um.

+0

Denkst du nicht, dass du Fehler mit dieser Angewohnheit einbringst? – ojblass

+0

Es könnte in einigen Szenarien ein Risiko darstellen, in anderen nicht.Zum Beispiel würde ich mich selbst sicher fühlen, wenn komplette Komponententests vorhanden wären, die jeden Anwendungsfall für den regulären Ausdruck abdecken. Ohne diese Tests wäre es sicher gruselig! –

+3

gar nicht, sie zu ändern ist risikoreicher, als sie in den meisten Fällen neu zu schreiben, Sie müssen den ganzen Umfang verstehen. Unittest ist ein Muss, wenn Sie komplexe reguläre Ausdrücke verwenden –

1

Ich könnte immer noch damit arbeiten. Ich würde einfach Regulator verwenden. Eine Sache, die Sie tun können, ist die Regex zusammen mit Testdaten dafür zu speichern.

Natürlich könnte ich auch Kommentare hinzufügen.


Hier ist, was Expresso produziert. Ich hatte es noch nie benutzt, aber jetzt ist Regulator aus einem Job:

 
// using System.Text.RegularExpressions; 

/// 
/// Regular expression built for C# on: Thu, Apr 2, 2009, 12:51:56 AM 
/// Using Expresso Version: 3.0.3276, http://www.ultrapico.com 
/// 
/// A description of the regular expression: 
/// 
/// Select from 3 alternatives 
///  ^(?:(?:(?:0?[13578]|1[02])(\/|-|\.)31)\1|(?:(?:0?[13-9]|1[0-2])(\/|-|\.)(?:29|30)\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$ 
///   Beginning of line or string 
///   Match expression but don't capture it. [(?:(?:0?[13578]|1[02])(\/|-|\.)31)\1|(?:(?:0?[13-9]|1[0-2])(\/|-|\.)(?:29|30)\2)] 
///    Select from 2 alternatives 
///     (?:(?:0?[13578]|1[02])(\/|-|\.)31)\1 
///      Match expression but don't capture it. [(?:0?[13578]|1[02])(\/|-|\.)31] 
///       (?:0?[13578]|1[02])(\/|-|\.)31 
///        Match expression but don't capture it. [0?[13578]|1[02]] 
///         Select from 2 alternatives 
///          0?[13578] 
///           0, zero or one repetitions 
///           Any character in this class: [13578] 
///          1[02] 
///           1 
///           Any character in this class: [02] 
///        [1]: A numbered capture group. [\/|-|\.] 
///         Select from 3 alternatives 
///          Literal/
///          - 
///          Literal . 
///        31 
///      Backreference to capture number: 1 
///     (?:(?:0?[13-9]|1[0-2])(\/|-|\.)(?:29|30)\2) 
///      Return 
///      New line 
///      Match expression but don't capture it. [(?:0?[13-9]|1[0-2])(\/|-|\.)(?:29|30)\2] 
///       (?:0?[13-9]|1[0-2])(\/|-|\.)(?:29|30)\2 
///        Match expression but don't capture it. [0?[13-9]|1[0-2]] 
///         Select from 2 alternatives 
///          0?[13-9] 
///           0, zero or one repetitions 
///           Any character in this class: [13-9] 
///          1[0-2] 
///           1 
///           Any character in this class: [0-2] 
///        [2]: A numbered capture group. [\/|-|\.] 
///         Select from 3 alternatives 
///          Literal/
///          - 
///          Literal . 
///        Match expression but don't capture it. [29|30] 
///         Select from 2 alternatives 
///          29 
///           29 
///          30 
///           30 
///        Backreference to capture number: 2 
///   Return 
///   New line 
///   Match expression but don't capture it. [(?:1[6-9]|[2-9]\d)?\d{2}] 
///    (?:1[6-9]|[2-9]\d)?\d{2} 
///     Match expression but don't capture it. [1[6-9]|[2-9]\d], zero or one repetitions 
///      Select from 2 alternatives 
///       1[6-9] 
///        1 
///        Any character in this class: [6-9] 
///       [2-9]\d 
///        Any character in this class: [2-9] 
///        Any digit 
///     Any digit, exactly 2 repetitions 
///   End of line or string 
///  ^(?:0?2(\/|-|\.)29\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$ 
///   Beginning of line or string 
///   Match expression but don't capture it. [0?2(\/|-|\.)29\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00)))] 
///    0?2(\/|-|\.)29\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))) 
///     0, zero or one repetitions2 
///     [3]: A numbered capture group. [\/|-|\.] 
///      Select from 3 alternatives 
///       Literal/
///       - 
///       Literal . 
///     29 
///     Backreference to capture number: 3 
///     Match expression but don't capture it. [(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))] 
///      Match expression but don't capture it. [(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00)] 
///       Select from 2 alternatives 
///        (?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26]) 
///         Match expression but don't capture it. [1[6-9]|[2-9]\d], zero or one repetitions 
///          Select from 2 alternatives 
///           1[6-9] 
///            1 
///            Any character in this class: [6-9] 
///           [2-9]\d 
///            Any character in this class: [2-9] 
///            Any digit 
///         Match expression but don't capture it. [0[48]|[2468][048]|[13579][26]] 
///          Select from 3 alternatives 
///           0[48] 
///            0 
///            Any character in this class: [48] 
///           [2468][048] 
///            Any character in this class: [2468] 
///            Any character in this class: [048] 
///           [13579][26] 
///            Any character in this class: [13579] 
///            Any character in this class: [26] 
///        (?:(?:16|[2468][048]|[3579][26])00) 
///         Return 
///         New line 
///         Match expression but don't capture it. [(?:16|[2468][048]|[3579][26])00] 
///          (?:16|[2468][048]|[3579][26])00 
///           Match expression but don't capture it. [16|[2468][048]|[3579][26]] 
///            Select from 3 alternatives 
///             16 
///              16 
///             [2468][048] 
///              Any character in this class: [2468] 
///              Any character in this class: [048] 
///             [3579][26] 
///              Any character in this class: [3579] 
///              Any character in this class: [26] 
///           00 
///   End of line or string 
///  ^(?:(?:0?[1-9])|(?:1[0-2]))(\/|-|\.)(?:0?[1-9]|1\d|2[0-8])\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$ 
///   Beginning of line or string 
///   Match expression but don't capture it. [(?:0?[1-9])|(?:1[0-2])] 
///    Select from 2 alternatives 
///     Match expression but don't capture it. [0?[1-9]] 
///      0?[1-9] 
///       0, zero or one repetitions 
///       Any character in this class: [1-9] 
///     Match expression but don't capture it. [1[0-2]] 
///      1[0-2] 
///       1 
///       Any character in this class: [0-2] 
///   Return 
///   New line 
///   [4]: A numbered capture group. [\/|-|\.] 
///    Select from 3 alternatives 
///     Literal/
///     - 
///     Literal . 
///   Match expression but don't capture it. [0?[1-9]|1\d|2[0-8]] 
///    Select from 3 alternatives 
///     0?[1-9] 
///      0, zero or one repetitions 
///      Any character in this class: [1-9] 
///     1\d 
///      1 
///      Any digit 
///     2[0-8] 
///      2 
///      Any character in this class: [0-8] 
///   Backreference to capture number: 4 
///   Match expression but don't capture it. [(?:1[6-9]|[2-9]\d)?\d{2}] 
///    (?:1[6-9]|[2-9]\d)?\d{2} 
///     Match expression but don't capture it. [1[6-9]|[2-9]\d], zero or one repetitions 
///      Select from 2 alternatives 
///       1[6-9] 
///        1 
///        Any character in this class: [6-9] 
///       [2-9]\d 
///        Any character in this class: [2-9] 
///        Any digit 
///     Any digit, exactly 2 repetitions 
///   End of line or string 
/// 
/// 
/// 
public static Regex regex = new Regex(
     "^(?:(?:(?:0?[13578]|1[02])(\\/|-|\\.)31)\\1|\r\n(?:(?:0?[13-9]"+ 
     "|1[0-2])(\\/|-|\\.)(?:29|30)\\2))\r\n(?:(?:1[6-9]|[2-9]\\d)?\\d"+ 
     "{2})$|^(?:0?2(\\/|-|\\.)29\\3(?:(?:(?:1[6-9]|[2-9]\\d)?(?:0["+ 
     "48]|[2468][048]|[13579][26])|\r\n(?:(?:16|[2468][048]|[3579][2"+ 
     "6])00))))$|^(?:(?:0?[1-9])|(?:1[0-2]))\r\n(\\/|-|\\.)(?:0?[1-9"+ 
     "]|1\\d|2[0-8])\\4(?:(?:1[6-9]|[2-9]\\d)?\\d{2})$", 
    RegexOptions.CultureInvariant 
    | RegexOptions.Compiled 
    ); 

+0

Es wäre interessant zu sehen, was dieses Tool für das Beispiel RE in der Frage ergibt. Ich wollte es versuchen, aber das Herunterladen und Installieren konnte ich nicht stören. Willst du es versuchen? – paxdiablo

+0

Ich habe es sehr schnell versucht. Es mag es nicht sehr, zumindest nicht im Analysator. Es hat gut geparst. Was soll es zusammenbringen? –

+0

Genau, du hast einen Fehler drin ... was machst du?!?!?! – ojblass

1

Ich denke, die Antwort auf regulären Ausdruck Aufrechterhaltung mit Kommentierung oder regex Konstrukten nicht so viel.

Wenn ich mit dem Debuggen des von Ihnen angegebenen Beispiels beauftragt wurde, würde ich mich vor einem Regex-Debug-Tool (wie Regex Coach) hinsetzen und den regulären Ausdruck der Daten durchlaufen, die es verarbeiten soll.

+0

Es wäre interessant zu sehen, was dieses Tool für das Beispiel RE in der Frage ergibt. Ich wollte es versuchen, aber das Herunterladen und Installieren konnte ich nicht stören. Willst du es versuchen? – paxdiablo

+0

Immer noch so viel Böses alles in einer Zeile Code ... Ich bin mir sicher, dass dies in meinem Code ist es einfach nicht wert. – ojblass

+0

Ich werde es versuchen Pax (nette Antwort übrigens, ich bevorzuge deins zu meins;)). Ich denke, dass ich einige Testdaten benötige. ojblass, können Sie einige in der Frage veröffentlichen? – RedBlueThing

7

Einige Leute benutzen REs für die falschen Dinge (ich warte auf die erste SO Frage, wie man ein gültiges C++ Programm mit einem RE findet).

Ich finde normalerweise, dass, wenn ich meine RE nicht innerhalb von 60 Zeichen anpassen kann, es besser ist, ein Stück Code zu sein, da das fast immer lesbarer sein wird.

In jedem Fall, ich immer Dokument, in dem Code, was der RE soll, im Detail zu erreichen. Das liegt daran, dass ich aus bitterer Erfahrung weiß, wie schwer es für jemand anderen (oder sogar mich, sechs Monate später) ist, hereinzukommen und zu versuchen, es zu verstehen.

Ich glaube nicht, dass sie böse sind, obwohl ich glaube, dass einige Leute, die sie benutzen, böse sind (dich nicht ansehend, Michael Ash :-). Sie sind ein großartiges Werkzeug, aber wie eine Kettensäge schneiden Sie Ihre Beine ab, wenn Sie nicht wissen, wie man sie richtig verwendet.

UPDATE: Eigentlich habe ich folgte einfach den Link zu dieser Ungeheuerlichkeit, und es ist m/d/y Datum- zwischen den Jahren 1600 und 9999. Das ist ein klassischer Fall von wo ausgewachsenem Code zu validieren wäre lesbarer und wartbarer.

Sie teilen es einfach in drei Felder auf und überprüfen Sie die einzelnen Werte. Ich würde es fast als Delikt betrachten, das einer Kündigung würdig wäre, wenn einer meiner Schergen mir das gekauft hätte. Ich würde sie sicherlich zurückschicken, um es richtig zu schreiben.

+0

Auch dann bieten die meisten Plattformen Funktionen, die ein Datum für Sie konvertieren können. Wirklich, benutze diese! – strager

+1

Bitte lassen Sie das spezielle Beispiel die Frage nicht trüben. – ojblass

+0

Ich bevorzuge einen Morast von String-Manipulationsfunktionen zu einem Regex. Ich kann lernen, was der Moras im Debugger macht. Ein Regex ist nur eine Blackbox. – jmucchiello

30

Verwenden Sie , die eine hierarchische, englische Gliederung einer Regex gibt.

Oder

Diese tip von Darren Neimke:

.NET regulären Ausdruck ermöglicht Muster mit eingebetteten Kommentare über die RegExOptions.IgnorePatternWhitespace Compiler-Option zu Autor und die (? # ...) Syntax eingebettet in jeder Zeile der Musterzeichenfolge.

Dies ermöglicht psuedo-Code-like Kommentare in jeder Zeile eingebettet werden und die folgende Auswirkung auf Lesbarkeit hat:

Dim re As New Regex (_ 
    "(?<=  (?# Start a positive lookBEHIND assertion) " & _ 
    "(#|@)  (?# Find a # or a @ symbol) " & _ 
    ")   (?# End the lookBEHIND assertion) " & _ 
    "(?=  (?# Start a positive lookAHEAD assertion) " & _ 
    " \w+  (?# Find at least one word character) " & _ 
    ")   (?# End the lookAHEAD assertion) " & _ 
    "\w+\b  (?# Match multiple word characters leading up to a word boundary)", _ 
    RegexOptions.Multiline Or RegexOptions.IgnoreCase Or RegexOptions.IgnoreWhitespace _ 
) 

Hier ist eine andere .NET-Beispiel (erfordert die RegexOptions.Multiline und RegexOptions.IgnorePatternWhitespace Optionen):

static string validEmail = @"\b # Find a word boundary 
       (?<Username>  # Begin group: Username 
       [a-zA-Z0-9._%+-]+ # Characters allowed in username, 1 or more 
       )     # End group: Username 
       @     # The e-mail '@' character 
       (?<Domainname>  # Begin group: Domain name 
       [a-zA-Z0-9.-]+  # Domain name(s), we include a dot so that 
            # mail.somewhere is also possible 
       .[a-zA-Z]{2,4}  # The top level domain can only be 4 characters 
            # So .info works, .telephone doesn't. 
       )     # End group: Domain name 
       \b     # Ending on a word boundary 
       "; 

Wenn Ihre RegEx auf ein allgemeines Problem anwendbar ist, eine andere Die Option besteht darin, sie zu dokumentieren und an RegExLib zu senden, wo sie bewertet und kommentiert wird. Nichts schlägt viele Augenpaare ...

Ein weiteres RegEx-Tool ist The Regulator

+0

inline kommentieren ... gute Tipp – ojblass

+0

Es wäre interessant zu sehen, was dieses Tool für das Beispiel RE in der Frage ergibt. Ich wollte es versuchen, aber das Herunterladen und Installieren konnte ich nicht stören. Willst du es versuchen? – paxdiablo

+0

Nimmt Sekunden zum Herunterladen und Installieren ... –

16

Nun, der ganze Zweck im Leben des PCRE Modifikator/x ist, damit Sie reguläre Ausdrücke mehr leserlich zu schreiben, wie in diesem trivialen Beispiel:

my $expr = qr/ 
    [a-z] # match a lower-case letter 
    \d{3,5} # followed by 3-5 digits 
/x; 
+0

Ich habe Probleme, Dokumentation zu diesem Thema zu finden ... vielleicht verwende ich nicht die richtigen Begriffe oder die richtige Quelle. Kannst du helfen? – ojblass

+0

http://perldoc.perl.org/perlre.html, etwa eine Seite nach unten, Absatz beginnt mit "Der/x-Modifikator selbst benötigt ein wenig mehr Erklärung".Es gibt auch Beispiele, die durch den Rest der Seite verstreut sind. – chaos

+0

(Das ist aka 'man perlre' natürlich.) – chaos

4

I haben gelernt, alle außer der einfachsten Regexp zu vermeiden. Ich bevorzuge andere Modelle wie das String-Scanning von Icon oder Haskells Parsing-Kombinatoren. In beiden Modellen können Sie benutzerdefinierten Code schreiben, der die gleichen Berechtigungen und den gleichen Status wie die integrierten String-Ops besitzt. Wenn ich in Perl programmieren würde, würde ich wahrscheinlich in Perl einige Parsing-Kombinierer einrichten. Ich habe es für andere Sprachen gemacht. Eine sehr schöne Alternative ist die Verwendung von Parsing Expression Grammars, wie Roberto Ierusualimschy es mit seinem LPEG Paket gemacht hat, aber im Gegensatz zu Parser-Kombinatoren ist das etwas, was man an einem Nachmittag nicht auspeitschen kann. Aber wenn jemand bereits PEGs für Ihre Plattform erstellt hat, ist das eine sehr gute Alternative zu regulären Ausdrücken.

+0

Ich komponierte eine Antwort, als Ihre kam; Ich habe dich rausgeschmissen und stattdessen deine vereidigt. – MarkusQ

+0

Jedes Mal, wenn ich etwas anderes als die einfachsten Strings analysiere, mache ich Fehler. – ojblass

+0

@objlass: Aus diesem Grund hasst John Levine handgeschriebene Parser. –

4

Ich habe eine schöne Methode gefunden, einfach den Matching-Prozess in mehrere Phasen aufzuteilen. Es wird wahrscheinlich nicht so schnell ausgeführt, aber Sie haben den zusätzlichen Vorteil, dass Sie auch in der Lage sind, auf einer feineren Körnungsebene zu erkennen, warum die Übereinstimmung nicht auftritt.

Eine andere Route ist LL oder LR Parsing zu verwenden. Einige Sprachen sind nicht als reguläre Ausdrücke ausdrückbar, wahrscheinlich sogar mit Perl's nicht-fsm Erweiterungen.

+0

Die Literatur zu Ihrer Aussage wird einige Studien erfordern. – ojblass

+0

Die Übereinstimmung mit einem feineren Kornniveau ist essentiell. – ojblass

+0

An dieser Stelle kann Perl mit seinen Regexen alles analysieren (sie sind jetzt Turing-Maschinen-kompatibel), aber sie sind nicht die beste Sprache für das Parsen komplexer Strukturen. Parse :: RecDescent ist viel besser. –

4

Wow, das ist hässlich. Es sieht so aus, als ob es funktionieren sollte, modulo ein unvermeidbarer Fehler, der 00 als ein zweistelliges Jahr behandelt (es sollte in einem Viertel der Zeit ein Schaltjahr sein, aber ohne das Jahrhundert kann man nicht wissen, was es sein soll). Es gibt eine Menge Redundanz, die wahrscheinlich in Sub-Regexes einbezogen werden sollte, und ich würde drei Sub-Regexes für die drei Hauptfälle erstellen (das ist mein nächstes Projekt heute Abend). Ich habe auch ein anderes Zeichen für das Trennzeichen verwendet, um zu vermeiden, dass Schrägstriche entkoppelt werden müssen, änderte die einzelnen Zeichenwechsel in Zeichenklassen (was uns glücklich macht, die Periode zu vermeiden) und änderte \d in [0-9], da die erste mit einem Ziffernzeichen übereinstimmt (einschließlich U+1815MONGOLIAN DIGIT FIVE: & # x1815;) in Perl 5.8 und 5.10.

Warnung, ungetesteten Code:

#!/usr/bin/perl 

use strict; 
use warnings; 

my $match_date = qr{ 
    #match 29th - 31st of all months but 2 for the years 1600 - 9999 
    #with optionally leaving off the first two digits of the year 
    ^
    (?: 
     #match the 31st of 1, 3, 5, 7, 8, 10, and 12 
     (?: (?: 0? [13578] | 1[02]) ([/-.]) 31) \1 
     | 
     #or match the 29th and 30th of all months but 2 
     (?: (?: 0? [13-9] | 1[0-2]) ([/-.]) (?:29|30) \2) 
    ) 
    (?: 
     (?:      #optionally match the century 
      1[6-9] |   #16 - 19 
      [2-9][0-9]  #20 - 99 
     )? 
     [0-9]{2}     #match the decade 
    ) 
    $ 
    | 
    #or match 29 for 2 for leap years 
    ^
    (?: 
    #FIXME: 00 is treated as a non leap year 
    #even though 2000, 2400, etc are leap years 
     0?2      #month 2 
     ([/-.])     #separtor 
     29      #29th 
     \3      #separator from before 
     (?:      #leap years 
      (?: 
       #match rule 1 (div 4) minus rule 2 (div 100) 
       (?: #match any century 
        1[6-9] | 
        [2-9][0-9] 
       )? 
       (?: #match decades divisible by 4 but not 100 
        0[48]  | 
        [2468][048] | 
        [13579][26] 
       ) 
       | 
       #or match rule 3 (div 400) 
       (?: 
        (?: #match centuries that are divisible by 4 
         16   | 
         [2468][048] | 
         [3579][26] 
        ) 
        00 
       ) 
      ) 
     ) 
    ) 
    $ 
    | 
    #or match 1st through 28th for all months between 1600 and 9999 
    ^
    (?: (?: 0?[1-9]) | (?:1[0-2])) #all months 
    ([/-.])       #separator 
    (?: 
     0?[1-9] |    #1st - 9th or 
     1[0-9] |    #10th - 19th or 
     2[0-8]     #20th - 28th 
    ) 
    \4        #seprator from before 
    (?:        
     (?:      #optionally match the century 
      1[6-9] |   #16 - 19 
      [2-9][0-9]  #20 - 99 
     )? 
     [0-9]{2}     #match the decade 
    ) 
    $ 
}x; 
+0

Auch in dieser Form würde ich mich mit dem Code immer noch nicht wohl fühlen. – ojblass

+1

Als ich ein Kind war, sagte mir meine Mutter: "Nimm von der Kante und blase". Was für Suppe geht, wird doppelt für Regexes. Nimm sie ein bisschen, bremse und genieße die Stücke. In Kürze wird die Schüssel leer sein. ;-) –

+0

Nein, ich würde es auch nicht benutzen. 70 Zeilen (3 Seiten) Code, der durch einen einzigen Aufruf einer Bibliotheksfunktion ersetzt werden könnte, nein danke. Ich wollte nur sehen, wie es funktioniert. Und jetzt bin ich besessen davon, es einfacher zu machen. –

3

Einige Leute, wenn sie mit einem Problem konfrontiert, denken: "Ich weiß, ich werde reguläre Ausdrücke verwenden." Jetzt haben sie zwei Probleme. - Jamie Zawinski in comp.lang.emacs.

Halten Sie die regulären Ausdrücke so einfach wie möglich (KISS). In Ihrem Datumsbeispiel würde ich wahrscheinlich für jeden Datumstyp einen regulären Ausdruck verwenden.

Oder noch besser, ersetzt es mit einer Bibliothek (d. H. Eine Datum-Parsing-Bibliothek).

Ich würde auch Schritte ergreifen, um sicherzustellen, dass die Eingabequelle einige Einschränkungen (d. H. Nur eine Art von Datumszeichenfolgen, idealerweise ISO-8601) hatte.

Auch

  • Eine Sache, zu der Zeit (mit der möglichen Ausnahme der Extraktion Werte)
  • Erweiterte Konstrukte sind in Ordnung, wenn sie richtig verwendet wird (wie in den Ausdruck simplying und damit die Wartung reduziert)

EDIT:

„erweiterte Konstrukte führen zu Wartung Fragen“

Mein ursprünglicher Punkt war, dass verwendet, wenn richtig es einfacher Ausdrücke führen sollten, nicht mehr schwierigsten. Einfachere Ausdrücke sollten Wartung reduzieren.

Ich habe den obigen Text aktualisiert, um so viel zu sagen.

Ich würde darauf hinweisen, dass reguläre Ausdrücke kaum als fortgeschrittene Konstrukte an und für sich gelten. Nicht mit einem bestimmten Konstrukt vertraut zu sein, macht es nicht zu einem fortgeschrittenen Konstrukt, nur zu einem unbekannten. Das ändert nichts daran, dass reguläre Ausdrücke mächtig, kompakt und - wenn sie richtig verwendet werden - elegant sind. Ähnlich wie ein Skalpell liegt es ganz in den Händen desjenigen, der es ausübt.

+0

ein Problem wird zwei ... aber ich muss argumentieren, dass erweiterte Konstrukte zu Wartungsproblemen führen ... – ojblass

+0

Von allen Aspekten des Codes sehe ich auf reguläre Ausdrücke, Synchronisation und Operater Überladung in dieser Reihenfolge sind die schwersten Probleme zu bekommen zur Ursache, wenn ein Defekt gefunden wird. Sie sind vielleicht nicht fortgeschritten, aber sie sind Lastwagen mit viel Kraft für den Platz, den sie einnehmen. – ojblass

5

Hier ist der gleiche Regex in verdauliche Stücke unterteilt. Zusätzlich dazu, dass sie lesbarer sind, können einige der Sub-Regexes selbst nützlich sein. Es ist auch wesentlich einfacher, die erlaubten Trennzeichen zu ändern.

#!/usr/local/ActivePerl-5.10/bin/perl 

use 5.010; #only 5.10 and above 
use strict; 
use warnings; 

my $sep   = qr{ [/.-] }x;    #allowed separators  
my $any_century = qr/ 1[6-9] | [2-9][0-9] /x; #match the century 
my $any_decade = qr/ [0-9]{2} /x;   #match any decade or 2 digit year 
my $any_year = qr/ $any_century? $any_decade /x; #match a 2 or 4 digit year 

#match the 1st through 28th for any month of any year 
my $start_of_month = qr/ 
    (?:       #match 
     0?[1-9] |    #Jan - Sep or 
     1[0-2]     #Oct - Dec 
    ) 
    ($sep)      #the separator 
    (?: 
     0?[1-9] |    # 1st - 9th or 
     1[0-9] |    #10th - 19th or 
     2[0-8]     #20th - 28th 
    ) 
    \g{-1}      #and the separator again 
/x; 

#match 28th - 31st for any month but Feb for any year 
my $end_of_month = qr/ 
    (?: 
     (?: 0?[13578] | 1[02]) #match Jan, Mar, May, Jul, Aug, Oct, Dec 
     ($sep)     #the separator 
     31      #the 31st 
     \g{-1}     #and the separator again 
     |      #or 
     (?: 0?[13-9] | 1[0-2]) #match all months but Feb 
     ($sep)     #the separator 
     (?:29|30)    #the 29th or the 30th 
     \g{-1}     #and the separator again 
    ) 
/x; 

#match any non-leap year date and the first part of Feb in leap years 
my $non_leap_year = qr/ (?: $start_of_month | $end_of_month) $any_year/x; 

#match 29th of Feb in leap years 
#BUG: 00 is treated as a non leap year 
#even though 2000, 2400, etc are leap years 
my $feb_in_leap = qr/ 
    0?2       #match Feb 
    ($sep)      #the separtor 
    29       #the 29th 
    \g{-1}      #the separator again 
    (?: 
     $any_century?   #any century 
     (?:      #and decades divisible by 4 but not 100 
      0[48]  | 
      [2468][048] | 
      [13579][26] 
     ) 
     | 
     (?:      #or match centuries that are divisible by 4 
      16   | 
      [2468][048] | 
      [3579][26] 
     ) 
     00      
    ) 
/x; 

my $any_date = qr/$non_leap_year|$feb_in_leap/; 
my $only_date = qr/^$any_date$/; 

say "test against garbage"; 
for my $date (qw(022900 foo 1/1/1)) { 
    say "\t$date ", $date ~~ $only_date ? "matched" : "didn't match"; 
} 
say ''; 

#comprehensive test 

my @code = qw/good unmatch month day year leap/; 
for my $sep (qw(/ - .)) { 
    say "testing $sep"; 
    my $i = 0; 
    for my $y ("00" .. "99", 1600 .. 9999) { 
     say "\t", int $i/8500*100, "% done" if $i++ and not $i % 850; 
     for my $m ("00" .. "09", 0 .. 13) { 
      for my $d ("00" .. "09", 1 .. 31) { 
       my $date = join $sep, $m, $d, $y; 
       my $re = $date ~~ $only_date || 0; 
       my $code = not_valid($date); 
       unless ($re == !$code) { 
        die "error $date re $re code $code[$code]\n" 
       } 
      } 
     } 
    } 
} 

sub not_valid { 
    state $end = [undef, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 
    my $date  = shift; 
    my ($m,$d,$y) = $date =~ m{([0-9]+)[-./]([0-9]+)[-./]([0-9]+)}; 
    return 1 unless defined $m; #if $m is set, the rest will be too 
    #components are in roughly the right ranges 
    return 2 unless $m >= 1 and $m <= 12; 
    return 3 unless $d >= 1 and $d <= $end->[$m]; 
    return 4 unless ($y >= 0 and $y <= 99) or ($y >= 1600 and $y <= 9999); 
    #handle the non leap year case 
    return 5 if $m == 2 and $d == 29 and not leap_year($y); 

    return 0; 
} 

sub leap_year { 
    my $y = shift; 
    $y = "19$y" if $y < 1600; 
    return 1 if 0 == $y % 4 and 0 != $y % 100 or 0 == $y % 400; 
    return 0; 
} 
1

gab ich eine question recently about commenting regexes with embedded comments Es gab nützliche Antworten und vor allem ein von @mikej

den Beitrag sehen von Martin Fowler auf ComposedRegex für einige weitere Ideen auf regexp Lesbarkeit zu verbessern. In Zusammenfassung, befürwortet er eine komplexen Regexp in kleinere Teile zerlegen, die sinnvolle Variable Namen gegeben werden können. z.B.