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
何だか今さらな感じですが、 古来より「表」のデータを表すときに使われてきた形式。
文字列をコンマで区切った 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形式にしてみます。
とりあえず、これまで見てきたルールで実装してます。
カラムのリスト要素は文字列以外も許す事にして、文字列に変換します。
入力は、標準入力からの文字列を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秋 での「表」の形式がそれです。
番号なしの箇条書き からの
テーブル の形式で
一般的な挨拶 | 朝の場合 | おはよう |
おはようさん | ||
昼の場合 | こんにちは | |
ごきげんさん | ||
夜の場合 | こんばんは | |
おばんです | ||
スポーツ | 野球 | ピッチャー |
バッター | ||
サッカー | キーパー | |
水泳 | クロール | |
平泳ぎ |
こういうヤツです。
いわゆる「大項目」「中項目」「小項目」的な。
テキストのデータは
一般的な挨拶 朝の場合 おはよう おはようさん 昼の場合 こんにちは ごきげんさん 夜の場合 こんばんは おばんです スポーツ 野球 ピッチャー バッター サッカー キーパー 水泳 クロール 平泳ぎ
このように。
字下げの「ざっくりしたレベル」が「カラム位置」と対応してます。
字下げが増えていく分にはカラムが移動。
字下げが同じか減ると改行が入る。
というような処理にしてたはずです。
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"」に。
このバッファの内容で、現在のカラムの文字列は決定!
なぜそうなるか?
なぜそのような仕様になっているか?
プログラムを作ってみるのが、理解の近道かも知れませんね。
CSV形式の文字列sをPythonのリスト形式に変換して返します。
Pythonのリスト(2次元)をCSV形式の文字列に変換して返します。
ezmdの箇条書き形式の文字列sをPythonのリスト形式に変換して返します。
簡易なおれおれマークダウン 2019秋の使用例 / 番号なしの箇条書き / テーブル
Pythonのリスト(2次元)を ezmdの箇条書き形式の文字列に変換して返します。
簡易なおれおれマークダウン 2019秋の使用例 / 番号なしの箇条書き / テーブル
YAML形式の文字列sをPythonのリスト形式に変換して返します。
文字列sはPythonのリスト(2次元)をYAML形式に変換したものを指定します。
Pythonのリスト(2次元)をYAML形式の文字列に変換して返します。
文字列sをPythonのリスト形式に変換して返します。
文字列sはPythonのリスト(2次元)を文字列に変換したものを指定します。
Pythonのリスト(2次元)を文字列に変換して返します。