TypeError: coercing to Unicode: need string or buffer. ???

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
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

Hallo zusammen,

kann mir bitte jemand auf die Sprünge helfen?

Ich komme nicht weiter und dreh mich nur noch im Kreis mit der Problematik.

Folgendes Programm:

myfindduplicates.py

Code: Alles auswählen

#!/usr/bin/python
from __future__ import with_statement
import os
import md5
import sys

def fsize(file):
    return os.stat(file)[6]

def fmd5sum(file):
    return md5.new(file.read()).digest()

def mkprimkey(file):
    output = set()
    output.add(fsize(file))
    output.add(fmd5sum(file))
    return output

def addentry(dict, pkey, fname):
    if dict.has_key(pkey):
        dict[pkey].add(fname)
    else:
        valset = set()
        dict[pkey] = valset.add(fname)

### main programm ###

mydict = dict()

for fname in sys.stdin:
    fname = fname.split('\n')[0]
    with open(fname) as file:
        pkey = mkprimkey(file)
        addentry(mydict, pkey, fname)

print mydict
Aufgerufen wird das ganze in einem Ordner mit 11 Dateien, wobei 10 verschieden sind und eine Datei nur die Kopie einer anderen ist mit anderem Namen. Das Kommando des Programmstarts sieht so aus:

# find . -maxdepth 1 -type f -name "*.py" -printf "%f\n" | ./myfindduplicates.py

Es kommt zu folgender Fehlermeldung, mit der ich nichts anfangen kann:

Code: Alles auswählen

Traceback (most recent call last):
  File "./fsizemd5sum.py", line 40, in <module>
    pkey = mkprimkey(file)
  File "./fsizemd5sum.py", line 22, in mkprimkey
    output.add(fsize(file))
  File "./fsizemd5sum.py", line 15, in fsize
    return os.stat(file)[6]
TypeError: coercing to Unicode: need string or buffer, file found
Kann jemand was intelligentes dazu sagen? Ich steh grad aufm Schlauch.
BlackJack

`os.stat()` erwartet einen Datei*namen* und kein Datei*objekt*.

`fsize()` gibt's schon: `os.path.getsize()`.

Die gesamte Datei in den Speicher zu lesen um die Prüfsumme zu berechnen, kann ungemütlich werden, wenn man das in einem Ordner mit Filmen, CD/DVD-Images, o.Ä. macht.

Ein `set()` kann man nicht als Schlüssel verwenden, da nicht "hashable". Da würde man eher ein Tupel verwenden.

`addentry()` kann man sich ab Python 2.5 sparen wenn man `collections.defaultdict()` verwendet. Davor geht es mit der `dict.setdefault()`-Methode einfacher als mit dem ``if``/``else``.

Das entfernen des Zeilenendes geht mit der `rstrip()`-Methode etwas eleganter.
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

Ok ich habs jetzt etwas umgestellt:

Code: Alles auswählen

#!/usr/bin/python

from __future__ import with_statement
import os
import md5
import sys

def fsize(fname):
    return os.path.getsize(fname) % 2**64

def fmd5sum(file):
    return md5.new(file.read()).digest()

def addentry(dict, pkey, fname):
    if dict.has_key(pkey):
        dict[pkey].add(fname)
    else:
        valset = set()
        dict[pkey] = valset.add(fname)

### main programm ###

mydict = dict()

for fname in sys.stdin.read().split('\0')[:-1]:
    with open(fname) as file:
        pkey = str(fsize(fname)) + fmd5sum(file)
        addentry(mydict, pkey, fname)

print mydict
Mein Problem ist jetzt aber, dass die Dateinamen garnicht hinzugefügt werden, als Wert für die jeweiligen Primary Keys.

Output vom Programm ist:

Code: Alles auswählen

smitty@smitty-PC tmp $ find . -maxdepth 1 -type f -print0 | ./myfinddups.py
{'30463\xdc\xce\xe7"K\x83n\xf4\xba\xb1\xa1\x8b\xdem\x89': None, '166247792`M1\xdf\xf5o\xaf\xdbV\x90SH\x8c\xf0#': None, '2406400\x9e~\x0f\xa7\xde\xae!\xdbg\x00\x1d\xe1,"\x8f\xfc': None, '122880\xd1[DZ\x94\xe2\xbd\x1c^^\xce\xac\x8aU!\xda': None, '770S\xa36\xb9\x1f*\x94b\xce\xcf\xd6\xcd\x9b"\xe2Q': None, '752\xbf\x01\xd9\xee3\x13\xfd\xbc\xaa\xbe\x91\xc7\xa9\xcf0:': None, '583\xc7\x14\xfe\x0c<\x91\x1b\\%\x01a\x89n\x9f\x07\xb3': None, '14113#\xf3^C\x11D+\xf7}\xd3L6\x98b\x03\xb0': None}
Man kann direkt erkennen, dass er die Dateigrössen richtig einträgt als Schlüssel (+ md5 Summen als \xFOOBAR), aber anstelle der entsprechenden Dateinamen steht "None".

Ich sehe meinen Fehler nicht. Ich füge doch mit dict[pkey].add(fname) den Dateinamen ein, oder wieso landet er da nicht?

EDIT:

PS: Wie kann ich ein Python Integer (modulo 2 hoch 64) zu einem 64 bit String umwandeln?
Ich will eine fixe Grösse von 64bit oder 8 byte für die filesize haben, welche ich mit den fixen 128 bit des md5s concantenieren will (als pkey-192bit-string).
Momentan (siehe Code oben) habe ich ein dreckigen Workaround mit der Umwandlung in einen String (igit, igit). Da muss ich demnächst weg von.
BlackJack

Bei dem Quelltext komme ich gar nicht bis zum Ende sondern es bricht schon hier ab:

Code: Alles auswählen

bj@s8n:~$ find txt -maxdepth 1 -type f -print0 | python forum.py
Traceback (most recent call last):
  File "forum.py", line 29, in <module>
    addentry(mydict, pkey, fname)
  File "forum.py", line 17, in addentry
    dict[pkey].add(fname)
AttributeError: 'NoneType' object has no attribute 'add'
Bei einer doppelten Datei passiert das. Überleg Dir mal genau was Deine Zeile 19 macht.

Für das Integer-Objekt als Bytestring schau Dir mal das `struct`-Modul an.
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

Danke für den Hinweis @Zeile19.
Ich dachte:

Code: Alles auswählen

valset = set()
valset.add(foobar)
dict[pkey]=valset
sei äquivalent zu:

Code: Alles auswählen

valset = set()
dict[pkey]=valset.add(foobar)
Stimmt offensichtlich nciht und nur das erste Beispiel ist richtig.
Hab ich wohl an der falschen Stelle ne Zeile gespart.

Danke auch für den Hinweis @struct.

PS: Das Programm tut jetzt wie ichs eigentlich geplant hatte. :)
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

Ok also seit 5 min sieht mein Programm so aus:

Code: Alles auswählen

#!/usr/bin/python

import os
import md5
import sys
from struct import *

def fsize(fname):
    return os.path.getsize(fname) % 2**64

def fmd5sum(file):
    return md5.new(file.read()).digest()

def addentry(dict, pkey, fname):
    if dict.has_key(pkey):
        dict[pkey].add(fname)
    else:
        valset = set()
        valset.add(fname)
        dict[pkey] = valset

### main programm ###

mydict = dict()

for fname in sys.stdin.read().split('\0')[:-1]:
    file = open(fname)
    pkey = pack('Q', fsize(fname)) + fmd5sum(file)
    addentry(mydict, pkey, fname)
    file.close()

print mydict
Ich denke die Verwendung von struct ist nun so, wie du es empfohlen hast. @blackjack

Danke für die Tipps.
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Naja, jetzt kannst du dich mal daran machen das Programm in Funktionen zu verpacken, die Stern-Importe loszuwerden, die Funktionsnamen an PEP 8 anzupassen, aufhören Builtins zu überschrieben (dict, file), ``hashlib`` zu verwenden (spreche ich an, weil du scheinbar Python 2.5 mit dem ``with``-Statement nutzt). Achja, entweder nutzt du das, oder schließt die Dateien explizit, damit keine offenen Referenzen über bleiben. Statt ``has_key`` kannst du schon lange ``in`` verwenden. Das ``valset`` in zwei Schritten zu erzeugen leuchtet mir auch nicht sonderlich ein.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

Python Library Reference:
3.8 Mapping Types -- dict

k in a | True if a has a key k, else False
a.has_key(k) | Equivalent to k in a, use that form in new code
Laut oberer Aussage ist aber gerade has_key die neue Form für a in b.

Ansonsten müsste nach deinen Einwänden das Programm in Zukunft so aussehen:

Code: Alles auswählen

#!/usr/bin/python

import os
import hashlib
import sys
from struct import pack

def get_fsize(fname):
    return os.path.getsize(fname) % 2**64

def get_fdigest(infile):
    fdigest = hashlib.new(use_digest)
    fdigest.update(infile.read())
    return fdigest.digest()

def addentry(indict, pkey, fname):
    if use_abspath:
        fname = os.path.abspath(fname)
    if use_realpath:
        fname = os.path.realpath(fname)
    if indict.has_key(pkey):
        indict[pkey].add(fname)
    else:
        indict[pkey] = set([fname])

### main programm ###

mydict = dict()

# select digest algorithm
use_digest = 'md5' # default = 'md5'
#use_digest = 'ripemd160'
#use_digest = 'sha512'

# store absolute filenames. same as "readlink -f file" in unix.
use_abspath = False # default = False

# follow symbolic links first, then store the resulting filename.
use_realpath = False # default = False

for fname in sys.stdin.read().split('\0')[:-1]:
    file = open(fname)
    fsize = get_fsize(fname)
    if fsize > 0: # zerobyte files are booooring...
        addentry(mydict, pack('!Q', fsize) + get_fdigest(file), fname)
    file.close()

print mydict

Das with statement habe ich wieder weggemacht, um abwärtskompatibel zu bleiben (py <v2.5).

Müsste mir nur noch gescheite Kommandoparameter einfallen lassen, um den digest (use_digest) von aussen zu setzen und die Fileliste flexibler einlesen zu können (nimmt momentan nur \0 separierte strings von stdin).

Hey, was man nicht alles in wenigen Minuten hinbekommt in Python. :)

Hier mal 5 min, da mal 5 min coden. Immer zwischendrin Tipps ausm Forum lesen und gleich umsetzen. Cool.

Wenn das so weitergeht wird mein Prog vielleicht eleganter/kleiner/schneller/besser als all die anderen Finddup Klones.
Benutzeravatar
mkesper
User
Beiträge: 919
Registriert: Montag 20. November 2006, 15:48
Wohnort: formerly known as mkallas
Kontaktdaten:

Python Library Reference 2.5 hat geschrieben:3.8 Mapping Types -- dict

k in a | True if a has a key k, else False
a.has_key(k) | Equivalent to k in a, use that form in new code
*augenreib* Ich habe es gerade nochmal nachgeschaut, das verwundert mich jetzt auch...
BlackJack

Das 'that form' betzieht sich in dem Satz auf das ``in`` nicht auf `has_key()`. In Python 3.0 wird's `dict.has_key()` wohl nicht mehr geben.

Code: Alles auswählen

Python 3.0a1 (py3k, Aug 31 2007, 21:20:42)
[GCC 4.1.2 (Ubuntu 4.1.2-0ubuntu4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> dict.has_key
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'dict' has no attribute 'has_key'
Man könnte beides im vorliegenden Quelltext vermeiden, wenn man die `setdefault()`-Methode verwendet. Oder halt ein `collections.defaultdict` falls man nicht unbedingt auf Python <2.5 angewiesen ist.

Code: Alles auswählen

    indict.setdefault(pkey, set()).add(fname)
Ich würde noch empfehlen Konstanten wie z.B. `use_abspath`, komplett gross zu schreiben und an den Anfang des Quelltextes zu verschieben. Und das Hauptprogramm könnte man auch noch in eine Funktion stecken und mit

Code: Alles auswählen

if __name__ == '__main__':
    main()
vor der Ausführung schützen wenn man das Modul nicht ausführt, sondern importiert.

Ein kleiner Bug ist in der Abfrage von `fsize` auf 0: Es werden hier nicht nur leere Dateien ignoriert, sondern alle Dateien mit vielfachen Grössen von 2**64. Ich weiss, das klingt jetzt ein bisschen utopisch, aber es gab auch mal eine Zeit, da dachte man zwei Ziffern für Jahreszeitangaben seien genug, und die Programme werden um 2000 herum ganz bestimmt nicht mehr im Einsatz sein. ;-)
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

akis.kapo hat geschrieben:
Python Library Reference:
3.8 Mapping Types -- dict

k in a | True if a has a key k, else False
a.has_key(k) | Equivalent to k in a, use that form in new code
Laut oberer Aussage ist aber gerade has_key die neue Form für a in b.
Da ich dadurch sehr verwundert war und BlackJacks Aussage nicht gesehen habe, bin ich parallel dazu zum Schluss gekommen, dass das 'that' sich auf ``in`` bezieht. Also wird dazu geraten.
akis.kapo hat geschrieben:Das with statement habe ich wieder weggemacht, um abwärtskompatibel zu bleiben (py <v2.5).
Dann musst du die ``hashlib`` aber auch wieder rausnehmen. Und den Code auf Modul-Ebene kannst du in eine ``main()`` Funktion packen, die dann in einer ``if __name__ == '__main__':\n main()``-Abfrage gestartet wird. Dann kann man dein Programm auch gefahrlos importieren.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

[Leo von Lethal Weapon]
Ok ok ok ok!
[/Leo von Lethal Weapon]

Es ist erstaunlich, wie schnell und zielgerecht man in Python mit Hilfe von docs.python.org so wage Andeutungen zur Codeverbesserung (von BlackJack und co.) sicher umsetzen kann.

Habe noch nie was von collections gehört, ausser dass es das schon in java und c++ gab.

Aber nun gut. Alle Eindrücke aufgenommen und umgesetzt, hier bitteschön:

Code: Alles auswählen

#!/usr/bin/python

from __future__ import with_statement
from struct import pack
from collections import defaultdict
import os
import hashlib
import sys

# select digest algorithm
USE_DIGEST = 'md5' # default = 'md5'
#USE_DIGEST = 'ripemd160'
#USE_DIGEST = 'sha512'

# store absolute filenames. same as "readlink -f file" in unix.
USE_ABSPATH = False # default = False

# follow symbolic links first, then store the resulting filename.
USE_REALPATH = False # default = False

def get_fsize(fname):
    return os.path.getsize(fname) % 2**64

def get_fdigest(infile):
    fdigest = hashlib.new(USE_DIGEST)
    fdigest.update(infile.read())
    return fdigest.digest()

def addentry(indict, pkey, fname):
    if USE_ABSPATH:
        fname = os.path.abspath(fname)
    if USE_REALPATH:
        fname = os.path.realpath(fname)
    indict[pkey].add(fname)

def processfile(mydict, fname):
    with open(fname) as f:
        fsize = get_fsize(fname)
        if fsize > 0: # zerobyte files are booooring...
            addentry(mydict, pack('!Q', fsize) + get_fdigest(f), fname)

### main programm ###

def main():
    mydict = defaultdict(set)

    filelist = sys.stdin.read().split('\0')[:-1]

    for fname in filelist:
        processfile(mydict, fname)

    print mydict

if __name__ == "__main__":
    main()

Final ists immer noch nicht, aber immerhin ein ganzes Stückchen "wiederverwendbarer".

Übrigends setz ich nun doch auf python >= v2.5. (Wenn schon, denn schon).

Danke euch.

Und hört ja nicht auf am Code rumzumeckern! Verbesserungsvorschläge sind erwünscht.
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

Code: Alles auswählen

#!/usr/bin/python

from __future__ import with_statement
from struct import pack
from collections import defaultdict
import os
import hashlib
import sys

# select digest algorithm
USE_DIGEST = 'md5' # default = 'md5'
#USE_DIGEST = 'ripemd160'
#USE_DIGEST = 'sha512'

# store absolute filenames. same as "readlink -f file" in unix.
USE_ABSPATH = False # default = False

# follow symbolic links first, then store the resulting filename.
USE_REALPATH = False # default = False

def get_fdigest(infile):
    fdigest = hashlib.new(USE_DIGEST)
    fdigest.update(infile.read())
    return fdigest.digest()

def addentry(indict, pkey, fname):
    if USE_ABSPATH:
        fname = os.path.abspath(fname)
    if USE_REALPATH:
        fname = os.path.realpath(fname)
    indict[pkey].add(unicode(fname))

def processfile(mydict, fname):
    with open(fname) as f:
        fsize = os.path.getsize(fname)
        fdigest = get_fdigest(f)
    noQs = get_noQs(fsize)
    pack_args = [(fsize >> (i*64)) % 2**64 for i in xrange(noQs)]
    if fsize > 0: # zerobyte files are booooring...
        addentry(mydict, pack(noQs*'!Q', *pack_args) + fdigest, fname)

def get_noQs(fsize):
    noQs = 1
    while max(fsize-1,1) >> (noQs*64):
        noQs += 1
    return noQs

### main programm ###

def main():
    mydict = defaultdict(set)

    filelist = sys.stdin.read().split('\0')[:-1]

    for fname in filelist:
        processfile(mydict, fname)

    print mydict

if __name__ == "__main__":
    main() 

Jetzt mit beliebig grossen Dateien und Dateinamen werden (zukunft-)sicherheitshalber gleich in Unicode gespeichert.
BlackJack

Das mit dem Unicode funktioniert nicht. Probier's mal mit Dateinamen aus, die Zeichen ausserhalb von ASCII enthalten.

Da unter Unix Dateinamen nur aus Bytes bestehen und durchaus Dateinamen mit verschiedenen Kodierungen nebeneinander existieren können, ist eher sicherer die Dateinamen *nicht* als Unicode zu speichern.

Und diesen "Stunt" mit dem umwandeln der (grossen) Zahlen habe ich auch nicht ganz verstanden. Was soll das!?
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

BlackJack hat geschrieben:Und diesen "Stunt" mit dem umwandeln der (grossen) Zahlen habe ich auch nicht ganz verstanden. Was soll das!?
Eigentlich war mir der MD alleine nicht genug als "Fingerabdruck".
Ich wollte unbedingt die Dateigrösse in Bytes mit rein nehmen in den MD.
Der MD ist bei mir die Dateigrösse in x*64 bit Strings + MD5 String,
welches zugleich als Schlüssel für das Dictionary dient.

Ich wollte nur beliebig grosse Dateien, bzw. deren Dateigrösse, in
ein Byteformat umwandeln, mehr nicht. Einfach als vielfaches von 8 Byte,
je nach Bedarf mal mehr, mal weniger 8 Byte Päckchen.

Gehts denn noch einfacher?

EDIT:
Aber jetzt bitte nichts mit Pickle antworten...
BlackJack

Man kann einfach ein Tupel aus Dateigrösse und Prüfsumme als Schlüssel nehmen. Also an der entsprechenden Stelle:

Code: Alles auswählen

        addentry(mydict, (fsize, fdigest), fname)
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

Ja, und genau deswegen wollte ich auch keine Pickle Antwort.

Weil ich plattformunabhängig bleiben will und man sich ja auch mal vornehmen könnte, dasselbe in C zu portieren.
Spätestens dann ist es schon viel einfacher, wenn man ein klar definiertes Byteformat hat.

Schneller und einfacher (zum Coden) ist dein Vorschlag aber natürlich alle mal.
Vielleicht führe ich es als python-only Version weiter in 2 Branches.

EDIT:
python is ja auch plattformunabhängig. :)
Aber der Key muss auch hashable sein. Weiss nicht ob das mit dem tupel funktioniert.
BlackJack

Solange alle Elemente im `tuple` "hashable" sind, ist es auch das Tupel selbst.

Code: Alles auswählen

In [317]: hash((42L, 'spam'))
Out[317]: -300945752
Wenn Du das irgendwann mal in C umsetzen willst reicht ein `unsigned long` oder `unsigned long long`. Grössere Zahlen wird auch die C-Bibliothek für die Dateigrösse nicht liefern.

Und letztendlich dürfte ein Textformat für beliebig grosse Zahlen am portabelsten sein, weil über dem was der Prozessor in einem Register halten kann bzw. was C mit einem `unsigned long long` aufnehmen kann, jede Sprache/Implementierung ein eigenes Süppchen für solche grossen Zahlen als Binärformat kocht.
Antworten