SyntaxExpressions

    • Beta

    Es gibt 9 Antworten in diesem Thema. Der letzte Beitrag () ist von Agita.

      SyntaxExpressions

      Haupt-Projekt "SyntaxExpressions"
      Soll das Parsen von nahezu jeder Script-Sprache ermöglichen,
      in dem bis hin zu den kleinsten Bausteinen alles auseinander
      genommen und wieder individuell zusammen gefügt werden kann.

      Unter-Projekt "CodeShredder" (Step 1)
      Script anhand von Regex-Pattern gezielt in alle Bausteine zerlegen.
      Liest den SourceCode aus einem String oder einer existierenden Datei
      und gibt eine Auflistung mit allen gefundenen Bausteinen,
      sowie deren Start- und End-Positionen zurück.

      CodeShredder v1.1.1 (07.08.2012)
      - Einleitung
      - SHRED und NOSHRED
      - Komplexe Verschachtelungen
      Dateien
      • CodeShredder.dll

        (15,87 kB, 105 mal heruntergeladen, zuletzt: )
      • CodeShredder.vb

        (9,14 kB, 201 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 11 mal editiert, zuletzt von „Agita“ ()

      CodeShredder - Einleitung

      Zu erst muss der Shredder initialisiert werden

      VB.NET-Quellcode

      1. Dim myShredder = new CodeShredder()

      Als Consolen-Anwendung gibt es die Debug-Option

      VB.NET-Quellcode

      1. myShredder.Debug = True

      Nun müssen die Muster hinzugefügt werden, nach denen gesucht werden soll.
      Der Aufruf der Funktion ist: AddPattern(Name, RegexPattern, RegexOptions)

      VB.NET-Quellcode

      1. myShredder.AddPattern("IfThenEndif", _
      2. " (?<SHRED> " & _
      3. " \b(?<Word>\w+)\b " & _
      4. " ) ", _
      5. CodeShredderOptions.IgnoreCase _
      6. Or CodeShredderOptions.SingleLine _
      7. Or CodeShredderOptions.MultiLine)
      Das benutzte Pattern (Muster) ist ganz simpel und sucht nach allen normalen Wörtern.

      Einfachste Art einen Code zu laden:

      VB.NET-Quellcode

      1. Dim Source As String = "if a then if b then c endif endif"
      2. Dim myShredderResult As ShredderResult = myShredder.ResultFromString(Source)
      3. ' oder
      4. Dim myShredderResult As ShredderResult = myShredder.ResultFromFile("MyPath/MyFile.txt")

      "myShredderResult" besitzt 3 Variablen auf die zugegriffen werden kann:
      ".OriginalText" - hat den den Code gespeichert, wie er vor dem Zerlegen war.
      ".ResultText" - ist der Code der nach dem Zerlegen übrig geblieben ist.
      ".Matches" - ist letzt endlich die Auflistung aller gefundener Bausteine.

      ".Matches" ist eine Sortedlist mit einem Integer als Schlüssel.
      Dieser Schlüssel ist zugleich die Position im Code an der der Baustein anfängt.

      Die Werte der Sortedlist haben den Typ "ShredderMatch". Dieser Typ hat folgende Member:
      ".Name" - Der Name des Musters mit dem dieser Baustein gefunden wurde.
      ".Range" - Bietet Angaben über Start und End-Position des Gesamten Bausteins.
      ".Groups" - Hier befinden sich Schließlich alle gefundenen Gruppen.

      Eine gefundene Gruppe ist vom Typ "ShredderGroup" und hat folgende Member:
      ".Name" - Der Name der Gruppe
      ".Value" - Inhalt der Gruppe
      ".Range" - Wie oben auch.

      Jedoch kurz zurück zum Hinzufügen der Muster.
      In unserem Falle hat das Muster den Namen "IfThenEndif".
      Die einzelnen Zeilen des Pattern bedeuten:

      VB.NET-Quellcode

      1. (?<SHRED>
      2. ' Gruppe mit dem Namen "SHRED" beginnt.
      3. ' Dies ist wichtig, denn alles was innerhalb dieser Gruppe
      4. ' gefunden wird, wird gespeichert und im "ResultText"
      5. ' mit leerem Raum ersetzt

      VB.NET-Quellcode

      1. \b(?<Word>\w+)\b
      2. ' Dies findet ein normales Wort und Speichert es
      3. ' in den Gruppen unter dem Namen "Word" ab.
      4. ' Auch dieses Wort wird in "ResultText" mit
      5. ' leerem Raum ersetzt.

      VB.NET-Quellcode

      1. )
      2. ' Dies beendet lediglich die SHRED-Gruppe


      Das Script arbeitet also nur mit "ResultText", geht alle zuvor eingegebenen Pattern durch, speichert die gefundenen Sachen, ersetzt den Teil in "ResultText" mit Space und sucht dann weiter.

      Nehmen wir dazu unseren Test Code von ganz oben: "if a then if b then c endif endif"
      Nach und nach werden alle Wörter gefunden. Das sieht in etwa so aus:

      Quellcode

      1. if a then if b then c endif endif
      2. a then if b then c endif endif
      3. then if b then c endif endif
      4. if b then c endif endif
      5. b then c endif endif
      6. then c endif endif
      7. c endif endif
      8. endif endif
      9. endif

      In "myShredderResult.Matches" stecken also nun die 9 Bausteine.
      Sie alle haben den Namen "IfThenEndif". Logisch, denn sie alle wurden mir diesem Muster gefunden.

      Die zugehörigen Schlüssel (Da es sich um eine Sortedlist handelt) sind folgende:
      myShredderResult.Matches.Keys(0) = 0
      myShredderResult.Matches.Keys(1) = 3
      myShredderResult.Matches.Keys(2) = 5
      myShredderResult.Matches.Keys(3) = 10
      myShredderResult.Matches.Keys(4) = 13
      myShredderResult.Matches.Keys(5) = 15
      myShredderResult.Matches.Keys(6) = 20
      myShredderResult.Matches.Keys(7) = 22
      myShredderResult.Matches.Keys(8) = 28

      Wir erinnern uns an das Muster mit dem Pattern. Dort werden
      die Wörter ebenfalls gespeichert durch: \b(?<Word>\w+)\b

      Jedes Match besitzt somit je eine Gruppe. Und jede davon hat somit den Namen "Word".
      Die Werte für die Gruppen sind:
      myShredderResult.Matches.Values(0).Groups("Word").Item(0).Value = "if"
      myShredderResult.Matches.Values(1).Groups("Word").Item(0).Value = "a"
      myShredderResult.Matches.Values(2).Groups("Word").Item(0).Value = "then"
      myShredderResult.Matches.Values(3).Groups("Word").Item(0).Value = "if"
      myShredderResult.Matches.Values(4).Groups("Word").Item(0).Value = "b"
      myShredderResult.Matches.Values(5).Groups("Word").Item(0).Value = "then"
      myShredderResult.Matches.Values(6).Groups("Word").Item(0).Value = "c"
      myShredderResult.Matches.Values(7).Groups("Word").Item(0).Value = "endif"
      myShredderResult.Matches.Values(8).Groups("Word").Item(0).Value = "endif"

      Und das war es auch schon. Mehr macht der Shredder nicht,
      aber das was er macht, macht er bestens und in einer Art,
      die auf allzu jeden Syntax anwendbar ist.

      Dieser Beitrag wurde bereits 11 mal editiert, zuletzt von „Agita“ ()

      CodeShredder - SHRED und NOSHRED

      Nehmen wir als Beispiel folgendes Muster:

      VB.NET-Quellcode

      1. TestShredder.AddPattern("IfThenEndif", _
      2. " (?:.*) " & _
      3. " (?<SHRED> " & _
      4. " \bif\b " & _
      5. " (?<if>(?<NOSHRED>.*?)) " & _
      6. " \bthen\b " & _
      7. " (?<NOSHRED>.*?) " & _
      8. " \bendif\b " & _
      9. " ) ", _
      10. CodeShredderOptions.IgnoreCase _
      11. Or CodeShredderOptions.SingleLine _
      12. Or CodeShredderOptions.MultiLine)


      Die Gruppe von SHRED wird komplett im ResultText mit leerem Raum überschrieben. Dies kann manchmal ungewollt Auswirkungen haben. Denn die Blöcke für die If-Anweisung und die Then-Befehle können noch Sachen beinhalten die weiter zerlegt werden müssen. Sie müssen also irgendwie im ResultText bleiben, sodass nur "if", "then" und "else" gefunden und ersetzt werden.

      Diese möglichkeit gibt uns NOSHRED. Es gibt an, dass der Inhalt dieser Gruppe wieder zurück geschrieben werden soll, damit er evtl weiter zerlegt werden kann. Es ist auch kein Problem, den Inhalt dieser NOSHRED-Gruppen trotzdem speichern zu lassen.

      (?<if>(?<NOSHRED>.*?))
      Speichert in dem Match des Types "IfThenEndif" eine Gruppe mit dem Namen "if" und den Inhalt dieser Gruppe. Gleich darauf befindet sich darin die NOSHRED Gruppe.

      (?<NOSHRED>.*?)
      Dies jedoch speichert nichts und gibt sofort an, dass der Inhalt wieder dahin soll wo er her kam.
      CodeShredder - Komplexe Verschachtelungen

      Wärend ich dieses Beispiel erarbeitete stieß ich auf einen kleinen Bug, der behoben wurde.
      Für dieses Beispiel ist v1.1.1 oder höher erforderlich.

      Dieses Beispiel zeigt, wie es möglich ist sogar schwierige Sachen wie if-then-elseif-elseif-elseif-else-endif zerlegen zu lassen.

      Das einzige benutze Muster

      Quellcode

      1. TestShredder.AddPattern("IfThenEndif", _
      2. " (?:.*) " & _
      3. " (?<SHRED> " & _
      4. " \bif\b " & _
      5. " (?<if>(?<NOSHRED>.*?)) " & _
      6. " \bthen\b " & _
      7. " (?<NOSHRED>.*?) " & _
      8. " (?(?=\belseif\b) " & _
      9. " \belseif\b " & _
      10. " (?<elseif>(?<NOSHRED>.*?)) " & _
      11. " \bthen\b " & _
      12. " (?<NOSHRED>.*?) " & _
      13. " |)*? " & _
      14. " (?(?=\belse\b) " & _
      15. " \belse\b " & _
      16. " (?<NOSHRED>.*?) " & _
      17. " |) " & _
      18. " \bendif\b " & _
      19. " ) ", _
      20. CodeShredderOptions.IgnoreCase _
      21. Or CodeShredderOptions.SingleLine _
      22. Or CodeShredderOptions.MultiLine)

      Hier der Test Code

      Quellcode

      1. if a then
      2. if b then
      3. else
      4. endif
      5. if c then
      6. elseif d then
      7. elseif e then
      8. elseif f then
      9. else
      10. endif
      11. endif


      Und hier die Ausgabe

      Quellcode

      1. Matches: 3
      2. Match: IfThenEndif
      3. Start:
      4. AllPosition: 2
      5. LineIndex: 1
      6. LinePosition: 0
      7. End:
      8. AllPosition: 135
      9. LineIndex: 14
      10. LinePosition: 5
      11. Groups: 1
      12. Group: if
      13. Value: a
      14. Start:
      15. AllPosition: 4
      16. LineIndex: 1
      17. LinePosition: 2
      18. End:
      19. AllPosition: 7
      20. LineIndex: 1
      21. LinePosition: 5
      22. Match: IfThenEndif
      23. Start:
      24. AllPosition: 17
      25. LineIndex: 3
      26. LinePosition: 2
      27. End:
      28. AllPosition: 43
      29. LineIndex: 5
      30. LinePosition: 7
      31. Groups: 1
      32. Group: if
      33. Value: b
      34. Start:
      35. AllPosition: 19
      36. LineIndex: 3
      37. LinePosition: 4
      38. End:
      39. AllPosition: 22
      40. LineIndex: 3
      41. LinePosition: 7
      42. Match: IfThenEndif
      43. Start:
      44. AllPosition: 49
      45. LineIndex: 7
      46. LinePosition: 2
      47. End:
      48. AllPosition: 126
      49. LineIndex: 12
      50. LinePosition: 7
      51. Groups: 2
      52. Group: elseif
      53. Value: d
      54. Start:
      55. AllPosition: 68
      56. LineIndex: 8
      57. LinePosition: 8
      58. End:
      59. AllPosition: 71
      60. LineIndex: 8
      61. LinePosition: 11
      62. Group: elseif
      63. Value: e
      64. Start:
      65. AllPosition: 85
      66. LineIndex: 9
      67. LinePosition: 8
      68. End:
      69. AllPosition: 88
      70. LineIndex: 9
      71. LinePosition: 11
      72. Group: elseif
      73. Value: f
      74. Start:
      75. AllPosition: 102
      76. LineIndex: 10
      77. LinePosition: 8
      78. End:
      79. AllPosition: 105
      80. LineIndex: 10
      81. LinePosition: 11
      82. Group: if
      83. Value: c
      84. Start:
      85. AllPosition: 51
      86. LineIndex: 7
      87. LinePosition: 4
      88. End:
      89. AllPosition: 54
      90. LineIndex: 7
      91. LinePosition: 7
      Dateien
      • MainModule.vb

        (7,41 kB, 139 mal heruntergeladen, zuletzt: )
      • test.txt

        (137 Byte, 117 mal heruntergeladen, zuletzt: )
      Frage 1, die ich mir gerade stelle:

      Die gefunden Sachen werden in ShredderResult.Matches gespeichert
      und zwar immer (int)StartPos => (ShredderMatch)Match

      Das hat die Vorteile, dass man zum Durchgehen eine "For"-Loop anstelle einer "For Each"-Loop benutzen kann und somit sofort Zugriff auf ShredderResult.Matches.Keys(i) (StartPos) und ShredderResult.Matches.Values(i) (Match) hat.
      Ausserdem ist alles der Reihe nach geordnet.

      Schwierig wird es nur wenn man mehrere Muster hat und zB nur die Matches von "IfThenElse" wissen will. Dann muss man trotzdem alles einmal durchlaufen und die gefundenen Matches zwischen speichern.

      Wäre es also besser
      ShredderResult.Matches
      von
      Sortedlist(Of Integer, ShredderMatch)
      in
      Sortedlist(Of String, List(Of ShredderMatch))
      zu ändern?

      Dann kann man sofort das was man sucht per ShredderResult.Matches("IfThenElse") finden. Allerdings ist dann nichts mehr nach der Reihe sortiert...

      Es kommt auch darauf an, welcher Weg besser ist um die Bausteine weiter verarbeiten zu können. Und da komme ich auch schon zu meiner Frage 2, die mir im Kopf rumschwirrt...

      Ursprünglich hatte ich geplant eine Klasse pro Aufgabe zu erstellen. "Shredder" sollte nur dafür gedacht sein, um den Originalcode in alle kleinsten Teile zu zerlegen um sie mit der nächsten geplanten Klasse "Sweeper" wieder vereinzelt aufzusammeln und in hierarchische Form zu bringen.

      Ist es von der Projektgestalltung her akzeptabel oder wäre es sinnvoller eine Klasse für beide Schritte zu benutzen? Fakt ist, dass der Algorithmus von "Shredder" so wie er ist durchlaufen muss, damit die Sachen verarbeitet werden können. Es gbt also keine Möglichkeit Teile aus dem geplanten "Sweeper" mit in die Schleifen von "Shredder" einzubauen.
      ist das nicht ziemlich inperformant RegEx zu verwenden? RegEx 1x im Code zu verwenden ist schon aufwändig, dann aber noch so oft? Da doch lieber den Parser komplett selbst schreiben...
      Hmmm wegen irgend einem doofen Fehler darf ich alles nochmal schreiben -.- Okay... LetzeGo...

      Regex ist nur so schnell wie man es benutzt, bzw arbeiten lässt. Habe sogar erlebt, dass man mit nur einem Pattern alles lahmlegen kann, aber auch Situationen in denen seeeeeeeehr lange Texte recht schnell und effizient durchsucht werden.

      Hab beim Bearbeiten eines Posts den Text wohl gelöscht, aber... In dem Text stand, dass ich nicht sage, dass dies die beste Lösung wäre, nicht die schnellste, nicht die schlauste. Aber es gibt die Möglichkeit durch nur ein paar Pattern den Syntax anzupassen.

      Klar kann man auch einen eigenen Parser für schnelle sachen basteln. Ich selbst hatte mal einen HTML-Parser der nen kompletten Stammbaum zurückgab, mit Fehlerüberprüfung (Da es sich um ein Bot-Projekt für ein Mmorpg handelte xD). Und ich kann dir sagen, dass die Variante Substring-Für-Substring-auswerten genauso aufwendig ist... Gibt aber noch die Möglichkeit nur Zeile für Zeile zu bearbeiten, was allerdings nicht überall anwendbar ist. Selbst wenn... Müsste ich dann mein eigenes Muster-Erkennungssystem basteln um Syntax-übergreifendes Parsing zu gewärleisten. Ich würde dann nichts anderes machen als... Regex neu erfinden. Und das ist meiner einung nach auch doof :P
      Die Frage ist aber auch, was man möchte. Was man haben möchte und was man machen möchte und wie man es möchte.

      Entstanden ist dieses Projekt eigentlich dadurch, dass ich aus Langeweile mal wieder ein Warcraft3 Map-Projekt starten wollte und dort mit der gegebenen Scriptsprache arbeiten muss. Dazu wollte ich mir ein Tool schreiben, dass das normale MapScript und geschriebene UserScripts vereint. Dazu bräuchte ich allerdings 2 Parser. einen für das Original, einen für die UserScripts. Dann viel mir ein, dass es bereits diverse Tools mit UserScriptSyntax gibt, die mir allerdings nicht gefallen, wesswegen ich mein eigenes schreiben wollte. Sollte ich diese Syntaxe Dritter ebenfalls supporten? Dann bräuchte ich dafür ja ebenfalls je einen eigenen Parser. Und da begann dieses Projekt. Ein einziger Parser, der immer gleich arbeitet, immer das selbe liefert und nur durch Pattern "eingestellt" werden muss.

      Selbst wenn es von der Benutzung her ablehnend sein sollte, so stelle ich immer noch den SourceCode bereit um damit vielleicht jemandem helfen zu können. Dies ist auch nicht mehr die aktuellste Version. In meiner Momentanen befinden sich zahlreiche Bugfixes und ich versuche gerade vom Aufbau der Schleife(n) das ganze zu beschleunigen. Doof nur, dass meine Kenntnisse in solchen .NET Sachen noch nicht so groß sind, muss also erst diverse Sachen ausprobieren =)

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „Agita“ ()

      Derzeitiger Status:
      ShredderV2 ist in Arbeit und wird erst dann veröffentlicht, wenn auch der Sweeper fertig ist. Denn alleine mit dem jetzigen können viele bestimmt noch nicht viel anfangen =) Der Shredder hat alleine die Aufgabe den Code zu zerlegen, der Sweeper sammelt dann die Bausteine wieder systematisch(!) auf.

      Es bleibt bei der Idee mit 2 Schritten und je einer Klasse, denn somit kann beides unabhängig von einander entwickelt und verbessert werden. Da der Code erst bis aufs letzte zerlegt werden MUSS ist es unmöglich alles in einem Schritt zu tun.

      Desweiteren bleibe ich bei der Idee Regex zu benutzen. Derzeitiger Test mit 7000 Zeichen und knapp 400 Zeilen wird, fast problem los, schneller als erwartet zerlegt. Fast problemlos, denn einziges Problem ist derzeit folgendes:

      Pattern die nach Verschachtelungen suchen, bei denen aber von vornherein oder am Ende nichts mehr gefunden wird, verursachen einen extremen Lag, je größer der Code, desto noch größerer der Lag.

      Ein Pattern beispielsweise ist:

      VB.NET-Quellcode

      1. shTemplate.AddPattern("GlobalsBlock", _
      2. " .* " & _
      3. " (?<SHRED> " & _
      4. " ^[^\S\n]*?globals[^\S\n]*?$ " & _
      5. " (?<NOSHRED>.*?) " & _
      6. " ^[^\S\n]*?endglobals[^\S\n]*?$ " & _
      7. " ) " & _
      8. " ", _
      9. ShredderOptions.IgnoreCase _
      10. Or ShredderOptions.SingleLine _
      11. Or ShredderOptions.MultiLine)

      Klein, aber oho... Das Problem liegt in der ersten Zeile bei .* Dies wird benutzt, um auch wirklich Verschachtelungen von innen nach außen zu finden. Ansonsten könnte auch folgendes gefunden werden:

      Quellcode

      1. globals
      2. globals
      3. globals
      4. ...
      5. endglobals


      Das .* sucht gierig, womit die ersten beiden "globals" ausgelassen werden. Allerdings ist die Gier auch die Ursache die den Lag verursacht, denn es wird so lange hin und her gesucht bis Regex wirklich sicher ist dass nichts mehr zu finden ist.
      Stellen wir uns vor, wir haben 3 Zeilen:

      Quellcode

      1. aaa
      2. aaa
      3. aaa
      Und nun suchen wir immer nach 2 Reihen mit "aaa", egal was davor, dazwischen und danach kommt. In diesem Falle bekommen wir diese 3 Treffer:

      Quellcode

      1. aaa <- Zeile1
      2. aaa <- Zeile2
      3. aaa

      Quellcode

      1. aaa <- Zeile1
      2. aaa
      3. aaa <- Zeile2

      Quellcode

      1. aaa
      2. aaa <- Zeile1
      3. aaa <- Zeile2
      Bei 4 Zeilen mit "aaa" wären es dann schon 6 Treffer. Und bei 700 Zeilen?

      Idee zur Problem Behebung? Nein, ich bleibe bei Regex, sowie ich bei dem .* bleibe. Derzeitige Idee ist ein Wrapper für Regex der Timeout unterstützt. Allein 200ms würden als Teimout reichen, nein, ich würde sogar sagen, dass 200ms schon viel zu viel sind. Denn selbst meine derzeitig 7000 Zeichen werden so schnell geparst, dass ich die Debug-Nachrichten in der Console nicht mal lesen kann.

      Edit: Gerade gelesen, dass NET4.5 bereits Timeout unterstützt

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Agita“ ()

    • Benutzer online 1

      1 Besucher

    • 2 Benutzer haben hier geschrieben