BLOGサブスレッドの日常

2016.08.09

django(Python3) の FileSystemStorage クラスを使って、CSVを読み込む

torikai

月曜日担当の秋山です。よろしくお願いします。

今日のひとネタ

ネタというほどのネタではないんですけども… 過去につまづいたネタ。

便利に使っているdjango。デフォルトのファイルストレージは、FileSystemStorageとなっています。
djangoでは、settings.pyDEFAULT_FILE_STORAGEに利用するファイルストレージを指定してあげると簡単に切り替わるので、
本番ではS3BotoStorageクラスを使うけど、開発環境、特にローカルではFileSystemStorageクラスを使う。なんてことがお手軽に出来ます。

出来るわけですが、このFileSystemStorage、ちょっとだけ、ちょっとだけハマりどころが。
多分、Python3を使ってるから、というのもあるんですが、まぁどういう時にハマったかというと。

SJISのCSVファイルを読み込みたかった

こんな感じのCSVファイルがあるとして。

"1","大阪","000000","カレー美味しい"
"2","広島","111111","お好み焼き美味しい"
"3","名古屋","222222","手羽先美味しい"

こんな感じのコードを書いていました。

# -*- coding:utf-8 -*-
import csv
from django.core.files.storage import default_storage
from django.shortcuts import render


def netacsv(request):
    context = {}
    data = []
    storage = default_storage
    storage.location = 'temp/'
    with storage.open('csvtest.csv', mode='r') as fp:
        reader = csv.reader(fp, delimiter=',', quotechar='"', lineterminator='\r\n')
        for row in reader:
            data.append(row)
        context['data'] = data
    return render(request, 'csv.html', context)

(´・ω・`)…
そりゃそうだ。CSVはSJISなのに、プログラムコードがUTF-8だもの。私が悪かった。

と思って、openencoding='cp932'を渡せばあっさり解決…

しない!

FileSystemStorageが、というより、FileSystemStorageの継承元のStorageクラスが、そもそもopenメソッドにencodingの引数をサポートしていない模様。

class Storage(object):
    """
    A base storage class, providing some default behaviors that all other
    storage systems can inherit or override, as necessary.
    """

    # The following methods represent a public interface to private methods.
    # These shouldn't be overridden by subclasses unless absolutely necessary.

    def open(self, name, mode='rb'):
        """
        Retrieves the specified file from storage.
        """
        return self._open(name, mode)
...
@deconstructible
class FileSystemStorage(Storage):
    """
    Standard filesystem storage
    """
...

    def _open(self, name, mode='rb'):
        return File(open(self.path(name), mode))

うーんからい(´・ω・`)

returnで返している処理の中の、open()encodingの引数をサポートしているじゃないですかー…
吸収してくれたって良さそうなものなのに。

そもそもストレージクラスを使ってCSVファイルを読み込む事自体が微妙なのかしら…?
いや、読みたい場面はそれなりにある、はず。

というわけで

utils.pyみたいなトコにクラスを生やして

# -*- coding:utf-8 -*-
from django.core.files.storage import FileSystemStorage, File


class MyFileSystemStorage(FileSystemStorage):
    def open(self, name, mode='rb', *args, **kwargs):
        return File(open(self.path(name), mode, *args, **kwargs))

settings.pyDEFAULT_FILE_STORAGEを定義すれば…

DEFAULT_FILE_STORAGE = 'neta.utils.MyFileSystemStorage'
1 大阪 000000 カレー美味しい
2 広島 111111 お好み焼き美味しい
3 名古屋 222222 手羽先美味しい

やったー∩(・ω・)∩ 読み込めた!

因みに

この処理の書き方だと、S3BotoStorageクラスを利用する時にエラーが出るので、

# -*- coding:utf-8 -*-
import csv
from django.core.files.storage import default_storage
from django.shortcuts import render
from django.conf import settings
from io import StringIO


def netacsv(request):
    context = {}
    data = []
    storage = default_storage
    storage.location = 'temp/'
    encoding = 'cp932'

    kwargs = {}
    if settings.DEFAULT_FILE_STORAGE == 'neta.utils.MyFileSystemStorage':
        kwargs['encoding'] = encoding

    with storage.open('csvtest.csv', mode='r', **kwargs) as fp:
        if settings.DEFAULT_FILE_STORAGE == 'neta.utils.FileSystemStorage':
            body = fp
        else:
            body = StringIO(fp.read().decode(encoding))
        reader = csv.reader(body, delimiter=',', quotechar='"', lineterminator='\r\n')
        for row in reader:
            data.append(row)
        context['data'] = data
    return render(request, 'csv.html', context)

こんな感じで分岐を入れてあげるとどちらの環境でも動作します。
ちょっと無理矢理感有りますが、
body = StringIO(fp.read().decode(encoding))してるとことか、プロジェクト毎にMyFileSystemStorageを用意しなきゃいけないとか)
SJISCSVを読みたい時はこんな感じで書くようになりました。

今日のひとネタでした(・ω・)ノ

この記事を書いた人

torikai