Erstes Phython Script - pyodbc Problem wenn das Ergebnis leer ist

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Antworten
Synthoras
User
Beiträge: 8
Registriert: Freitag 26. Juli 2019, 10:45

Hallo zusammen,

ich habe vor ca. 2 Wochen angefangen mich mit Phython bzw. mit dem Programmieren zu beschäftigen.
Die Idee ist eine Datenbank abzufragen, und ein Ergebnis per Mail zu verschicken, falls es ein Ergebnis gibt. Die Abfragen werden in einer xml gespeichert.
Bisher konnte mir google soweit weiterhelfen und es klappt alles solange die Abfrage auch ein Ergebnis liefert! Wenn nicht, bleibt alles hängen.

Hier stehe ich jetzt auf dem schlauch...
mein Code:

Code: Alles auswählen

import pyodbc 
conn = pyodbc.connect('Driver={SQL Server};'
                      'Server=server;'
                      'Database=dbname;'
                      'Trusted_Connection=yes;')

import xml.etree.ElementTree as ET
tree = ET.parse('C:\\sql-scripts.xml')
root = tree.getroot()

sqlmessage = ''

count = root[0][0].text
count = int(count)

while count > 0:
	select = root[count][0].text
	table = root[count][1].text
	where = root[count][2].text
	error = root[count][3].text
	
	select = "select top 1 ["+select+"] "
	table = "from [dbname].[dbo].["+table+"] "
	where = "where "+where
	sql = select+table+where

	cursor = conn.cursor()
	cursor.execute(sql)

	for row in cursor:
		sqlmessage = ''
		sqlmessage = sqlmessage + row[0] + '  Error:  ' + error

		message = ''
		print(sqlmessage)   
		count = count-1
Den Part mit dem E-Mail versenden habe ich jetzt entfernt.
Die XML dazu:

Code: Alles auswählen

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<SQL-Scripts>
    <Count>
        <Anzahl>2</Anzahl>
    </Count>
    <Daten>
        <Feld>Kundennummer</Feld>
        <from>Kunde</from>
        <where>name = ''</where>
        <error>Name leer</error>
    </Daten>
    <Daten>
        <Feld>Nummer</Feld>
        <from>Artikel</from>
        <where>Kostentraeger = ''</where>
        <error>Kostentraeger leer</error>
    </Daten>
</SQL-Scripts>
Bestimmt alles noch nicht optimal, aber funktional solange es einen Kunden ohne Namen UND einen Artikel ohne Kostenträger gibt.
Sobald aber das SQL-Statement kein Ergebnis liefert, bleibt alles "hängen". Wenn ich das Script dann abbreche erscheint folgendes:
cursor.execute(sql)
KeyboardInterrupt

Das würde ich gerne Abfangen, vor allem sollte auch die Zeile "print(sqlmessage) " nicht ausgeführt werden, wenn das SQL-Ergebnis leer ist.

Hat jemand eine Idee? Leider bin ich mit try nicht weiter gekommen.

Danke & Gruß
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Synthoras: Was da passiert ist im Grunde einfach zu verstehen. Versuch das doch mal in Worten zu erklären was da abläuft und gehe davon aus, dass kein Ergebnis geliefert wird. Welcher Name ist dann zu welchem Zeitpunkt an welchen Wert gebunden? Spiel mal Python und erkläre warum das Deiner Meinung nach *nicht* hängen bleiben sollte.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Synthoras
User
Beiträge: 8
Registriert: Freitag 26. Juli 2019, 10:45

@_blackjack_:
Ehrlich gesagt verstehe ich das nicht so ganz.
Wenn ich das Script im interaktiven Modus schritt für Schritt durchgehe (ohne die "count" Schleife)
Dann bekomme ich nach "cursor.execute(sql)" normal weiter. danach ist in for row in cursor: ... das Ergebnis von Print(row[0]) einfach leer.
Also wäre meine Erwartungshaltung das es sich in der Schleife nicht anders verhält und ich als "print" auch einfach '' erhalten würde oder?

Code: Alles auswählen

>>> cursor.execute(sql)
<pyodbc.Cursor object at 0x000000D9755CD0A8>
>>> for row in cursor:
...     sqlmessage = sqlmessage + row[0] + '  Error:  ' + error
...
>>> sqlmessage
''
>>>
Die Variable wäre nur leer, das könnte man wenn man will im Anschluss auch wieder abfangen.

Danke & Gruß
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Synthoras: Was passiert denn dann beim nächsten Durchlauf der ``while``-Schleife? Was wird dann abgefragt aus der Datenbank?

Mal von dem Logikproblem im Programm abgesehen, ist da auch einiges kritikwürdig.

Eingerückt wird mit vier Leerzeichen pro Ebene.

Importe gehören an den Anfang des Moduls, damit man leicht sehen kann wovon es abhängig ist.

Namen sollte man nicht abkürzen. Wenn `connection` gemeint ist dann schreibt man nicht `conn`. Ist ja nicht so als wenn Buchstaben knapp wären oder man noch Editoren verwenden würde die keine Autovervollständigung bieten.

Wenn man aus `conn` `connection` macht, sieht man warum Quelltext ausrichten an etwas anderem als Einrückebenen doof ist – man muss plötzlich Folgezeilen ändern, nur damit wieder alles sauber aussieht, aber ohne das sich in den Zeilen tatsächlich inhaltlich etwas ändert.

Man muss nicht jedes Zwischenergebnis an einen Namen binden. Schon gar nicht wenn sich dabei auch noch der Datentyp des Wertes ändert.

Sowohl die Datenbankverbindung als auch den Cursor sollte man nach Verwendung ordentlich schliessen. Die ``with``-Anweisung und `contextlib.closing()` sind dabei hilfreich das auch sicherzustellen.

Die erste Zuweisung an `sqlmessage` wird nirgends verwendet, die kann man ersatzlos streichen.

Die unurchsichtigen Indexzugriffe in das XML-Dokument machen den Code nicht nur schwerer verständlich, sondern sind auch fragil, wenn sich an der Form des XML mal etwas ändern sollte. In XML gibt es Tagnamen und Struktur – orientiere Dich daran.

Statt ``root[0][0].text`` würde man ``root.find('Count/Anzahl').text`` schreiben und schon wüsste der Leser was man da aus dem Dokument holt. Und es ist sogar unabhängig davon wo <Count> innerhalb des Wurzelelements steht oder ob es vor <Anzahl> vielleicht noch ein Element innerhalb von <Count> gibt. Wobei die Schachtelung von <Anzahl> in <Count> was ja auch Anzahl heisst, etwas merkwürdig ist. Was soll das? Zudem braucht man diese Information ja gar nicht, denn die Anzahl der <Daten>-Elemente ist ja offensichtlich, eben die Anzahl der <Daten>-Elemente. Die Information ist also redundant und kann ja sogar falsch sein. Die würde man nur zur Überprüfung verwenden, sonst ist die uninteressant.

Dieses zusammenstückeln der SQL-Anweisung per ``+`` ist eher BASIC als Python. In Python gibt es dafür Zeichenkettenformatierung mit der `format()`-Methode und ab Python 3.6 f-Zeichenkettenliterale.

Auch später im Code ist das initialisieren von `sqlmessage` mit einer leeren Zeichenkette sinnlos. Und `message` wird definiert, aber nirgends verwendet.

Ich komme dann (ungetestet) ungefähr bei so etwas an:

Code: Alles auswählen

#!/usr/bin/env python3
from contextlib import closing
import xml.etree.ElementTree as ET

import pyodbc

DB_CONFIG = (
    'Driver={SQL Server};'
    'Server=server;'
    'Database=dbname;'
    'Trusted_Connection=yes;'
)
SQL_SCRIPTS_FILENAME = r'C:\sql-scripts.xml'


def main():
    root = ET.parse(SQL_SCRIPTS_FILENAME).getroot()
    
    with closing(pyodbc.connect(DB_CONFIG)) as connection:
        for daten in root.iterfind('Daten'):
            with closing(connection.cursor()) as cursor:
                column_name = daten.find('Feld')
                table_name = daten.find('from')
                condition = daten.find('where')
                
                cursor.execute(
                    f'SELECT TOP 1 [{column_name}]'
                    f' FROM [dbname].[dbo].[{table_name}]'
                    f' WHERE {condition}'
                )
                for row in cursor:
                    print(row[0], ' Error: ', daten.find('error'))


if __name__ == '__main__':
    main()
Bei der XML-Datei ist es komisch das manche Tagnamen mit einem Grossbuchstaben anfangen und andere komplett klein geschrieben sind. Und die Mischung Deutsch/Englisch ist auch nicht gut.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Synthoras
User
Beiträge: 8
Registriert: Freitag 26. Juli 2019, 10:45

@_blackjack_:
Danke für deine Hilfe und Kritik, ich stehe ja noch sehr am Anfang und gelobe Besserung.
Trotzdem würde ich mein Logikproblem im Programm gerne verstehen.

An sich hole ich ja über:

Code: Alles auswählen

count = root[0][0].text
count = int(count)
Nur die 2 ab. Klar, zählen lassen wäre besser, aber an sich weiß ich ab dem Zeitpunkt doch, das ich insgesamt 2 SQL-Scripte ausführen möchte.

Code: Alles auswählen

while count > 0:
	select = root[count][0].text
	table = root[count][1].text
	where = root[count][2].text
	error = root[count][3].text
...
	print(sqlmessage)   
	count = count-1
	
Hier fange ich also bei 2 an, also mit dem letzten Script und baue danach BASIC like die Abfrage.
Er macht die Abfrage, gibt mir das Ergebnis aus, Zählt Count um 1 runter und fängt wieder von oben an.
Also mit 1, in den letzten Durchlauf.

Wo habe ich hier das Logikproblem, wenn die Abfrage "leer" ist, dann hätte ich nunmal eine leere Zeile, oder Antwort erwartet.

Danke nochmal!

Tante Edit sagt ich schulde hier noch andere antworten:
__blackjack__ hat geschrieben: Freitag 26. Juli 2019, 12:38 @Synthoras: Was passiert denn dann beim nächsten Durchlauf der ``while``-Schleife? Was wird dann abgefragt aus der Datenbank?
Das meinte ich oben, die 2 Durchläufe.
__blackjack__ hat geschrieben: Freitag 26. Juli 2019, 12:38 Namen sollte man nicht abkürzen. Wenn `connection` gemeint ist dann schreibt man nicht `conn`. Ist ja nicht so als wenn Buchstaben knapp wären oder man noch Editoren verwenden würde die keine Autovervollständigung bieten.
Das liegt einfach daran, dass man bei Oracle meistens conn statt connection verwendet. Für mich ist das einfach nicht abgekürzt.

__blackjack__ hat geschrieben: Freitag 26. Juli 2019, 12:38 Auch später im Code ist das initialisieren von `sqlmessage` mit einer leeren Zeichenkette sinnlos. Und `message` wird definiert, aber nirgends verwendet.
Das liegt jetzt an der Kürzung des Mail parts.

Ich versuche den Code von dir auf jeden Fall mal anzupassen/zu erweitern, und auch die anderen Dinge zu beherzigen.
Nur für mich noch als Frage, warum 4 Lesezeichen und kein Tab?
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Synthoras: Du hast jetzt beschrieben was Du denkst was die ``while``-Schleife macht. Das macht sie aber nicht. Sagen wir die erste (also count = 2) Abfrage liefert kein Ergebnis. Dann wird im nächsten ``while``-Schleifendurchlauf welche Abfrage gemacht? Also nicht was Du erwartest, sondern was da tatsächlich passiert! Welchen Wert hat `count` dann, und warum *wenn die vorherige Abfrage leer war*!

Einrückung ist in Python wichtig, unterschiedlich einrücken kann Fehler verursachen, das heisst jeder sollte gleich einrücken. Es könnten auch alle mit Tab einrücken, aber es rücken *alle* mit vier Leerzeichen ein. Wer das nicht tut stinkt, ist doof, oder Bayern München Fan. 😜

Gegen Tabs spricht das die keine Breite haben, mein Tab ist anders als dein Tab, Tab in Browser ist anders als Tab in Editor A, ist anders als Tab in Editor B, ist anders als Tab in Konsole, ist anders als Tab in E-Mail-Client…

Ausserdem sieht man die nicht, und man sieht auch nicht zwingend wenn Tabs und Leerzeichen gemischt werden wodurch dann richtig *aussehender* Code, de fakto falsch eingerückt sein kann.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Synthoras
User
Beiträge: 8
Registriert: Freitag 26. Juli 2019, 10:45

@_blackjack_:
Also bleibt er immer auf 2 und ich lande in ner Endlosschleife?
Das warum ist mir dabei nicht klar.

Danke für die Erklärung, sauber sind also nur 4 Leerzeichen, verstanden!

Gruß
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Synthoras: Warum sollte `count` *nicht* auf zwei bleiben? Es wird ja nicht herunter gezählt.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Synthoras
User
Beiträge: 8
Registriert: Freitag 26. Juli 2019, 10:45

@_blackjack_:
Ich wäre davon ausgegangen das ein leeres Ergebnis immer noch ein Ergebnis ist.
Daher auch die Schleife voll durchlaufen wird.
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Synthoras: Was heisst denn ”voll durchlaufen”? Wie oft? Und welchen Wert hätte `row` dann? Welchen Wert hätte der Ausdruck ``row[0]`` dann?
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Synthoras
User
Beiträge: 8
Registriert: Freitag 26. Juli 2019, 10:45

@_blackjack_:
Ich dachte die Abfrage wird gestellt.
Ergebnis row[0] ist leer und dann geht er weiter und macht count-1
Also row[0] = ‘‘
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Synthoras: Und wie würdest Du dann ein leeres Ergebnis von einem unterscheiden was genau einen Datensatz liefert bei dem ``row[0]`` die leere Zeichenkette als Wert hat? Das ist ja ein legitimer Wert den man auch in der Datenbank speichern kann.

Was passiert ist das die Abfrage gestellt wird und der Cursor halt keine Datensätze liefert wenn keine Datensätze vorhanden sind auf die das SELECT passt. Und aus irgendwelchen Gründen erwartest Du, dass der Cursor einen Datensatz liefert wenn keine passenden Datensätze vorhanden sind, der dann willkürlich eine leere Zeichenkette als Spaltenwert hat. Was ja grundsätzlich bei CHAR, VARCHAR, TEXT, … auch ein gültiger Wert sein kann, der genau so in der Datenbank steht.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Synthoras
User
Beiträge: 8
Registriert: Freitag 26. Juli 2019, 10:45

@_blackjack_:

Deine Erklärung ist absolut logisch. Ich glaube ich hatte die "Idee" weil ich bei einem test mal für print(row[0]) '' als Ergebnis hatte. Aber eventuell war das einfach ein Fehler von mir.
__blackjack__ hat geschrieben: Freitag 26. Juli 2019, 18:03 Und wie würdest Du dann ein leeres Ergebnis von einem unterscheiden was genau einen Datensatz liefert bei dem ``row[0]`` die leere Zeichenkette als Wert hat?
Genau das würde ich gern wissen. Wie unterscheide ich es. Bzw wie kann ich das Abfangen?
Ich habe versucht dein Script zu verwenden. Laufe hier aber leider auch noch auf einen Fehler.

Code: Alles auswählen

Traceback (most recent call last):
  File "bj-sql.py", line 36, in <module>
    main()
  File "bj-sql.py", line 27, in main
    f'SELECT TOP 1 [{column_name}]'
pyodbc.ProgrammingError: ('42000', "[42000] [Microsoft][ODBC SQL Server Driver][
SQL Server]Falsche Syntax in der Nähe von '<'. (102) (SQLExecDirectW)")
Gruß
Synthoras
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Upsi, ich habe bei den `find()`\s am Ende das Abfragen des `text`-Attributs vergessen. Da werden die Tags in die SQL-Abfrage formatiert.

Wenn man *kein* Ergebnis hat, dann liefert der Cursor keine Datensätze. Wenn man sowieso nur maximal einen Datensatz als Ergebnis erwartet, dann ist eine Schleife etwas irreführend – dann kann man einfach einmal `next()` aufrufen und entweder auf das `StopIteration` reagieren, oder das zweite Argument von `next()` verwenden um testen zu können ob man einen Datensatz oder beispielsweise `None` bekommt, wenn man das übergibt.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Synthoras
User
Beiträge: 8
Registriert: Freitag 26. Juli 2019, 10:45

@_blackjack_: Super, danke! Dein Script läuft jetzt.

Danke & Gruß
Synthoras
Antworten