2017-04-27 2 views
9

J'ai un fichier pickle qui a été créé avec python 2.7 que j'essaye de porter sur python 3.6. Le fichier est enregistré dans py 2.7 via pickle.dumps(self.saved_objects, -1)Le portage pickle py2 à py3 chaînes deviennent des octets

et chargé en python 3.6 via loads(data, encoding="bytes") (à partir d'un fichier ouvert en mode rb). Si j'essaie d'ouvrir en mode r et que je transmets encoding=latin1 à loads, j'obtiens des erreurs UnicodeDecode. Quand je l'ouvre comme un flux d'octets, il charge, mais littéralement chaque chaîne est maintenant une chaîne d'octets. Les clés __dict__ de chaque objet sont toutes b"a_variable_name", ce qui génère ensuite des erreurs d'attribut lors de l'appel an_object.a_variable_name car __getattr__ transmet une chaîne et __dict__ ne contient que des octets. J'ai l'impression d'avoir déjà essayé toutes les combinaisons d'arguments et de protocoles de pickle. En dehors de la conversion de force tous les objets __dict__ touches aux chaînes, je suis à perte. Des idées?

** Passer au 28/04/17 mise à jour pour meilleur exemple

-------------------------- -------------------------------------------------- ---------------------------------

** Mise à jour 4/27/17

Cet exemple minimal illustre mon problème:

PY 2.7.13

import pickle 

class test(object): 
    def __init__(self): 
     self.x = u"test ¢" # including a unicode str breaks things 

t = test() 
dumpstr = pickle.dumps(t) 

>>> dumpstr 
"ccopy_reg\n_reconstructor\np0\n(c__main__\ntest\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nS'x'\np6\nVtest \xa2\np7\nsb." 

PY 3.6.1

import pickle 

class test(object): 
    def __init__(self): 
     self.x = "xyz" 

dumpstr = b"ccopy_reg\n_reconstructor\np0\n(c__main__\ntest\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nS'x'\np6\nVtest \xa2\np7\nsb." 

t = pickle.loads(dumpstr, encoding="bytes") 

>>> t 
<__main__.test object at 0x040E3DF0> 
>>> t.x 
Traceback (most recent call last): 
    File "<pyshell#15>", line 1, in <module> 
    t.x 
AttributeError: 'test' object has no attribute 'x' 
>>> t.__dict__ 
{b'x': 'test ¢'} 
>>> 

-------------------- -------------------------------------------------- ---------------------------------------

Mise à jour 4/28/17

Pour recréer mon problème je poste mes données cornichon brutes réelles here

Le fichier cornichon a été créé en python 2.7.13, Windows 10 à l'aide

with open("raw_data.pkl", "wb") as fileobj: 
    pickle.dump(library, fileobj, protocol=0) 

(protocole 0 donc il est humain lisible)

Pour l'exécuter, vous aurez besoin classes.py

# classes.py 

class Library(object): pass 


class Book(object): pass 


class Student(object): pass 


class RentalDetails(object): pass 

Et le script de test ici:

# load_pickle.py 
import pickle, sys, itertools, os 

raw_pkl = "raw_data.pkl" 
is_py3 = sys.version_info.major == 3 

read_modes = ["rb"] 
encodings = ["bytes", "utf-8", "latin-1"] 
fix_imports_choices = [True, False] 
files = ["raw_data_%s.pkl" % x for x in range(3)] 


def py2_test(): 
    with open(raw_pkl, "rb") as fileobj: 
     loaded_object = pickle.load(fileobj) 
     print("library dict: %s" % (loaded_object.__dict__.keys())) 
     return loaded_object 


def py2_dumps(): 
    library = py2_test() 
    for protcol, path in enumerate(files): 
     print("dumping library to %s, protocol=%s" % (path, protcol)) 
     with open(path, "wb") as writeobj: 
      pickle.dump(library, writeobj, protocol=protcol) 


def py3_test(): 
    # this test iterates over the different options trying to load 
    # the data pickled with py2 into a py3 environment 
    print("starting py3 test") 
    for (read_mode, encoding, fix_import, path) in itertools.product(read_modes, encodings, fix_imports_choices, files): 
     py3_load(path, read_mode=read_mode, fix_imports=fix_import, encoding=encoding) 


def py3_load(path, read_mode, fix_imports, encoding): 
    from traceback import print_exc 
    print("-" * 50) 
    print("path=%s, read_mode = %s fix_imports = %s, encoding = %s" % (path, read_mode, fix_imports, encoding)) 
    if not os.path.exists(path): 
     print("start this file with py2 first") 
     return 
    try: 
     with open(path, read_mode) as fileobj: 
      loaded_object = pickle.load(fileobj, fix_imports=fix_imports, encoding=encoding) 
      # print the object's __dict__ 
      print("library dict: %s" % (loaded_object.__dict__.keys())) 
      # consider the test a failure if any member attributes are saved as bytes 
      test_passed = not any((isinstance(k, bytes) for k in loaded_object.__dict__.keys())) 
      print("Test %s" % ("Passed!" if test_passed else "Failed")) 
    except Exception: 
     print_exc() 
     print("Test Failed") 
    input("Press Enter to continue...") 
    print("-" * 50) 


if is_py3: 
    py3_test() 
else: 
    # py2_test() 
    py2_dumps() 

mettre tous 3 dans le même répertoire et exécutez c:\python27\python load_pickle.py premier qui va créer 1 fichier cornichon pour chacun des 3 protocoles. Ensuite, exécutez la même commande avec python 3 et notez que cette version convertit les clés __dict__ en octets. Je l'ai eu travailler pendant environ 6 heures, mais pour la vie de moi, je ne peux pas comprendre comment je l'ai cassé à nouveau.

+1

Avez-vous essayé l'utf-8? – zondo

+0

ouais, ut8, utf16, latin1 ,, cp – user2682863

+0

Si votre pickle est dans un fichier, pourquoi utilisez-vous 'load' au lieu de' load'? – BrenBarn

Répondre

1

Question: py2 cornichon portage à cordes deviennent PY3 octets

Le encoding='latin-1' donné ci-dessous, est ok.
Votre problème avec b'' sont le résultat de l'utilisation encoding='bytes'. Cela entraînera le décodage des clés de dict comme octets au lieu de str. Les données de problème sont datetime.date values '\x07á\x02\x10', commençant à la ligne dans raw-data.pkl.

C'est un problème connu, comme déjà souligné.
Unpickling python2 datetime under python3
http://bugs.python.org/issue22005

Pour contourner ce problème, je l'ai patché pickle.py et obtenu unpickled object, par exemple

book.library.books [0] .rentals [0] = .rental_date 2017-02-16


Cela fonctionne pour moi:

t = pickle.loads(dumpstr, encoding="latin-1") 

Sortie:
< principal objet .test à 0xf7095fec>
t .__ dict __ = { 'x': 'test ¢'}
Test ¢

testé avec Python: 3.4.2

+0

merci pour la réponse, j'ai mis à jour la question pour refléter plus précisément mon problème – user2682863

+0

@ user268: Je vais essayer et revenir avec mes résultats. Pourquoi n'utilisez-vous pas 'Protocol version 2'? – stovfl

+0

au cas où quelqu'un s'inquiétait de l'exécution de données de cornichons aléatoires téléchargés sur Internet – user2682863

6

En bref , vous frappez bug 22005 avec datetime.date objets dans les objets RentalDetails.

Cela peut être contourné avec le paramètre encoding='bytes', mais qui laisse vos classes avec __dict__ octets contenant:

>>> library = pickle.loads(pickle_data, encoding='bytes') 
>>> dir(library) 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
TypeError: '<' not supported between instances of 'str' and 'bytes' 

Il est possible de corriger manuellement que en fonction de vos données spécifiques:

def fix_object(obj): 
    """Decode obj.__dict__ containing bytes keys""" 
    obj.__dict__ = dict((k.decode("ascii"), v) for k, v in obj.__dict__.items()) 


def fix_library(library): 
    """Walk all library objects and decode __dict__ keys""" 
    fix_object(library) 
    for student in library.students: 
      fix_object(student) 
    for book in library.books: 
      fix_object(book) 
      for rental in book.rentals: 
        fix_object(rental) 

Mais c'est fragile et assez de douleur, vous devriez chercher une meilleure option.

1) Mettre en œuvre __getstate__/__setstate__ qui associe des objets datetime à une représentation non brisée, par exemple:

class Event(object): 
    """Example class working around datetime pickling bug""" 

    def __init__(self): 
      self.date = datetime.date.today() 

    def __getstate__(self): 
      state = self.__dict__.copy() 
      state["date"] = state["date"].toordinal() 
      return state 

    def __setstate__(self, state): 
      self.__dict__.update(state) 
      self.date = datetime.date.fromordinal(self.date) 

2) Ne pas utiliser cornichon du tout.Sur les lignes de __getstate__/__setstate__, vous pouvez simplement implémenter des méthodes to_dict/from_dict ou similaires dans vos classes pour enregistrer leur contenu sous json ou un autre format simple.

Une note finale, ayant une référence arrière à la bibliothèque dans chaque objet ne devrait pas être nécessaire.

+0

Je l'avais réduit à ce bogue date/heure et je lisais un rapport de bogue différent. Y at-il un moyen d'obtenir un meilleur retraçage de pickle lorsque certains éléments ne peuvent pas être défaites? Les références à la bibliothèque sont juste là pour la commodité d'accès. Je pense que la solution __status __/__ setstate__ est probablement idéale. – user2682863

+0

Problèmes avec pickle sont en effet difficiles à déboguer, qui, avec les problèmes de sécurité sont une bonne raison de sérialiser via un mécanisme différent où possible. –

+0

dernière question, quel encodage utiliseriez-vous après l'implémentation de l'état get/state? – user2682863

1

Vous devez traiter les données pickle comme étant spécifiques à la version (majeure) de Python qui l'a créée.

(Voir Gregory Smith's message w.r.t. issue 22005.)

La meilleure façon de se déplacer est d'écrire un programme Python 2.7 pour lire les données marinées et l'écrire dans un format neutre.

Jeter un oeil rapide à vos données réelles, il me semble qu'une base de données SQLite est appropriée en tant que format d'échange, puisque les Book s contiennent des références à un Library et RentalDetails. Vous pouvez créer des tables séparées pour chacun.