web-dev-qa-db-fra.com

Comment résoudre "l'itérateur doit renvoyer des chaînes, pas des octets"

J'essaie d'importer un fichier CSV en utilisant un formulaire pour le télécharger depuis le système client. Une fois que j'ai le fichier, je vais en prendre des parties et remplir un modèle dans mon application. Cependant, je reçois un message d'erreur "l'itérateur doit renvoyer des chaînes, et non des octets" lorsque je vais parcourir les lignes du fichier téléchargé. J'ai passé des heures à essayer différentes choses et à lire tout ce que je pouvais trouver à ce sujet, mais je n'arrive pas à résoudre le problème (remarque, je suis relativement nouveau sur Django- qui exécute 1.5 et python - qui exécute 3.3). J'ai effacé les choses pour arriver à l'erreur et je les ai exécutées comme cela pour m'assurer qu'elle est toujours là. L'erreur s'affiche lors de l'exécution de la ligne "for clubs in club_list" dans tools_clubs_import ():

Ci-dessous, le views.py corrigé qui fonctionne, basé sur la réponse marquée ci-dessous:

import csv
from io import TextIOWrapper
from Django.shortcuts import render
from Django.http import HttpResponseRedirect
from Django.core.urlresolvers import reverse
from rank.forms import ClubImportForm

def tools_clubs_import(request):
    if request.method == 'POST':
        form = ClubImportForm(request.POST, request.FILES)
        if form.is_valid():
            # the following 4 lines dumps request.META to a local file
            # I saw a lot of questions about this so thought I'd post it too
            log = open("/home/joel/meta.txt", "w")
            for k, v in request.META.items():
                print ("%s: %s\n" % (k, request.META[k]), file=log)
            log.close()
            # I found I didn't need errors='replace', your mileage may vary
            f = TextIOWrapper(request.FILES['filename'].file,
                    encoding='ASCII')
            club_list = csv.DictReader(f)
            for club in club_list:
                # do something with each club dictionary entry
                pass
            return HttpResponseRedirect(reverse('rank.views.tools_clubs_import_show'))
    else:
        form = ClubImportForm()

    context = {'form': form, 'active_menu_item': 4,}
    return render(request, 'rank/tools_clubs_import.html', context)

def tools_clubs_import_show(request):
    return render(request, 'rank/tools_clubs_import_show.html')

Ce qui suit est la version originale de ce que j'ai soumis (le code HTML qui génère le formulaire est inclus au bas de cette liste de codes:

views.py
--------
import csv
from Django.shortcuts import render
from Django.http import HttpResponseRedirect
from rank.forms import ClubImportForm

def tools(request):
    context = {'active_menu_item': 4,}
    return render(request, 'rank/tools.html', context)

def tools_clubs(request):
    context = {'active_menu_item': 4,}
    return render(request, 'rank/tools_clubs.html', context)

def tools_clubs_import(request):
    if request.method == 'POST':
        form = ClubImportForm(request.POST, request.FILES)
        if form.is_valid():
            f = request.FILES['filename']
            club_list = csv.DictReader(f)
            for club in club_list:
                # error occurs before anything here is executed
                # process here... not included for brevity
            return HttpResponseRedirect(reverse('rank.views.tools_clubs_import_show'))
    else:
        form = ClubImportForm()

    context = {'form': form, 'active_menu_item': 4,}
    return render(request, 'rank/tools_clubs_import.html', context)

def tools_clubs_import_show(request):
    return render(request, 'rank/tools_clubs_import_show.html')

forms.py
--------
from Django import forms


class ClubImportForm(forms.Form):
    filename = forms.FileField(label='Select a CSV to import:',)


urls.py
-------
from Django.conf.urls import patterns, url
from rank import views

urlpatterns = patterns('',
    url(r'^tools/$', views.tools, name='rank-tools'),
    url(r'^tools/clubs/$', views.tools_clubs, name='rank-tools_clubs'),
    url(r'^tools/clubs/import$',
        views.tools_clubs_import,
        name='rank-tools_clubs_import'),
    url(r'^tools/clubs/import/show$',
        views.tools_clubs_import_show,
        name='rank-tools_clubs_import_show'),
)


tools_clubs_import.html
-----------------------
{% extends "rank/base.html" %}
{% block title %}Tools/Club/Import{% endblock %}
{% block center_col %}

    <form enctype="multipart/form-data" method="post" action="{% url 'rank-tools_clubs_import' %}">{% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Submit" />
    </form>

{% endblock %}

Valeur d'exception:

l'itérateur doit renvoyer des chaînes et non des octets (avez-vous ouvert le fichier en mode texte?)

Emplacement d'exception: /usr/lib/python3.3/csv.py dans les noms de champs, ligne 96

23
MeaOrdo

request.FILES vous donne binary fichiers, mais le module csv veut plutôt avoir des fichiers en mode texte.

Vous devez envelopper le fichier dans une instance io.TextIOWrapper() , et vous devez comprendre le codage:

from io import TextIOWrapper

f = TextIOWrapper(request.FILES['filename'].file, encoding=request.encoding)

Il serait probablement préférable que vous preniez le paramètre charset à partir de l'en-tête Content-Type s'il est fourni. c'est ce que le client vous dit que le jeu de caractères est.

Vous ne pouvez pas contourner le besoin de connaître le codage correct pour les données du fichier; vous pouvez forcer l'interprétation en ASCII, par exemple, en fournissant également un mot clé errors (en le définissant sur 'replace' ou 'ignore'), mais cela entraîne la perte de données:

f = TextIOWrapper(request.FILES['filename'].file, encoding='ascii', errors='replace')

Utiliser TextIOWrapper ne fonctionnera que si vous utilisez Django 1.11 et versions ultérieures (en tant que ce changeset a ajouté le support requis ). Dans les versions antérieures, vous pouvez personnaliser le support après-coup:

from Django.core.files.utils import FileProxyMixin

if not hasattr(FileProxyMixin, 'readable'):
    # Pre-Django 1.11, add io.IOBase support, see
    # https://github.com/Django/django/commit/4f474607de9b470f977a734bdd47590ab202e778        
    def readable(self):
        if self.closed:
            return False
        if hasattr(self.file, 'readable'):
            return self.file.readable()
        return True

    def writable(self):
        if self.closed:
            return False
        if hasattr(self.file, 'writable'):
            return self.file.writable()
        return 'w' in getattr(self.file, 'mode', '')

    def seekable(self):
        if self.closed:
            return False
        if hasattr(self.file, 'seekable'):
            return self.file.seekable()
        return True

    FileProxyMixin.closed = property(
        lambda self: not self.file or self.file.closed)
    FileProxyMixin.readable = readable
    FileProxyMixin.writable = writable
    FileProxyMixin.seekable = seekable
39
Martijn Pieters

En python 3, j'ai utilisé:

import csv
from io import StringIO
csvf = StringIO(xls_file.read().decode())
reader = csv.reader(csvf, delimiter=',')

xls_file étant le fichier obtenu à partir du formulaire POST. J'espère que ça aide.

14
Moise

Fusionnez vos deux méthodes, cela n'échoue jamais dans Python 3.5.2 et Django 1.9

delimitador = list_delimitadores[int(request.POST['delimitador'])][1]
try:
    text = TextIOWrapper(request.FILES['csv_x'].file, encoding='utf-8 ', errors='replace')
    reader = csv.reader(text, delimiter=delimitador)
except:
    text = StringIO(request.FILES['csv_x'].file.read().decode())
    reader = csv.reader(text, delimiter=delimitador)
0
Victor Villacorta