CSV
python2.7

python 2.7 で csv のカンマを指定した文字に置き換える

経緯

csv ファイルを扱う必要があって、データ内にカンマやエスケープされたダブルクォーテーションを含む時に上手くいかない時があった。sed/awk でちまちま書くとコードが大量かつ見た目に分かりにくくなりそうな雰囲気を自分なりに悟ったんで、何か良い方法がないか探していたら python に csv のモジュールが標準ライブラリとして存在していることを知った。

Python 2.7 標準ライブラリ CSVファイルの読み書き

よく読むとUnicode対応の問題と、NULL文字の問題で素直に使えない部分があるとのことで、使用例で解決策を提示しているから見よ、となっていた。

Python 2.7 使用例

ここらを参考に、CSV ファイルのカラムの内容を指定した区切り文字で区切ったデータに置き換える処理を書いてみた。

実行環境

OS macOS Siera
version Python 2.7.10

目標とする仕様

  1. 処理対象ファイルを複数指定可能であること
  2. 処理対象はファイル以外に標準入力へ出力できること
  3. 処理結果を標準出力へ出力すること
  4. 処理対象ファイル、処理結果はそれぞれのエンコードを指定する文字セットを指定できること
  5. 新たな区切り文字は指定できること
  6. エンコードはデフォルト値として utf-8 で動作すること
  7. 区切り文字はデフォルト値として水平タブで動作すること

使用した Python のモジュール

sys, csv, argparse, codecs, cStringIO

argparse は実行時の引数をコマンドラインから指定したいので使ってみた。
sys は標準入出力のリダイレクトで使用。

実装したスクリプト

replacesep.py
import sys, csv, argparse, codecs, cStringIO

class UTF8Recoder:
  """
  Iterator that reads an encoded stream and reencodes the input to UTF-8
  """
  def __init__(self, f, encoding):
    self.reader = codecs.getreader(encoding)(f)

  def __iter__(self):
    return self

  def next(self):
    return self.reader.next().encode("utf-8")

class UnicodeReader:
  """
  A CSV reader which will iterate over lines in the CSV file "f",
  which is encoded in the given encoding.
  """

  def __init__(self, f, encoding="utf-8"):
    f = UTF8Recoder(f, encoding)
    self.reader = csv.reader(f)

  def next(self):
    row = self.reader.next()
    return [unicode(s, "utf-8") for s in row]

  def __iter__(self):
    return self

parser = argparse.ArgumentParser(description='Replace column separator of csv file', formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-ie', dest='ienc', default='utf-8', help='charset name for input')
parser.add_argument('-oe', dest='oenc', default='utf-8', help='charset name for output')
parser.add_argument('filenames', metavar='csv file', nargs='*', help='csv file path. if no file is specified then read from stdin')
parser.add_argument('-sep', dest='sep', default=r'\t', help='delimiter for replaced')
args = parser.parse_args()
sep = eval('"'+args.sep+'"')

sys.stdout = codecs.getwriter(args.oenc)(sys.stdout)
if args.filenames:
  for f in args.filenames:
    with open(f, 'r') as fd:
      reader = UnicodeReader(fd, args.ienc)
      for r in reader:
        print sep.join(r)
else:
  reader = UnicodeReader(sys.stdin, args.ienc)
  for r in reader:
    print sep.join(r)

簡単な説明

二つのクラスは先に示した Python 2.7 のマニュアルにあった「使用例」に示されていたもの。
リダイレクト周りはネットの情報をいくらか探して試行錯誤の結果。
コマンド行引数の '-sep' のデフォルト値を次のようにヘルプ表示でタブとわかるように表示するために、raw データをデフォルト値に指定した結果、それを通常の文字列とする方法が今ひとつ見つからずちょっと苦労した。 上の実装がベストとも思えないが最低限の目的は達成できたのでとりあえずメモとして記録した次第。

usage: replacesep.py [-h] [-ie IENC] [-oe OENC] [-sep SEP]
                     [csv file [csv file ...]]

Replace column separator of csv file

positional arguments:
  csv file    csv file path. if no file is specified then read from stdin
              (default: None)

optional arguments:
  -h, --help  show this help message and exit
  -ie IENC    charset name for input (default: utf-8)
  -oe OENC    charset name for output (default: utf-8)
  -sep SEP    delimiter for replaced (default: \t)

終わりに

10年ほど前、jython を使う必要があり、 python も少し勉強したことがあったが、 python をきちんと(?)使ったのは今回が初めて。 いろんなスクリプト言語があるけど、改まってインストールしなくても良さげな python をチョイスしたのだが、この処理を実装するベストな選択は果たしてなんだろうか。ご存知の方あれば教えて欲しいです。