Lernen Sie, wie man einen Mini SQL Parser in Python schreibt (Schritt für Schritt)
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Implementierung eines einfachen SQL-Parsers in Python: Von Prinzipien zur Praxis
Im Bereich der Datenverarbeitung ist SQL (Structured Query Language) ein zentrales Werkzeug für die Interaktion mit Datenbanken. Aber haben Sie sich jemals gefragt, wie ein Programm eine Anweisung wie SELECT id, name FROM users WHERE age > 18
versteht, wenn Sie sie schreiben? Dahinter steckt die Kraft eines SQL-Parsers – er konvertiert menschenlesbaren SQL-Text in strukturierte Daten (wie einen abstrakten Syntaxbaum), die ein Programm ausführen kann.
SQL-Parser werden häufig in ORM-Frameworks (z. B. Django ORM), SQL-Auditing-Tools (z. B. Sqitch) und Datenbank-Middleware (z. B. Sharding-JDBC) verwendet. Ausgehend von den Parsing-Prinzipien führt Sie dieser Artikel durch die Implementierung eines einfachen SQL-Parsers in Python, der die wichtigsten SQL-Anweisungen unterstützt (SELECT ... FROM ... WHERE
), und hilft Ihnen, die Kernlogik von Parsern zu verstehen.
I. Der Kern des SQL-Parsing: Zwei Schlüsselphasen
Das Parsing jeder Sprache – einschließlich SQL – umfasst zwei wesentliche Phasen: „lexikalische Analyse“ und „syntaktische Analyse“. Wir können diesen Prozess mit dem „Lesen eines englischen Satzes“ vergleichen: zuerst einzelne Wörter identifizieren (lexikalische Analyse), dann die grammatikalische Struktur des Satzes verstehen (syntaktische Analyse).
1. Lexikalische Analyse: Aufteilen in „Wörter“
Das Ziel der lexikalischen Analyse ist es, fortlaufenden SQL-Text in einzelne Tokens (Lexeme) mit klarer Bedeutung zu zerlegen. Tokens sind wie die „Wörter“ von SQL. Zum Beispiel:
- SQL-Anweisung:
SELECT id, name FROM users WHERE age > 18
- Token-Sequenz nach dem Aufteilen:
[SELECT, ID(id), COMMA, ID(name), FROM, ID(users), WHERE, ID(age), GT(>), INT(18)]
Zu den gängigen SQL-Token-Typen gehören:
- Schlüsselwörter:
SELECT
,FROM
,WHERE
(Groß- und Kleinschreibung wird nicht beachtet) - Bezeichner: Tabellennamen (z. B.
users
), Spaltennamen (z. B.id
) - Literale: Zahlen (
18
), Zeichenketten ('Alice'
) - Operatoren:
=
(Gleichheit),>
(größer als),<
(kleiner als) - Interpunktion:
,
(Komma),*
(Sternchen, das alle Spalten darstellt)
2. Syntaktische Analyse: Konstruieren der „Satzstruktur“
Die syntaktische Analyse verwendet SQL-Grammatikregeln (z. B. „Auf SELECT
müssen Spaltennamen oder *
folgen, und auf FROM
muss ein Tabellenname folgen“), um die Token-Sequenz in einen Abstract Syntax Tree (AST) zu konvertieren. Ein AST ist eine Baumstruktur, die die logische Hierarchie der SQL-Anweisung klar darstellt. Zum Beispiel kann der AST der obigen SQL-Anweisung wie folgt vereinfacht werden:
Query
├─ select_clause: [id, name]
├─ from_clause: users
└─ where_clause:
├─ column: age
├─ operator: >
└─ value: 18
Der Wert eines AST liegt in seiner Fähigkeit, unstrukturierten Text in strukturierte Daten zu konvertieren. Durch das Durchlaufen des AST kann ein Programm auf einfache Weise Schlüsselinformationen abrufen, z. B. „welche Spalten abzufragen sind“, „aus welcher Tabelle abzufragen ist“ und „welche Filterbedingungen anzuwenden sind“.
II. Praxis: Implementierung eines einfachen SQL-Parsers in Python
Wir werden den Parser mit der Python-Bibliothek ply
(Python Lex-Yacc) implementieren. ply
ist eine Bibliothek, die die klassischen Compiler-Konstruktionstools lex
(für die lexikalische Analyse) und yacc
(für die syntaktische Analyse) simuliert. Es ist einfach, damit anzufangen, und es stimmt eng mit der Kernlogik von Parsern überein.
1. Umgebung vorbereiten
Installieren Sie zuerst die Bibliothek ply
:
pip install ply
2. Schritt 1: Implementieren Sie den lexikalischen Analysator (Lexer)
Der Kern eines lexikalischen Analysators ist die Verwendung regulärer Ausdrücke, um verschiedene Arten von Tokens zu finden und irrelevante Zeichen wie Leerzeichen und Kommentare zu ignorieren.
Code-Implementierung (Lexer)
import ply.lex as lex # 1. Definieren Sie Token-Typen (müssen zuerst definiert werden, um Fehler zu vermeiden) tokens = ( 'SELECT', 'FROM', 'WHERE', # Schlüsselwörter 'ID', 'INT', 'STRING', # Bezeichner und Literale 'EQ', 'GT', 'LT', # Operatoren (Gleichheit, größer als, kleiner als) 'COMMA', 'STAR' # Interpunktion (Komma, Sternchen) ) # 2. Definieren Sie Übereinstimmungsregeln für Schlüsselwörter (höhere Priorität als Bezeichner, da Schlüsselwörter auch aus Buchstaben bestehen) reserved = { 'select': 'SELECT', 'from': 'FROM', 'where': 'WHERE' } # 3. Definieren Sie reguläre Ausdrücke für Tokens (geordnet nach Priorität von höchster zu niedrigster) # Zeichenkettenliterale: in einzelne Anführungszeichen eingeschlossen, z. B. 'Alice' def t_STRING(t): r"'[^']*'" # Regex: entspricht jedem Zeichen (außer einzelnen Anführungszeichen) innerhalb einzelner Anführungszeichen t.value = t.value[1:-1] # Entfernen Sie die einzelnen Anführungszeichen, um den tatsächlichen Inhalt beizubehalten return t # Ganzzahlliterale: Ziffernfolgen def t_INT(t): r'\d+' t.value = int(t.value) # Konvertieren in den ganzzahligen Typ return t # Bezeichner (Tabellennamen, Spaltennamen): beginnen mit einem Buchstaben, gefolgt von Buchstaben/Ziffern/Unterstrichen def t_ID(t): r'[a-zA-Z_][a-zA-Z0-9_]*' # Überprüfen Sie, ob der Bezeichner ein Schlüsselwort ist (z. B. 'select' sollte als SELECT und nicht als ID erkannt werden) t.type = reserved.get(t.value.lower(), 'ID') return t # Operatoren t_EQ = r'=' # Gleichheit t_GT = r'>' # Größer als t_LT = r'<' # Kleiner als # Interpunktion t_COMMA = r',' # Komma t_STAR = r'\*' # Sternchen (muss maskiert werden, da * in Regex eine besondere Bedeutung hat) # 4. Ignorieren Sie irrelevante Zeichen (Leerzeichen, Tabstopps, Zeilenumbrüche) t_ignore = ' \t\n' # 5. Fehlerbehandlung (ausgelöst, wenn nicht erkennbare Zeichen auftreten) def t_error(t): print(f"Illegales Zeichen: '{t.value[0]}'") t.lexer.skip(1) # Überspringen Sie das ungültige Zeichen und fahren Sie mit dem Analysieren des nachfolgenden Inhalts fort # 6. Erstellen Sie eine Lexer-Instanz lexer = lex.lex() # Testen Sie den Lexer: Geben Sie SQL-Text ein und geben Sie die Token-Sequenz aus def test_lexer(sql): lexer.input(sql) print("Lexikalisches Analyseergebnis (Token-Sequenz):") while True: tok = lexer.token() if not tok: break print(f"Typ: {tok.type:10}, Wert: {tok.value}") # Testfall test_sql = "SELECT id, name FROM users WHERE age > 18 AND name = 'Alice'" test_lexer(test_sql)
Laufergebnis
Lexikalisches Analyseergebnis (Token-Sequenz):
Typ: SELECT , Wert: select
Typ: ID , Wert: id
Typ: COMMA , Wert: ,
Typ: ID , Wert: name
Typ: FROM , Wert: from
Typ: ID , Wert: users
Typ: WHERE , Wert: where
Typ: ID , Wert: age
Typ: GT , Wert: >
Typ: INT , Wert: 18
Typ: ID , Wert: AND # Hinweis: Das AND-Schlüsselwort ist noch nicht definiert, daher wird es vorübergehend als ID erkannt
Typ: ID , Wert: name
Typ: EQ , Wert: =
Typ: STRING , Wert: Alice
3. Schritt 2: Implementieren Sie den syntaktischen Analysator (Parser)
Der Kern eines syntaktischen Analysators ist die Definition von SQL-Grammatikregeln und die Konvertierung der Token-Sequenz in einen AST. Wir werden die wichtigste Abfragesyntax unterstützen:
SELECT [column_list/*] FROM table_name [WHERE condition (column operator value)]
Code-Implementierung (Parser)
import ply.yacc as yacc from lexer import tokens # Importieren Sie die in Schritt 1 definierten Token-Typen # 1. Definieren Sie AST-Knoten (dargestellt durch Dictionaries für Einfachheit und Übersichtlichkeit) def create_ast(node_type, **kwargs): return {'type': node_type, **kwargs} # 2. Definieren Sie Grammatikregeln (geordnet nach Priorität von niedrigster zu höchster; die Startregel ist 'query') # Startregel: Abfrageanweisung = SELECT-Klausel + FROM-Klausel + [WHERE-Klausel] def p_query(p): '''query : select_clause from_clause where_clause_opt''' # p[0] ist der Rückgabewert der Regel; p[1] ist select_clause, p[2] ist from_clause, p[3] ist where_clause_opt p[0] = create_ast( 'Query', select=p[1], from_clause=p[2], where_clause=p[3] if p[3] else None # Optionale Klausel; auf None setzen, wenn sie nicht existiert ) # Optionale WHERE-Klausel: entweder vorhanden oder nicht vorhanden def p_where_clause_opt(p): '''where_clause_opt : WHERE condition | empty''' if len(p) == 3: # Entspricht "WHERE condition" p[0] = p[2] else: # Entspricht "empty" (keine WHERE-Klausel) p[0] = None # SELECT-Klausel: SELECT + (Sternchen / Spaltenliste) def p_select_clause(p): '''select_clause : SELECT STAR | SELECT column_list''' if p[2] == '*': # Entspricht "SELECT *" p[0] = create_ast('SelectClause', columns=['*']) else: # Entspricht "SELECT column_list" p[0] = create_ast('SelectClause', columns=p[2]) # Spaltenliste: mehrere IDs, die durch Kommas getrennt sind (z. B. id, name, age) def p_column_list(p): '''column_list : ID | column_list COMMA ID''' if len(p) == 2: # Einzelne Spalte (z. B. id) p[0] = [p[1]] else: # Mehrere Spalten (z. B. column_list, ID) p[0] = p[1] + [p[3]] # FROM-Klausel: FROM + Tabellenname (z. B. FROM users) def p_from_clause(p): '''from_clause : FROM ID''' p[0] = create_ast('FromClause', table=p[2]) # Bedingung: Spalte + Operator + Wert (z. B. age > 18 oder name = 'Alice') def p_condition(p): '''condition : ID EQ INT | ID EQ STRING | ID GT INT | ID LT INT''' p[0] = create_ast( 'Condition', column=p[1], operator=p[2], value=p[3] ) # Leere Regel (wird für optionale Klauseln verwendet) def p_empty(p): '''empty :''' p[0] = None # Syntaxfehlerbehandlung def p_error(p): if p: print(f"Syntaxfehler: In der Nähe von Token {p.type} (Wert: {p.value})") else: print("Syntaxfehler: Unerwartetes Eingabeende") # Erstellen Sie eine Parser-Instanz parser = yacc.yacc() # Testen Sie den Parser: Geben Sie SQL-Text ein und geben Sie den AST aus def parse_sql(sql): ast = parser.parse(sql) print("\nSyntaktisches Analyseergebnis (AST):") import json # Verwenden Sie JSON, um die Ausgabe für bessere Lesbarkeit zu formatieren print(json.dumps(ast, indent=2)) # Testfälle (unterstützen Sternchen, mehrere Spalten und INT/STRING-Bedingungen) test_sql1 = "SELECT id, name FROM users WHERE age > 18" test_sql2 = "SELECT * FROM orders WHERE product = 'phone'" parse_sql(test_sql1) parse_sql(test_sql2)
Laufergebnis (AST)
AST des ersten Testfalls (SELECT id, name FROM users WHERE age > 18
):
{ "type": "Query", "select": { "type": "SelectClause", "columns": ["id", "name"] }, "from_clause": { "type": "FromClause", "table": "users" }, "where_clause": { "type": "Condition", "column": "age", "operator": ">", "value": 18 } }
AST des zweiten Testfalls (SELECT * FROM orders WHERE product = 'phone'
):
{ "type": "Query", "select": { "type": "SelectClause", "columns": ["*"] }, "from_clause": { "type": "FromClause", "table": "orders" }, "where_clause": { "type": "Condition", "column": "product", "operator": "=", "value": "phone" } }
III. Anwenden des Parsing-Ergebnisses: Der Wert von AST
Sobald Sie das AST haben, können Sie viele Dinge damit tun. Sie können beispielsweise einen einfachen „Abfrageinterpreter“ schreiben, um das AST in eine natürliche Sprachbeschreibung zu konvertieren:
def interpret_ast(ast): if ast['type'] != 'Query': return "Nicht unterstützter Anweisungstyp" # Analysieren Sie die SELECT-Klausel select_cols = ', '.join(ast['select']['columns']) select_desc = f"Abfragespalten: {select_cols}" # Analysieren Sie die FROM-Klausel from_desc = f"Aus Tabelle: {ast['from_clause']['table']}" # Analysieren Sie die WHERE-Klausel where_desc = "" if ast['where_clause']: cond = ast['where_clause'] where_desc = f", Filterbedingung: {cond['column']} {cond['operator']} {cond['value']}" return f"Ausführungslogik: {select_desc} {from_desc}{where_desc}" # Testen Sie den Interpreter ast1 = parser.parse(test_sql1) print(interpret_ast(ast1)) # Ausgabe: Ausführungslogik: Abfragespalten: id, name Aus Tabelle: users, Filterbedingung: age > 18
IV. Einschränkungen und fortgeschrittene Richtungen
Der in diesem Artikel implementierte Parser unterstützt nur die grundlegendste SQL-Syntax und hat offensichtliche Einschränkungen:
- Er unterstützt keine komplexe Syntax wie Multi-Tabellen-Joins (
JOIN
), Aggregatfunktionen (COUNT
,SUM
) und Gruppierungen (GROUP BY
); - Er unterstützt keine semantische Analyse (z. B. Überprüfen, ob Tabellen/Spalten vorhanden sind oder ob Datentypen übereinstimmen);
- Er behandelt keine Sonderfälle wie SQL-Kommentare und gemischte Groß- und Kleinschreibung (z. B.
Select
).
Für praktischere SQL-Parsing-Funktionen können Sie sich an den folgenden fortgeschrittenen Richtungen orientieren:
- Verwenden Sie ausgereifte Bibliotheken: Priorisieren Sie für industrielle Szenarien die Verwendung von Bibliotheken wie
sqlparse
(Python) oderantlr4
(Sprachübergreifend).sqlparse
kann komplexe SQL direkt analysieren und ASTs generieren; - Erweitern Sie Grammatikregeln: Fügen Sie Regeln für
JOIN
,GROUP BY
usw. basierend aufply
hinzu. Achten Sie auf die Grammatikprioritäten (z. B. hatAND
eine höhere Priorität alsOR
); - Semantische Analyse: Fügen Sie nach der syntaktischen Analyse einen Schritt zur „Tabellenstrukturprüfung“ hinzu, um zu überprüfen, ob die Spalten in der
SELECT
-Klausel in der in derFROM
-Klausel angegebenen Tabelle vorhanden sind; - Abfrageoptimierung: Optimieren Sie Abfragen basierend auf dem AST (z. B. Herunterdrücken von Filterbedingungen, Auswählen von Indizes) – dies ist eine Kernfunktion von Datenbankkerneln.
V. Schlussfolgerung
Das Wesen des SQL-Parsings ist die „Konvertierung von Text in strukturierte Daten“, die auf zwei Kernschritten beruht: „Aufteilen in Tokens durch lexikalische Analyse“ und „Erstellen eines AST durch syntaktische Analyse“. Obwohl der in diesem Artikel mithilfe von ply
implementierte einfache Parser für Produktionsumgebungen nicht ausreicht, kann er Ihnen helfen, die Funktionsweise von Parsern zu verstehen.
Leapcell: Das Beste vom Serverless-Webhosting
Abschließend empfehlen wir eine ausgezeichnete Plattform zum Bereitstellen von Python-Diensten: Leapcell
🚀 Erstellen Sie mit Ihrer Lieblingssprache
Entwickeln Sie mühelos in JavaScript, Python, Go oder Rust.
🌍 Stellen Sie unbegrenzt Projekte kostenlos bereit
Zahlen Sie nur für das, was Sie verwenden – keine Anfragen, keine Gebühren.
⚡ Pay-as-You-Go, keine versteckten Kosten
Keine Leerlaufgebühren, nur nahtlose Skalierbarkeit.
📖 Entdecken Sie unsere Dokumentation
🔹 Folgen Sie uns auf Twitter: @LeapcellHQ