subprocess.Popen debuggen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
moinmoin
User
Beiträge: 14
Registriert: Samstag 14. Februar 2009, 00:30

Hallo!

Folgender code steht zur Debatte:

Code: Alles auswählen

Popen(['wget', '-np', '-U', agent, '-O', 'tmp_source', springer_source]).wait()
`springer_source` ist ein Argument, das die übergeordnete Funktion als Argument bekommt. `agent` ist ein String in einem String (weil die strings aus der obigen Liste einmal evaluiert werden - das vermute ich als Fehlerursache, denn der Aufruf liefert die wget Fehlermeldung "scheme missing"), der meinen Browser imitiert.

Es soll also so aussehen (und das funktioniert auch von der kommandozeile aus):

Code: Alles auswählen

wget -np -U 'hier der agent-string in single (double) quotes' -O tmp_source http://irgendeine.seite
Im Grunde geht es mir darum zu sehen was GENAU als subprocess aufgerufen wird, also welcher Aufruf konkret generiert wird, damit ich ihn mit dem gewünschten vergleichen kann... konnte in der Dokumentation für subprocess nichts finden.

Irgendwelche Tips / Hinweise? Hoffe es ist einigermaßen klar...
ichisich
User
Beiträge: 134
Registriert: Freitag 1. Januar 2010, 11:52

Wenn mich nicht alles täuscht kannst Du den kompletten String, samt Parameter auch vorher zusammenbauen und ihn dann an Subrocess übergeben.
Dann weißt Du zumindest ob der String so auschaut wie Du es dir vorstellst.

Gruß
moinmoin
User
Beiträge: 14
Registriert: Samstag 14. Februar 2009, 00:30

Danke ichisich!

Durch den Versuch den string vorher zusammenszustellen, habe ich folgendes entdeckt:

Code: Alles auswählen

'\\"Mozilla/5.0 ... Firefox/3.6.12\\"' 
wird zu

Code: Alles auswählen

'\\"Mozilla/5.0 ... Firefox/3.6.12\\"' 
und:

Code: Alles auswählen

'\"Mozilla/5.0 ... Firefox/3.6.12\"' 
wird zu

Code: Alles auswählen

'"Mozilla/5.0 ... Firefox/3.6.12"' 
Wie erhalte ich aber nun wirklich diesen String:

Code: Alles auswählen

'\"Mozilla/5.0 ... Firefox/3.6.12\"' 
Werde weiter suchen, komme aber im Moment nicht dahinter...
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Ich würde mit eher erstmal ``springer_source`` anzeigen lassen, wget meint ja das die URL nicht stimmt. Das was ichistich schreibt kannst du beruhigt mal vergessen, das ist genau der falsche Ansatz.

Und um den User-Agent brauchst du ja auch keine Double-Quotes. Denn diese brauchst du ja nur in der *Shell* um zu sagen dass es *ein* Argument ist. Aber in subprocess gibst du ja schon *ein* Argument an, da ist doch zusätzliches Quoten totaler Quatsch:

Code: Alles auswählen

agent = 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101117 Firefox/3.6.12'
Popen([.., '-U', agent, ..]).wait()
ist also vollkommen ausreichend.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
ichisich
User
Beiträge: 134
Registriert: Freitag 1. Januar 2010, 11:52

Leonidas hat geschrieben:Das was ichistich schreibt kannst du beruhigt mal vergessen, das ist genau der falsche Ansatz.
Danke hört man gerne ...

Schön wäre eine Erklärung warum !?

Abgeshen davon wars zum Debuggen eine von vermutlich mehreren Möglichkeiten und nen Schritt weiter ist er ja auch gekommen. Falsch ist relativ ....
lunar

@ichisich: Weil Du bei allem Respekt vollkommenen Unsinn erzählt hast.

Übergibt man subprocess eine Liste, so baut subprocess daraus keinesfalls intern eine Zeichenkette zusammen, sondern übergibt diese Liste vielmehr direkt an den Systemaufruf zum Starten der Prozesse. Wenn man zwecks Debugging nun vorher eine Zeichenkette zusammenbaut, dann erhält man schlicht etwas, was beim eigentlichen Aufruf gar nicht existiert.

Mit einer einzelnen Zeichenkette kann man subprocess nämlich nur dann nutzen, wenn man mittels "shell=True" eine Shell dazwischen schalten. Das aber führt den Sinn von subprocess in den meisten Fällen ad absurdum, denn subprocess verwendet man ja vor allem, um eben keine lästige Shell dazwischen zu haben. Vor allem aber ist das zum Debuggen vollkommen ungeeignet, weil es im Zweifelsfall eher neue Fehler erzeugt, anstatt Klarheit über die alten zu verschaffen. Man muss dann nämlich auf all die lästigen Eigenheiten der Shell Rücksicht nehmen, insbesondere korrekt maskieren, was beim Aufruf mit einer Liste nicht nötig ist.

Der einzig sinnvolle Schritt, den der OP zuerst tun kann, ist die komplette Liste unverändert auszugeben, e.g.

Code: Alles auswählen

cmd = ['wget', '-np', '-U', agent, '-O', 'tmp_source', springer_source]
print(cmd)
subprocess.call(cmd)
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

ichisich hat geschrieben:Schön wäre eine Erklärung warum !?
Gut, ich wollt das nicht extra schreiben weil das recht lang ist zum erklären. Aber dann mach ich das einmal, dann kann ich drauf linken.

Also, Prozesse werden unter Unix gestartet indem erstmal fork() aufgerufen wird, was den aktuellen Prozess *kopiert* und dann im Kindprozess exec() aufgerufen wird, dass den Prozess durch einen neuen ersetzt. Wenn man sich die Parameter der ``exec*()``-Funktionen anschaut, insbesondere ``arg0`` dann sieht man dort, dass das ein Character-Array ist. Also werden die Aufrufparameter an den Kernel nicht als String mit Quotes und sowas übergeben sondern ein Array von Strings. In jedem String ist genau *ein* Parameter. Wer schon mal in C programmiert hat wird da den ``argv``-Parameter der ``main``-Funktion wiedererkennen, denn genau dort werden vom Kernel die Parameter schließlich weitergeleitet.

Gut, also um Prozesse aufzurufen, wissen wir nun, werden Arrays übergeben. Eine naive Shell würde dem User genau diese Möglichkeit geben, ein Aufruf von ``wget`` könnte etwa so aussehen:

Code: Alles auswählen

$ ["wget", "-U", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101117 Firefox/3.6.12", "http://www.python-forum.de/"]
Das wäre der direkteste Weg, aber man stellt schnell fest, dass das sehr umständlich ist, gerade dass man jeden Parameter extra quoten muss. Man könnte doch eine Shell erfinden, die eine etwas einfache Syntax besitzt bei der einfach die Einträge in dem Array durch Leerzeichen getrennt werden, statt mit Kommas und Doublequotes. Die eckigen Klammern kann man sich ja auch sparen, also rufen wir mal auf:

Code: Alles auswählen

$ wget -U Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101117 Firefox/3.6.12 http://www.python-forum.de/
Was die Shell dann zu

Code: Alles auswählen

$ ["wget", "-U", "Mozilla/5.0", "(X11;", "U;", "Linux", "x86_64;", "en-US;", "rv:1.9.2.12)", "Gecko/20101117", "Firefox/3.6.12", "http://www.python-forum.de/"]
expandieren würde. Uh-oh! Da stimmt was nicht, plötzlich bekommt ``wget`` ja viel zu viele Argumente! Also müssen wir der Shell beibringen dass der eine String eben nicht an den Leerzeichen getrennt werden soll. Dazu könnten wir den String in Quotes stellen:

Code: Alles auswählen

$ wget -U "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101117 Firefox/3.6.12" http://www.python-forum.de/
würde dann von der Shell geparst werden als:

Code: Alles auswählen

$ ["wget", "-U", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101117 Firefox/3.6.12", "http://www.python-forum.de/"]
Das sieht schon ziemlich gut aus. Man sieht auch, dass die Programme die aufgerufen werden, also etwa ``wget`` den Useragent ohne Quotes übermittelt bekommen, denn die Quotes sind nur für die Shell gedacht.

Wenn wir nun Subprocess nutzen haben wir die Möglichkeit einen String anzugeben, der dann von einer Shell ausgeführt wird. Dazu müssen wir den String zusammenbauen und Quoten und so und die Shell aufrufen. Die Shell macht dann nichts anderes als das ganze Quoting wieder rückgängig zu machen und das Programm mit einem Array aufzurufen. Daher kann man in ``Popen`` auch *direkt* eine Liste mit Parametern angeben, so dass da *nichts* gequotet werden muss, da man ``argv``/``arg0`` direkt so übergibt.

Was ich auch schon gesehen habe als Fehler war, dass die Leute die Schalter sowie deren Werte als *einen* Parameter übergeben:

Code: Alles auswählen

$ wget "-U Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101117 Firefox/3.6.12" http://www.python-forum.de/
dieser Aufruf ist falsch, da das -U und der Useragent zwei separate Argumente sind. Wenn man das so macht wird es zu:

Code: Alles auswählen

$ ["wget", "-U Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101117 Firefox/3.6.12", "http://www.python-forum.de/"]
ausgewertet, aber ``wget`` kennt so einen Schalter in dem Spaces vorkommen nicht.

Etwas anderes ist, wenn man die lange Form verwendet:

Code: Alles auswählen

$ wget "--user-agent=Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101117 Firefox/3.6.12" http://www.python-forum.de/
wird zu

Code: Alles auswählen

$ ["wget", "--user-agent=Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101117 Firefox/3.6.12", "http://www.python-forum.de/"]
und damit kann ``wget`` umgehen, weil erwartet wird dass ``--user-agent=`` und der Wert als *ein* Parameter übergeben wird.

Deswegen ist ichistichs Ansatz des Debuggens über die die Stringausgabe Quatsch, da man für die String-Variante ja extra quoten *muss* (damit die Shell weiß was sie in das Array packen muss), was man bei der Array-Variante gar *nicht darf*. Also macht man sich damit das Leben nur noch schwerer. Sinnvoller wäre es, zu überlegen wie der Shell-Aufruf in "Array-Syntax" aussehen würde, wie ich hier in dem Post vorgemacht habe.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
ichisich
User
Beiträge: 134
Registriert: Freitag 1. Januar 2010, 11:52

Das hat meine Erwartungen übertroffen ....
Danke für die Mühe !
moinmoin
User
Beiträge: 14
Registriert: Samstag 14. Februar 2009, 00:30

Haha! Danke ersteinmal auch an die Veteranen lunar und Leonidas!

Ich habe einen Fall aufgeklärt...

Kleiner Erfahrungsbericht:

Nach dem Tip von lunar habe ich folgendes wiederum versucht:

Code: Alles auswählen

Popen(['wget', '-np', '-U', agent, '-O', 'tmp_source', springer_source])
Da das nicht funktioniert hat, habe ich nach Lesen des posts von Leonidas - der für mich übrigens sehr informativ war, da ich dachte `Popen` würde auch nur aus den Listenelementen einen string basteln, der dann an die shell weitergereicht würde, und nicht direkt an den Kernel (der Hinweis mit argv war einerseits passend, andererseits traumatisch...) - erst einmal spasshalber folgendes per hardcoding versucht:

Code: Alles auswählen

Popen(['wget -np -U \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.04 (lucid) Firefox/3.6.12\" -O tmp_source http://www.irgendeine.seite'], shell=True)
Das lief wie erwartet, also dachte ich: "gut, dann eben über die shell"... als ich dann aber

Code: Alles auswählen

Popen(['wget -np -U \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.04 (lucid) Firefox/3.6.12\" -O tmp_source ' + springer_source], shell=True)
versucht habe, hat das dann zu folgender Fehlermeldung geführt:

Code: Alles auswählen

Non-ASCII character '\xef' in file ./springer2pdf_v02.py on line 80, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
dann habe ich nach lunars tip

Code: Alles auswählen

cmd = ['wget', '-np', '-U', agent, '-O', 'tmp_source', springer_source]
print(cmd)
versucht und als output

Code: Alles auswählen

['wget', '-np', '-U', 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.04 (lucid) Firefox/3.6.12', '-O', 'tmp_source', '\xef\xbb\xbfhttp://www.irgendeine.seite/\n']
erhalten. Da war mir natürlich alles klar. Ich hole springer_source als Zeilenobjekt in die Methode, und dessen string-repräsentation sieht dann wohl wie oben aus... Jetzt muss ich nur noch herausfinden, wie ich entweder nur den string an die Methode übergebe, oder quasi `in-place` innerhalb der Methode `strippen` kann.

Die sauberste Lösung wäre in dem Fall wohl nicht das Zeilenobjekt, sondern den fertigen link als string zu übergeben.

War eine lustige Spurensuche. Ich bleibe dran...
syntor
User
Beiträge: 88
Registriert: Donnerstag 2. Dezember 2010, 03:56

Das sieht nach UTF-8/BOM aus. Der Inhalt kommt wohl aus einer Datei, ja?
Das wid wohl auch der Grund sein, warum das erste nicht funktioniert hat.
moinmoin
User
Beiträge: 14
Registriert: Samstag 14. Februar 2009, 00:30

@syntor:
Danke für Deinen Beitrag. Das ist richtig, wird aus einem file gelesen, welches utf-8 encoded ist. Durch googlen bin ich auch auf dieses BOM gestossen. Dazu habe ich auch das `codecs` modul gefunden. mal sehen was es da so gibt...
syntor
User
Beiträge: 88
Registriert: Donnerstag 2. Dezember 2010, 03:56

You need to be careful when using the BOM and UTF-8. Frankly, I think this is a bug in Python, but what do I know. Python will decode the value of the BOM into a Unicode character, instead of ignoring it. For example (tested with Python 2.3):

>>> codecs.BOM_UTF16.decode( "utf16" )
u''
>>> codecs.BOM_UTF8.decode( "utf8" )
u'\ufeff'

For UTF-16, Python decoded the BOM into an empty string, but for UTF-8, it decoded it into a character. Why is there a difference? I think the UTF-8 decoder should do the same thing as the UTF-16 decoder and strip out the BOM. However, it doesn't, so you will probably need to detect it and remove it yourself, like this:

Code: Alles auswählen

import codecs
if s.beginswith( codecs.BOM_UTF8 ):
	# The byte string s begins with the BOM: Do something.
	# For example, decode the string as UTF-8
	
if u[0] == unicode( codecs.BOM_UTF8, "utf8" ):
	# The unicode string begins with the BOM: Do something.
	# For example, remove the character.

# Strip the BOM from the beginning of the Unicode string, if it exists
u.lstrip( unicode( codecs.BOM_UTF8, "utf8" ) )
BOM führt übrigens an vielen Orten zu problemen. So weit ich weiss, ist BOM aber nur bei UTF-16 zwingend, bei UTF-8 nicht wirklich, es wird "lediglich" dazu verwendet anzugeben, ob die Datei UTF-8 ist. Sie könnte aber gerade so gut ISO-8859 sein oder sowas ;)
Zuletzt geändert von syntor am Freitag 3. Dezember 2010, 20:02, insgesamt 1-mal geändert.
moinmoin
User
Beiträge: 14
Registriert: Samstag 14. Februar 2009, 00:30

obwohl ich es gerne selber versuche, weil ich diese Lernmethode am besten finde, danke für deinen code...

Das liefert mir wie im Kommentar beschrieben aber "nur" einen unicode string. Was ich brauche, ist ein file, das keinen BOM mehr enthält, auf das ich dann ein `readlines()` laufen lassen will, um eine Liste mit den links zu erhalten...
syntor
User
Beiträge: 88
Registriert: Donnerstag 2. Dezember 2010, 03:56

Gute Einstellung :)
(Hättest es auch ignorieren können bis du selbst was gefunden hattest :P)

Die BOM wird durch deinen Texteditor gesetzt. Je nachdem was das für einer ist, kannst du das an verschiedenen Orten ein- bzw ausschalten.
moinmoin
User
Beiträge: 14
Registriert: Samstag 14. Februar 2009, 00:30

Hehe, ich bin halt auch sehr neugierig und meine Augen sind schnell, da ist der spoiler garantiert!

Ärgerlich: Ich habe die Datei von einem windows-user erhalten, sein Texteditor hat den BOM gesetzt. Mein geliebter GNU Emacs würde so etwas nie tun...

Die Ironie ist, dass ich dadurch einiges über Python und Unix gelernt habe...

Danke an alle, skript läuft gerade!
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

moinmoin hat geschrieben:Das liefert mir wie im Kommentar beschrieben aber "nur" einen unicode string. Was ich brauche, ist ein file, das keinen BOM mehr enthält, auf das ich dann ein `readlines()` laufen lassen will, um eine Liste mit den links zu erhalten...
``codecs.open`` macht genau sowas.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
moinmoin
User
Beiträge: 14
Registriert: Samstag 14. Februar 2009, 00:30

Danke Leonidas, habe Deinen post erst jetzt gesehen (Seite 2...)

Werde ich mir merken... da es sich um ein perönliches Projekt handelt, brauche ich den utf-8 check und ggf. codecs.open derzeit nicht. Aber immer gut eine Referenz zu haben...
Rekrul
User
Beiträge: 78
Registriert: Dienstag 7. Dezember 2010, 16:23

Hallo,

Code: Alles auswählen

subprocess.list2cmdline(args)
zeigt dir genau an wie der generierte Befehl aussieht. Scheint aber nichtmehr von Bedeutung zu sein, wollte ich aber dennoch vollständigkeitshalber erwähnen.
lunar

@Rekrul: Das gilt nur für Windows.
Rekrul
User
Beiträge: 78
Registriert: Dienstag 7. Dezember 2010, 16:23

Oh... das hat man davon wenn man ständig unter Windows programmieren muss. :|

Danke für den Hinweis.
Antworten