CSV形式について

2021/MAR/26

更新履歴
日付 変更内容
2020/MAR/17 新規作成
2020/MAR/19 CSV形式にエンコード
2020/MAR/20 箇条書き形式
2020/MAR/21 箇条書きを入力する方向の実装
まとめ
kon_ut風の説明
2020/MAR/22 fix typo
2020/MAR/23 fix typo
2020/MAR/23 余談
2020/MAR/26 fix typo

目次


CSV形式

何だか今さらな感じですが、 古来より「表」のデータを表すときに使われてきた形式。

文字列をコンマで区切った Comma Separated Value なCSV形式について。

ご存知の通り、 1行の文字列が行列の「行」を表して、 「列」(カラム)をコンマ(,)で区切ります。

例えば、CSV形式のデータ

ex1.csv
name,id,color
foo,1,red
bar,2,blue
hoge,3,green

Pythonのプログラムで読み込んで、

[ [ 'name', 'id', 'color' ],
  [ 'foo', '1', 'red' ],
  [ 'bar', '2', 'blue' ],
  [ 'hoge', '3', 'green' ] ]

なリストを作成するならば。

p1.py
#!/usr/bin/env python

import sys

if __name__ == "__main__":
	f = sys.stdin
	s = f.read()
	lst = s.strip().split( '\n' )
	f = lambda s: s.strip().split( ',' )
	lst = list( map( f, lst ) )
	print( lst )
# EOF

実行すると

$ cat ex1.csv | ./p1.py
[['name', 'id', 'color'], ['foo', '1', 'red'], ['bar', '2', 'blue'], ['hoge', '3', 'green']]

そうそう。これだけなら簡単ですね。


区切り文字

行(ロウ)は改行文字で区切って、列(カラム)は','で区切ってます。

では、文字列中に改行文字や','を含めたいときは?

ちょっと脱線

特殊な用途に使う文字のエスケープは、面倒な問題がつきまといます。

pythonの正規表現のモジュールのドキュメントに書いてあったように覚えてますが、 エスケープ文字の増殖問題が挙げられてて「なるほどな」と思いました。

例えば、C言語のソースコードで、文字列の中で'\'は特殊なエスケープ文字として扱われます。

例えば例えば、'\'+'n'は改行文字を表し、'\'自身を表すときは'\'+'\'と表現します。

例えば例えば例えば、hello world。

hello.c

#include <stdio.h>
main()
{
  printf( "hello world\n" );
}

このソースコードのテキストファイルの'\'+'n'の部分は'\'という文字と'n'という文字です。

明らかです。

では、ソースコードを生成するプログラムを作るとしたら?

安易に作るなら

make_hello.c
#include <stdio.h>
main()
{
  char *lst[] = {
    "#include <stdio.h>",
    "main()",
    "{",
    "printf( \"hello world\\n\" );",
    "}",
  };
  int n = sizeof(lst) / sizeof(*lst), i;
  for (i=0; i<n; i++)
    printf( "%s\n", lst[i] );
}

$ gcc -o make_hello make_hello.c
make_hello.c:2:1: warning: type specifier missing, defaults to 'int'
      [-Wimplicit-int]
main()
^
1 warning generated.

コンパイル。警告出つつもバイナリができて。

$ ./make_hello
#include <stdio.h>
main()
{
printf( "hello world\n" );
}
$ ./make_hello > hello.c
$ gcc -o hello hello.c
hello.c:2:1: warning: type specifier missing, defaults to 'int'
      [-Wimplicit-int]
main()
^
1 warning generated.
$ ./hello
hello world

このmake_hellow.cのデータ文字列の

"printf( \"hello world\\n\" );"

を生成するプログラムを書こうとすると、 そのときのソースコードの文字列は、

"(ダブルクォート) --> \"

\(エスケープ文字) --> \\

に変換せねばならず、

printf( "printf( \\\"hello world\\\\n\\\" );\"" );

などと、エスケープ文字がどんどん増殖します。

脱線終わり。

区切り文字含めてみる

実際にやってみます。

ブラウザchromeでGoogle SheetsでCSVを出力してみます。

まずは、普通のばやい

t1.csv
あ,foo
,bar
い,hoge
,fuga

$ hd t1.csv
00000000  e3 81 82 2c 66 6f 6f 0d  0a 2c 62 61 72 0d 0a e3  |...,foo..,bar...|
00000010  81 84 2c 68 6f 67 65 0d  0a 2c 66 75 67 61        |..,hoge..,fuga|
0000001e

改行文字はDOS形式のCRLF (0x0d 0x0a)ですね。

では文字列中に','を入れてみます。

t2.csv
あ,foo
,"bar,bar"
い,hoge
,fuga

$ hd t2.csv
00000000  e3 81 82 2c 66 6f 6f 0d  0a 2c 22 62 61 72 2c 62  |...,foo..,"bar,b|
00000010  61 72 22 0d 0a e3 81 84  2c 68 6f 67 65 0d 0a 2c  |ar".....,hoge..,|
00000020  66 75 67 61                                       |fuga|
00000024

なーるほど。

','を含む文字列は、文字列全体をダブルクォートで囲ってます。

では、文字列に改行を入れた場合は?

そして、ダブルクォートも含めたい場合は?

t3.csv
あ,foo
,"bar,bar"
い,"hoge
hoge"
,"fuga""fuga""fuga"

$ hd t3.csv
00000000  e3 81 82 2c 66 6f 6f 0d  0a 2c 22 62 61 72 2c 62  |...,foo..,"bar,b|
00000010  61 72 22 0d 0a e3 81 84  2c 22 68 6f 67 65 0a 68  |ar".....,"hoge.h|
00000020  6f 67 65 22 0d 0a 2c 22  66 75 67 61 22 22 66 75  |oge"..,"fuga""fu|
00000030  67 61 22 22 66 75 67 61  22                       |ga""fuga"|
00000039

この場合も、改行を含む文字列は、文字列全体をダブルクォートで囲ってます。

そして、含める改行文字はUNIX形式の LF (0x0a) ですね。

文字列中の1つのダブルクォートがある位置には、2つの連続するダブルクォートになってますね。

"fuga""fuga""fuga"

なるほど。

こうしておけば、ダブルクォートが現れたら、次にダブルクォートが現るまでの間は、 コンマも改行文字も特別扱いせずに、文字列データとして扱えば良く。

さらに、特別扱いする場合のコンマで区切った後で、 文字列の先頭と末尾にもしダブルクォートがあれば削除。

文字列中にはダブルクォートが現れるとしたら、2つ連続のセットで現れるはずです。

その連続セットを見つけたら、1つを削除してやればよろしいと。

文字列の先頭や末尾にダブルクォートを含めた場合も、そのルールになっているか、確かめておきます。

t4.csv
あ,"""foo"
,"bar,bar"""
い,"hoge
hoge"
,"fuga""fuga""fuga"

$ hd t4.csv
00000000  e3 81 82 2c 22 22 22 66  6f 6f 22 0d 0a 2c 22 62  |...,"""foo"..,"b|
00000010  61 72 2c 62 61 72 22 22  22 0d 0a e3 81 84 2c 22  |ar,bar""".....,"|
00000020  68 6f 67 65 0a 68 6f 67  65 22 0d 0a 2c 22 66 75  |hoge.hoge"..,"fu|
00000030  67 61 22 22 66 75 67 61  22 22 66 75 67 61 22     |ga""fuga""fuga"|
0000003f

そのルールのようですね。

Google Sheets以外にも、Ubuntuに入ってるLibreOffice Calcで試してみます。

t4.csv を LibraOffice で読み込ませて、CSV形式で保存しなおしてみます。

lo_t4.csv
あ,"""foo"
,"bar,bar"""
い,"hoge
hoge"
,"fuga""fuga""fuga"

$ hd lo_t4.csv
00000000  e3 81 82 2c 22 22 22 66  6f 6f 22 0a 2c 22 62 61  |...,"""foo".,"ba|
00000010  72 2c 62 61 72 22 22 22  0a e3 81 84 2c 22 68 6f  |r,bar"""....,"ho|
00000020  67 65 0a 68 6f 67 65 22  0a 2c 22 66 75 67 61 22  |ge.hoge".,"fuga"|
00000030  22 66 75 67 61 22 22 66  75 67 61 22 0a           |"fuga""fuga".|
0000003d
$ nkf --guess lo_t4.csv
UTF-8 (LF)

この場合、Linuxなので改行コードは全て LF (0x0a) になりました。

他は、Google Sheetsと同じですね。


パーズ

それでは、これまで見てきたルールで文字列を切り出してみます。

p2.py
#!/usr/bin/env python

import sys

import empty

def csv_new():
	e = empty.new()
	e.in_quote = False
	e.s = ''
	e.lst = [ [] ]

	def flush():
		s = e.s
		e.s = ''
		if len( s ) >= 2 and s[ 0 ] == '"' and s[ -1 ] == '"':
			s = s[ 1 : -1 ]
		s = s.replace( '""', '"' )
		e.lst[ -1 ].append( s )

	def add_c( c ):
		if c == '\r':
			return  # (^^;
		if not e.in_quote and c in ',\n':
			flush()
			if c == '\n':
				e.lst.append( [] )
			return
		e.s += c
		if c == '"':
			e.in_quote = not e.in_quote

	def add_s( s ):
		s = s.strip()
		for c in s:
			add_c ( c )

	return empty.add( e, locals() )

if __name__ == "__main__":
	f = sys.stdin
	s = f.read()
	csv = csv_new()
	csv.add_s( s )
	csv.flush()
	print( csv.lst )

# EOF

import empty

pythonのユーティリティ・プログラム 2020冬empty.py を使ってます。

( kon_pageのpythonモジュールのインストール )

$ ./p2.py < t1.csv
[['あ', 'foo'], ['', 'bar'], ['い', 'hoge'], ['', 'fuga']]
$ ./p2.py < t2.csv
[['あ', 'foo'], ['', 'bar,bar'], ['い', 'hoge'], ['', 'fuga']]
$ ./p2.py < t3.csv
[['あ', 'foo'], ['', 'bar,bar'], ['い', 'hoge\nhoge'], ['', 'fuga"fuga"fuga']]
$ ./p2.py < t4.csv
[['あ', '"foo'], ['', 'bar,bar"'], ['い', 'hoge\nhoge'], ['', 'fuga"fuga"fuga']]
$ ./p2.py < lo_t4.csv
[['あ', '"foo'], ['', 'bar,bar"'], ['い', 'hoge\nhoge'], ['', 'fuga"fuga"fuga']]

うまくいきました。


CSV形式にエンコード

逆にリストからCSV形式にしてみます。

とりあえず、これまで見てきたルールで実装してます。

カラムのリスト要素は文字列以外も許す事にして、文字列に変換します。

入力は、標準入力からの文字列をeval()でリストにしてみます。

p3.py
#!/usr/bin/env python

import sys

def to_csv( lst ):
	def cnv_row( cols ):
		def cnv_col( col ):
			s = str( col )
			if '"' in s:
				s = s.replace( '"', '""' )
			if any( map( lambda c: c in s, '",\n' ) ):
				s = '"' + s + '"'
			return s
		return ','.join( map( cnv_col, cols ) )
	return '\n'.join( map( cnv_row, lst ) )

if __name__ == "__main__":
	f = sys.stdin
	s = f.read()
	lst = eval( s )
	s = to_csv( lst )
	print( s )

# EOF

$ echo "[ [ 1, 2, 3 ], [ 'a', 'b', 'c' ] ]" | ./p3.py
1,2,3
a,b,c
$
$ ./p2.py < t4.csv
[['あ', '"foo'], ['', 'bar,bar"'], ['い', 'hoge\nhoge'], ['', 'fuga"fuga"fuga']]

なので

$ ./p2.py < t4.csv | ./p3.py
あ,"""foo"
,"bar,bar"""
い,"hoge
hoge"
,"fuga""fuga""fuga"

改行コードの都合で

$ ./p2.py < lo_t4.csv | ./p3.py
あ,"""foo"
,"bar,bar"""
い,"hoge
hoge"
,"fuga""fuga""fuga"

でも同じ結果で

$ ./p2.py < lo_t4.csv | ./p3.py | diff - lo_t4.csv
$

一致OK。


箇条書き形式

純粋な2次元の「表」というよりは、 入れ子の構造になった章立ての「目次」のような形式の「表」があります。

簡易なおれおれマークダウン 2019秋 での「表」の形式がそれです。

簡易なおれおれマークダウン 2019秋の使用例

番号なしの箇条書き からの

テーブル の形式で

一般的な挨拶 朝の場合 おはよう
おはようさん
昼の場合 こんにちは
ごきげんさん
夜の場合 こんばんは
おばんです
スポーツ 野球 ピッチャー
バッター
サッカー キーパー
水泳 クロール
平泳ぎ

こういうヤツです。

いわゆる「大項目」「中項目」「小項目」的な。

テキストのデータは

一般的な挨拶
  朝の場合
    おはよう
    おはようさん
  昼の場合
    こんにちは
    ごきげんさん
  夜の場合
    こんばんは
    おばんです
スポーツ
  野球
    ピッチャー
    バッター
  サッカー
    キーパー
  水泳
    クロール
    平泳ぎ

このように。

字下げの「ざっくりしたレベル」が「カラム位置」と対応してます。

字下げが増えていく分にはカラムが移動。

字下げが同じか減ると改行が入る。

というような処理にしてたはずです。

Pythonの2次元のリスト形式と、この字下げの形式との 「デコード」「エンコード」も可能にしておきたいなと。

実現できれば、 簡易なおれおれマークダウン 2019秋 の表のデータを、CSV形式にしてエクセルに取り込んだり、 逆に、エクセルの表を 簡易なおれおれマークダウン 2019秋 に貼り付けたりが容易になります。

縦方向のセルの結合の問題

ezmdの箇条書きのデータは、字下げのレベルがカラム位置を表しますが、厳密ではありません。

確か、前の行の字下げ数より「多いか、多くないか」くらいの処理にしてたはずです。

改行直後から、字下げが先頭行の字下げより多い場合。

これは

foo
  bar
    hoge
  fuga  # ここ

fugaの前の行のhogeの字下げのレベルから増えていなくて、 先頭行fooの字下げのレベルよりは、多く字下げされてます。

2次元にすると

foo bar  hoge
    fuga

こうなります。

ここで、ezmdの場合は「大項目」「中項目」「小項目」な「表」を目指してるので、 foo とその直下の空白のカラムは結合します。

foo bar hoge
fuga

この場合はこの扱いで、まぁOKとします。

この結合を「させたく無い」場合は、 引用符で空文字のカラムを作る仕様にしてます。

foo
  bar
    hoge
''
  fuga
foo bar hoge
fuga
foo bar  hoge
    fuga

つまり

[[ 'foo', 'bar', 'hoge' ],
 [ '',    'fuga', ''    ]]

なリストがあった場合、

foo
  bar
    hoge
  fuga

を出力する仕様で「まぁよし」とします。

気になるのは、2次元の表で次のような空欄を含む場合にどうすべきか?

箇条書きでは、想定してないパターンです。

foo  bar  hoge
fuga      guha

素直に解釈すると

foo
  bar
    hoge
fuga
    guha

ですが、guhaの処理では、前のfugaの字下げより「多いね」としか情報が使われてないので

foo bar hoge
fuga guha

そう。

foo
  bar
    hoge
fuga
  guha

とした場合と同じという事で、残念な結果になります。

なのでこの場合は、空欄を表す引用符の空文字が必要で

foo
  bar
    hoge
fuga
  ''
    guha

こう出力すれば

foo bar hoge
fuga guha

このように。

ややこしいですが、まとめると

改行してから、空欄のカラムが続く場合は出力せず。

空欄じゃないカラムが現れると、以降の空欄のカラムは引用符の空文字として出力します。

改行を含めたい場合

あたり前ですが、箇条書きの形式はCSVの形式のように「コンマ」を特別な扱いにしてません。

なので、コンマはカラムの中で自由に使えます。

何でもないような事が幸せです。

ですが、カラムや行を分けるる「改行」は特別扱いしてます。

ezmdの場合、文字列中に'\' + 'n' の2文字でデータとしての「改行」を扱います。

実装お試し

なんか、まだ見落としがいっぱいある気もしますが、とりあえず、このルールで実装してみましょう。

まずは簡単そうな、箇条書きを出力する方向から。

p4.py
#!/usr/bin/env python

import sys

def to_ul( lst ):
	buf = []
	for cols in lst:
		dirty = False
		for ( i, col ) in enumerate( cols ):
			if not col and not dirty:
				continue
			dirty = True
			if not col:
				col = "''"
			s = col.replace( '\n', '\\n' )
			s = ' ' * ( i * 2 ) + s
			buf.append( s )
	return '\n'.join( buf )

if __name__ == "__main__":
	f = sys.stdin
	s = f.read()
	lst = eval( s )
	s = to_ul( lst )
	print( s )

# EOF

$ cat lo_t4.csv | ./p2.py | ./p4.py
あ
  "foo
  bar,bar"
い
  hoge\nhoge
  fuga"fuga"fuga

の出力が得られるので

tbl
tbl_ul
あ
  "foo
  bar,bar"
い
  hoge\nhoge
  fuga"fuga"fuga
/

をezmdに貼り付けると

"foo
bar,bar"
hoge
hoge
fuga"fuga"fuga

ふむ。

$ echo "[[ 'foo', 'bar', 'hoge' ], [ 'fuga', '', 'guha' ]]" | ./p4.py
foo
  bar
    hoge
fuga
  ''
    guha

の出力で

tbl
tbl_ul
foo
  bar
    hoge
fuga
  ''
    guha
/

とezmdに貼り付けると

foo bar hoge
fuga guha

ふむ。

箇条書きを入力する方向の実装

こちらはちょっと難しそうです。

p5.py
#!/usr/bin/env python

import sys

import empty

def ul_new():
	e = empty.new()
	e.lst = [ [] ]
	e.ids = []

	get_spc = lambda s: 1 + get_spc( s[ 1 : ] ) if s and s[ 0 ] == ' ' else 0

	def cnv_nl( s ):
		k = '\\n'
		while k in s:
			i = s.index( k )
			j = i + len( k )
			n = get_spc( s[ j : ] )
			s = s[ : i ] + '\n' + s[ j + n : ]
		return s

	def cnv_qt( s ):
		qts = [ "'", '"' ]
		if len( s ) >= 2 and any( map( lambda qt: s[ 0 ] == qt and s[ -1 ] == qt, qts ) ):
			s = s[ 1 : -1 ]
		return s

	def add_s( s ):
		lst = s.strip().replace( '\\\n', '' ).split( '\n' )
		for s in lst:
			n = get_spc( s )
			s = s[ n : ]
			s = cnv_nl( s )
			s = cnv_qt( s )
			if e.ids and n <= e.ids[ -1 ]:
				e.ids = list( filter( lambda v: v < n, e.ids ) )
				e.lst.append( [] )
				e.lst[ -1 ].extend( [ '' ] * len( e.ids ) )
			e.lst[ -1 ].append( s )
			e.ids.append( n )

	return empty.add( e, locals() )

if __name__ == "__main__":
	f = sys.stdin
	s = f.read()
	ul = ul_new()
	ul.add_s( s )
	print( ul.lst )

# EOF

ul_new()のメモを少々

e.lst 結果の2次元のリスト
e.ids 処理中のrow(行)について、各column(列)の字下げの空白数を記録。
get_spc( s ) 文字列sの先頭から連続する空白の数を返す。
cnv_nl( s ) 文字列sの中の「'\\' + 'n' + 連続する空白」を検出して改行に置き換えて返す。
cnv_qt( s ) 文字列s全体が引用符または二重引用符で囲われていたら、中身の文字列だけにして返す。
add_s( s ) 箇条書き形式の文字列sを与え、e.lstに2次元のリストを生成する。
$ cat lo_t4.csv
あ,"""foo"
,"bar,bar"""
い,"hoge
hoge"
,"fuga""fuga""fuga"

CSVからリストに

$ cat lo_t4.csv  | ./p2.py
[['あ', '"foo'], ['', 'bar,bar"'], ['い', 'hoge\nhoge'], ['', 'fuga"fuga"fuga']]

リストから箇条書きに

$ cat lo_t4.csv  | ./p2.py | ./p4.py
あ
  "foo
  bar,bar"
い
  hoge\nhoge
  fuga"fuga"fuga

箇条書きからリストに

$ cat lo_t4.csv  | ./p2.py | ./p4.py | ./p5.py
[['あ', '"foo'], ['', 'bar,bar"'], ['い', 'hoge\nhoge'], ['', 'fuga"fuga"fuga']]

リストからCSVに

$ cat lo_t4.csv  | ./p2.py | ./p4.py | ./p5.py | ./p3.py
あ,"""foo"
,"bar,bar"""
い,"hoge
hoge"
,"fuga""fuga""fuga"

折り返して戻ってきて

$ cat lo_t4.csv  | ./p2.py | ./p4.py | ./p5.py | ./p3.py | diff - lo_t4.csv
$

一致OK

$ echo "[[ 'foo', 'bar', 'hoge' ], [ 'fuga', '', 'guha' ]]" | ./p4.py
foo
  bar
    hoge
fuga
  ''
    guha

この箇条書きを入力にすると

$ echo "[[ 'foo', 'bar', 'hoge' ], [ 'fuga', '', 'guha' ]]" | ./p4.py | ./p5.py
[['foo', 'bar', 'hoge'], ['fuga', '', 'guha']]

確かに元のリストに。OK。


まとめ

変換の関数をまとめて、ツールに仕立てておきます。

csv_ut.py
#!/usr/bin/env python

import sys
import yaml

import empty

def is_ht( s, c ):
	return len( s ) >= 2 and s[ 0 ] == c and s[ -1 ] == c

def from_csv( s ):
	e = empty.new()
	e.in_quote = False
	e.s = ''
	e.lst = [ [] ]

	def flush():
		s = e.s
		e.s = ''
		if is_ht( s, '"' ):
			s = s[ 1 : -1 ]
		s = s.replace( '""', '"' )
		e.lst[ -1 ].append( s )

	def add_c( c ):
		if c == '\r':
			return
		if not e.in_quote and c in ',\n':
			flush()
			if c == '\n':
				e.lst.append( [] )
			return
		e.s += c
		if c == '"':
			e.in_quote = not e.in_quote

	s = s.strip()
	for c in s:
		add_c( c )
	flush()

	return e.lst

def to_csv( lst ):
	def cnv_row( cols ):
		def cnv_col( col ):
			s = str( col )
			if '"' in s:
				s = s.replace( '"', '""' )
			if any( map( lambda c: c in s, '",\n' ) ):
				s = '"' + s + '"'
			return s
		return ','.join( map( cnv_col, cols ) )
	return '\n'.join( map( cnv_row, lst ) )

def from_ul( s ):
	e = empty.new()
	e.lst = [ [] ]
	e.ids = []

	get_spc = lambda s: 1 + get_spc( s[ 1 : ] ) if s and s[ 0 ] == ' ' else 0

	def cnv_nl( s ):
		k = '\\n'
		while k in s:
			i = s.index( k )
			j = i + len( k )
			n = get_spc( s[ j : ] )
			s = s[ : i ] + '\n' + s[ j + n : ]
		return s

	def cnv_qt( s ):
		qts = [ "'", '"' ]
		if any( map( lambda qt: is_ht( s, qt ), qts ) ):
			s = s[ 1 : -1 ]
		return s

	lst = s.strip().replace( '\\\n', '' ).split( '\n' )
	for s in lst:
		n = get_spc( s )
		s = s[ n : ]
		s = cnv_nl( s )
		s = cnv_qt( s )
		if e.ids and n <= e.ids[ -1 ]:
			e.ids = list( filter( lambda v: v < n, e.ids ) )
			e.lst.append( [] )
			e.lst[ -1 ].extend( [ '' ] * len( e.ids ) )
		e.lst[ -1 ].append( s )
		e.ids.append( n )

	return e.lst

def to_ul( lst ):
	buf = []
	for cols in lst:
		dirty = False
		for ( i, col ) in enumerate( cols ):
			col = str( col )
			if not col and not dirty:
				continue
			dirty = True
			if not col:
				col = "''"
			s = col.replace( '\n', '\\n' )
			s = ' ' * ( i * 2 ) + s
			buf.append( s )
	return '\n'.join( buf )

def from_yaml( s ):
	return yaml.load( s )

def to_yaml( lst ):
	return yaml.dump( lst )

def from_raw( s ):
	return eval( s )

def to_raw( lst ):
	return str( lst )

if __name__ == "__main__":
	from_func = from_csv
	to_func = to_ul

	gdic = globals()
	for a in sys.argv[ 1 : ]:
		if a.startswith( 'from_' ) and a in gdic:
			from_func = gdic.get( a )
		elif a.startswith( 'to_' ) and a in gdic:
			to_func = gdic.get( a )

	f = sys.stdin
	s = f.read()
	lst = from_func( s )
	s = to_func( lst )
	print( s )
# EOF

標準入力からの文字列を指定入力形式としてリストに変換し、 そのリストを指定出力形式の文字列に変換して、 標準出力に出力します。

デフォルトの入力形式はfrom_csv

デフォルトの出力形式はto_csv

方向 指定文字列 形式
入力 from_csv CSV
from_ul ezmdの箇条書き
from_yaml YAML
from_raw Pythonのリスト
出力 to_csv CSV
to_ul ezmdの箇条書き
to_yaml YAML
to_raw Pythonのリスト

CSV --> 箇条書き

$ cat t4.csv | ./csv_ut.py
あ
  "foo
  bar,bar"
い
  hoge\nhoge
  fuga"fuga"fuga
$

CSV --> YAML

$ cat t4.csv | ./csv_ut.py to_yaml
- ["\u3042", '"foo']
- ['', 'bar,bar"']
- ["\u3044", 'hoge

    hoge']
- ['', fuga"fuga"fuga]

$

CSV --> Pythonのリスト

$ $ cat t4.csv | ./csv_ut.py to_raw
[['あ', '"foo'], ['', 'bar,bar"'], ['い', 'hoge\nhoge'], ['', 'fuga"fuga"fuga']]
$

CSV --> CSV

$ cat t4.csv | ./csv_ut.py to_csv
あ,"""foo"
,"bar,bar"""
い,"hoge
hoge"
,"fuga""fuga""fuga"
$

CSV --> UL --> CSV

$ cat t4.csv | ./csv_ut.py | ./csv_ut.py from_ul to_csv
あ,"""foo"
,"bar,bar"""
い,"hoge
hoge"
,"fuga""fuga""fuga"
$

CSV --> YAML --> CSV

$ cat t4.csv | ./csv_ut.py to_yaml | ./csv_ut.py from_yaml to_csv
あ,"""foo"
,"bar,bar"""
い,"hoge
hoge"
,"fuga""fuga""fuga"
$

CSV --> Pythonのリスト --> CSV

$ cat t4.csv | ./csv_ut.py to_raw | ./csv_ut.py from_raw to_csv
あ,"""foo"
,"bar,bar"""
い,"hoge
hoge"
,"fuga""fuga""fuga"
$

pythonのユーティリティ・プログラム 2020冬 に追加しておきます。


余談

ダブルクォート

CSV形式でググろうとすると、「ダブルクォート」がサジェッションされました。

「エクセルがCSVファイルのダブルクォートを勝手に消すときの対処法」 的なページがごろごろと...

少し見てみると...

全てのカラムについて、文字列をダブルクォートで囲んでCSVデータを作成したのに、 エクセルで読み込んでCSV形式で保存しなおすと、ダブルクォートが消されて出力されるので注意!

といった内容のようです。

区切り文字含めてみる のところで試して見た限りではありますが、、、

カラムの文字列に、コンマ(,)、改行文字、ダブルクォート(")の、 いづれかを含む場合に、カラム全体がダブルクォートで囲まれて出力されました。

そして、カラム中のデータとしてのダブククォート(")1つ分は、 2つの連続するダブルクォートに変換されて出力されました。

このように出力しておけば、読み込む時に、ダブルクォートが現れると、 次にダブルクォートが現れるまでの期間は、コンマ(,)や改行文字は、 データとして扱って取り込めるようになって便利〜

というのは、先述の通りです。

エクセルの気持ちになって考えると、 読み込んだカラムの文字列全体がダブルクォートで囲われていたら、 カラムの文字列中にコンマ(,)か改行文字かダブルクォートが使われているために、 「元来のカラム文字列には含まれてなかった」ダブルクォートでの囲いが「追加されている」と思うはず。

なのでCSV形式のデータを読み込む段階で、まず外側のダブルクォートを外すと思います。

(なので「CSVで保存しなおすとダブルクォートが消された」は、ちょっと違うかも、です)

外した後は、カラム中に、コンマ(,)や改行文字、あるいは連続する2つのダブルクォートが含まれている事を想定していると思いますが、、、

エクセルの気持ちになると、あとは、連続する2つのダブルクォートがあれば1つに戻すくらいしかやる事がありません。

もし、カラム中にコンマ(,)も改行文字も連続する2つのダブルクォートも無い事を検出したら、 「これは変だ」と思って、外側のダブルクォートの囲みを復活させる、、、という処理も出来なくは無い。

でも、そうなると、、、読み込む時に楽するために「外側にダブルクォートを付加する仕様」にしたはずなのに、 そこまで苦労して不正な形式の対応の処理をするのも本末転倒かも。

そう、つい書いてしまいましたが、 カラム中にコンマ(,)も改行文字も連続する2つのダブルクォートも無いのに、 カラム全体をダブルクォートで囲むのは、不正な形式だと思います。

外側のダブルクォートを含むカラムにするのであれば、

区切り文字含めてみる で試した結果からすると、例えば

"""hello world""",

のように、3つ連続するダブルクォートで囲めば、不正じゃないはずです。

エクセルの気持ちになると、 1つめの"から2つめの"までの「""」をバッファに。

3つめの"をみて4つめの"までの「"hello world"」をバッファに追加して、 バッファは「"""hello world"」に。

5つめの"から6つめ"までの「""」をバッファに追加して、 バッファは「"""hello wolrd"""」

次に','をみて、「カラムの区切り」だ。

バッファの「"""hello world"""」をみると、ダブルクォートで囲われている。

コンマ(,)や改行文字あるいはダブルクォートをカラムに含めるために、 付加したダブルクォートで囲われている「はず」だろう。

なので、バッファの外側のダブルクォートは削除して、バッファは「""hello world""」に。

連続する2つのダブルクォートがあれば、1つに戻しておこう。バッファは「"hello world"」に。

このバッファの内容で、現在のカラムの文字列は決定!

なぜそうなるか?

なぜそのような仕様になっているか?

プログラムを作ってみるのが、理解の近道かも知れませんね。


kon_ut風の説明

kon_ut

csv_ut.py

from_csv( s )

CSV形式の文字列sをPythonのリスト形式に変換して返します。

to_csv( lst )

Pythonのリスト(2次元)をCSV形式の文字列に変換して返します。

from_ul( s )

ezmdの箇条書き形式の文字列sをPythonのリスト形式に変換して返します。

簡易なおれおれマークダウン 2019秋の使用例 / 番号なしの箇条書き / テーブル

to_ul( lst )

Pythonのリスト(2次元)を ezmdの箇条書き形式の文字列に変換して返します。

簡易なおれおれマークダウン 2019秋の使用例 / 番号なしの箇条書き / テーブル

from_yaml( s )

YAML形式の文字列sをPythonのリスト形式に変換して返します。

文字列sはPythonのリスト(2次元)をYAML形式に変換したものを指定します。

to_csv( lst )

Pythonのリスト(2次元)をYAML形式の文字列に変換して返します。

from_raw( s )

文字列sをPythonのリスト形式に変換して返します。

文字列sはPythonのリスト(2次元)を文字列に変換したものを指定します。

to_raw( lst )

Pythonのリスト(2次元)を文字列に変換して返します。