akiyoko blog

akiyoko の IT技術系ブログです

まだ CSV の文字化けで消耗してるの?(Excel で直接開いても文字化けしない CSVファイルを Python3 で作成するスマートな方法)

この投稿は 「python Advent Calendar 2017 - Qiita」 の 9日目の記事です。

こんにちは、akiyoko です。
「Python Advent Calendar」は 4年連続 4度目の参加になります。 *1, *2, *3



はじめに

皆さん、CSV は好きですよね? Excel も大好きですね?
じゃあ当然、CSVファイルは Excel で開きますよね。

文字化けは? ・・もちろん嫌いですよね。
でも CSVファイルを Excel で開こうとしたときに、こんな文字化け地獄を経験したことはありませんでしたか? *4


f:id:akiyoko:20171205230423p:plain:w400


ということで今回は、Excel で直接開いたときに文字化けしない CSV ファイルを Python3 で作成する方法 を紹介したいと思います。(おまけで Python2 でのやり方も書いておきますが、今時 Python2 で消耗している人なんていないですよね? *5



 

結論

結論を先に書くと、

  • Unicode の文字符号化方式は 「UTF-16(正確には、BOMありの UTF-16 LE)」
  • タブ区切り

で CSVファイルを作成すれば、Excel で直接開いても文字化けせず、それぞれの値がセルごとに分かれて表示されます。


(参考)Which encoding opens CSV files correctly with Excel on both Mac and Windows? - Stack Overflow



Windowsでは、リトルエンディアンのUTF-16符号化スキームが使われている。内部表現では16ビット符号なし整数を符号単位とするUTF-16符号化形式(CEFなのでBOMはなし)として扱い、ファイルなどではBOMありのUTF-16符号化スキーム(リトルエンディアン)が主である。


UTF-16 - Wikipedia

Note Microsoft uses UTF-16, little endian byte order.


Using Byte Order Marks | Microsoft Docs

とあるように、Microsoft Excel が 「BOMありの UTF-16 LE」を扱っているため、この方法がベストと言えそうです。


なお、「CSV(Comma-Separated Values)」と言いながらも区切り文字がタブなので、厳密には「TSV(Tab-Separated Values)」と呼ぶべきでしょうか。議論の余地はあるものの(*6)、拡張子を「.csv」としておくことでダブルクリック時に自動的に Excel が起動してくれるので(アプリケーションが関連付けられているので)、拡張子は「.csv」とした方がよいでしょう。



 

検証(Python 3)

ファイルオープン時に「encoding='utf-16'」と指定することで、符号化方式が「UTF-16 LE with BOM」となります。
「encoding='utf-8-sig'」(UTF-8 with BOM)だと、環境によっては(Mac + Excel 2011 とか?)文字化けすることがあるので推奨しません。

import csv


def main():
    rows = [['髙﨑 將'], ['あああ', 'いいい', 'ううう'], ['Ⅰ・Ⅱ・Ⅲ', '①②③']]

    # OK
    with open('utf_16_excel_tab.csv', 'w', newline='', encoding='utf-16') as f:
        w = csv.writer(f, dialect='excel-tab', quoting=csv.QUOTE_ALL)
        w.writerows(rows)

    # これでもOK
    with open('utf_16_excel_tab_2.csv', 'w', newline='', encoding='utf-16') as f:
        w = csv.writer(f, dialect='excel', delimiter='\t', quoting=csv.QUOTE_ALL)
        w.writerows(rows)

    # 文字化けしないが、セルごとに分かれないのでNG
    with open('utf_16.csv', 'w', newline='', encoding='utf-16') as f:
        w = csv.writer(f, quoting=csv.QUOTE_ALL)
        w.writerows(rows)

    # 文字化け (しない場合もある)
    with open('utf_8_sig.csv', 'w', newline='', encoding='utf-8-sig') as f:
        w = csv.writer(f, quoting=csv.QUOTE_ALL)
        w.writerows(rows)

    # 文字化け (しない場合もあるが、セルごとに分かれないのでNG)
    with open('utf_8_sig_excel_tab.csv', 'w', newline='', encoding='utf-8-sig') as f:
        w = csv.writer(f, dialect='excel-tab', quoting=csv.QUOTE_ALL)
        w.writerows(rows)

    # 文字化け
    with open('utf_8.csv', 'w', newline='', encoding='utf-8') as f:
        w = csv.writer(f, quoting=csv.QUOTE_ALL)
        w.writerows(rows)

    # 文字化け
    with open('utf_8_excel_tab.csv', 'w', newline='', encoding='utf-8') as f:
        w = csv.writer(f, dialect='excel-tab', quoting=csv.QUOTE_ALL)
        w.writerows(rows)


if __name__ == '__main__':
    main()


OK

f:id:akiyoko:20171205231119p:plain:w500

セルごとに分かれない

f:id:akiyoko:20171205231145p:plain:w500

文字化け

f:id:akiyoko:20171205231136p:plain:w500


なお、確認した環境は、

  • macOS 10.12.16 + Microsoft Office 365 & Excel for Mac 2011
  • Windows 10 + Microsoft Office 2010

です。



ちなみに、open 時に「newline=''」を指定している理由は、Windows 対策のためです。


(参考)

 

おまけ(Python 2)

# -*- coding: utf-8 -*-
import cStringIO

import codecs
import unicodecsv as csv


class UnicodeWriter:
    """
    A CSV writer which will write rows to CSV file "f",
    which is encoded in the given encoding.
    """

    def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
        # Redirect output to a queue
        self.queue = cStringIO.StringIO()
        self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
        self.stream = f
        self.encoder = codecs.getincrementalencoder(encoding)()

    def writerow(self, row):
        self.writer.writerow([s.encode("utf-8") for s in row])
        # Fetch UTF-8 output from the queue ...
        data = self.queue.getvalue()
        data = data.decode("utf-8")
        # ... and reencode it into the target encoding
        data = self.encoder.encode(data)
        # write to the target stream
        self.stream.write(data)
        # empty queue
        self.queue.truncate(0)

    def writerows(self, rows):
        for row in rows:
            self.writerow(row)


def main():
    rows = [[u'髙﨑 將'], [u'あああ', u'いいい', u'ううう'], [u'Ⅰ・Ⅱ・Ⅲ', u'①②③']]

    with open('test_unicode_writer.csv', 'w') as f:
        w = UnicodeWriter(f, dialect=csv.excel_tab, encoding='utf-16')
        w.writerows(rows)


if __name__ == '__main__':
    main()

https://docs.python.org/2/library/csv.html#examples の UnicodeWriter をそのまま使えばいいよという話ですが、それにしても面倒臭いですよね。いっそ滅んでしまえばいいのに、Python2。



 

まとめ

Excel で直接開いても文字化けしない CSVファイルを Python3 で作成するには、

  • ファイルオープン時に「encoding='utf-16'」と指定
  • csv.writer の引数に「dialect='excel-tab'」と指定

とするのがスマートで確実です。

今回はちょっとレガシーな話題でした。



明日は、driller さんの「python Advent Calendar 2017 - Qiita」 10日目の記事です。
よろしくお願いします。




 

おまけ

文字コードに詳しくなりたい人は、こちらをどうぞ。

*1:《過去記事》akiyoko.hatenablog.jp

*2:《過去記事》akiyoko.hatenablog.jp

*3:《過去記事》akiyoko.hatenablog.jp

*4:ネタが古いですね。図は、「悪循環画像ジェネレータ」を利用させていただきました。

*5:・・はい、私です。

*6:「タブ区切りは CSV じゃなくて TSV だろ」問題がついに解決した - 頭ん中