CSV、JSON、XMLは、データをプレーンテキストで保存するために用いる直列化フォーマットです。データを直列化して文字列に変換して保存します。プログラムでの作業をテキストファイルに保存して、インターネットで転送したり、メールに貼り付けたりできるようになります。Pythonには、これらのファイルフォーマットを扱いやすくしてくれるcsvとjsonとxmlのモジュールが同梱されています。
これらのフォーマットのファイルは、本質的に、Pythonのopen()関数その他の第10章で紹介した入出力関数で読み書きできるテキストファイルです。しかし、第13章でHTML形式のテキストを扱うのにBeautiful Soupを使ったように、Pythonの専用モジュールを利用するほうが簡便です。この3つのフォーマットには以下のような特徴があります。
CSV (Comma Separated Valuesの略でシーエスブイと発音します)は、スプレッドシートを単純化したフォーマットで、同じカラムを持つ複数の行のデータを保存するのに最適です。
JSON (JavaScript Object Notationの略でジェイソンと発音します)は、JavaScriptプログラミング言語のオブジェクトや配列というデータ型と同じ構文を使いますが、JavaScriptでのプログラミングを知らなくても大丈夫です。XMLよりシンプルになるように作られました。
XML (Extensible Markup Languageの略でエックスエムエルと発音します)は、比較的古く、構造がはっきりとしたデータ直列化フォーマットで、企業向けソフトウェアで広く用いられています。高度な機能が必要でなければ複雑すぎます。
本章では、これらのフォーマットの基本とPythonのコードを紹介します。
(.csvファイル拡張子の)CSVファイルの各行は、スプレッドシートの行と同じようなものであり、行内のセルはカンマで区切られています。例えば、https://
4/5/2035 13:34,Apples,73
4/5/2035 3:41,Cherries,85
4/6/2035 12:46,Pears,14
4/8/2035 8:59,Oranges,52
4/10/2035 2:07,Apples,152
4/10/2035 18:10,Bananas,23
4/10/2035 2:40,Strawberries,98
本章のCSVの対話型シェルの例ではこのファイルを使用します。ダウンロードするか上記のテキストをexample3.csvという名前で保存してください。
CSVファイルは値のリストのリスト(二次元リスト)だと考えることができます。Pythonのコードは[['4/5/2035 13:34', 'Apples', '73'], ['4/5/2035 3:41', 'Cherries', '85'], ... ['4/10/2035 2:40', 'Strawberries', '98']]のような値としてexample3.csvの内容を表現できます。CSVファイルはシンプルであり、以下に例示するようにExcelファイルに存在する機能の多くが欠けています。
CSVファイルにはシンプルであるという優位性があります。多くのアプリやプログラミング言語でサポートされていますし、(Muなどの)テキストエディタで中身を確認できますし、スプレッドシートのデータを直接表現できます。
CSVファイルはテキストファイルですから、文字列として読み取ってその文字列を第8章で学んだテクニックで処理しようと思われるかもしれません。例えば、CSVファイルの各セルはカンマで区切られていますから、各行についてsplit(',')を呼び出してカンマ区切りの値を文字列のリストとして取得しようと考えるかもしれません。しかし、CSVファイルのカンマがすべてセルの区切りを表しているとは限りません。CSVファイルには、カンマその他の記号を値の中に含められるエスケープ記号があります。split()メソッドはこうしたエスケープ記号を考慮しません。こうした潜在的な落とし穴があるため、csvモジュールを使ったほうがCSVファイルの読み書きを信頼して行えます。
CSVファイルを読み取るには、CSVファイルの行を反復処理できるcsv.readerオブジェクトを作成します。csvモジュールはPythonに同梱されていますから、改めてインストールしなくてもインポートできます。example3.csvを現在の作業ディレクトリに置いてから、対話型シェルで以下の内容を実行してください。
>>> import csv
>>> example_file = open('example3.csv')
>>> example_reader = csv.reader(example_file)
>>> example_data = list(example_reader)
>>> example_data
[['4/5/2035 13:34', 'Apples', '73'], ['4/5/2035 3:41', 'Cherries', '85'],
['4/6/2035 12:46', 'Pears', '14'], ['4/8/2035 8:59', 'Oranges', '52'],
['4/10/2035 2:07', 'Apples', '152'], ['4/10/2035 18:10', 'Bananas', '23'],
['4/10/2035 2:40', 'Strawberries', '98']]
>>> example_file.close()
csvモジュールでCSVファイルを読み取るには、他のテキストファイルと同じように、open()関数でファイルを開きます。次に、open()が返すFileオブジェクトについてread()メソッドやreadlines()メソッドを呼び出すのではなく、Fileオブジェクトをcsv.reader()関数に渡します。この関数はreaderオブジェクトを返します。csv.reader()関数にファイル名の文字列を直接渡すことはできません。
readerオブジェクト内の値にアクセスする最も簡単な方法は、list()に渡してPythonのリストに変換することです。readerオブジェクトについてlist()を使うとリストのリストが返され、これをexample_dataのような変数に格納できます。シェルでexample_dataと入力するとリストのリストが表示されます。
これでCSVファイルをリストのリストにできましたから、example_data[row][col]という式で行と列を指定して値にアクセスできます。rowはexample_dataリストのインデックスで、colはそのリストから取得する要素のインデックスです。以下の式を対話型シェルに入力してみてください。
>>> example_data[0][0] # 1行目の1列目
'4/5/2035 13:34'
>>> example_data[0][1] # 1行目の2列目
'Apples'
>>> example_data[0][2] # 1行目の3列目
'73'
>>> example_data[1][1] # 2行目の2列目
'Cherries'
>>> example_data[6][1] # 7行目の2列目
'Strawberries'
この出力からわかるように、example_data[0][0]は最初のリストの最初の文字列で、example_data[0][2]は最初のリストの3つ目の文字列です。
大きなCSVファイルでは、(リストのリストに変換せずに)readerオブジェクトをforループで使います。そうするとファイル全体を一度にメモリにロードしなくてすみます。対話型シェルで次のように入力してみてください。
>>> import csv
>>> example_file = open('example3.csv')
>>> example_reader = csv.reader(example_file)
❶ >>> for row in example_reader:
... ❷ print('Row #' + str(example_reader.line_num) + ' ' + str(row))
...
Row #1 ['4/5/2035 13:34', 'Apples', '73']
Row #2 ['4/5/2035 3:41', 'Cherries', '85']
Row #3 ['4/6/2035 12:46', 'Pears', '14']
Row #4 ['4/8/2035 8:59', 'Oranges', '52']
Row #5 ['4/10/2035 2:07', 'Apples', '152']
Row #6 ['4/10/2035 18:10', 'Bananas', '23']
Row #7 ['4/10/2035 2:40', 'Strawberries', '98']
csvモジュールをインポートしてCSVファイルからreaderオブジェクトを作成すると、readerオブジェクトの行をループで回せます(❶)。各行は変数rowに格納される値のリストで、そのリスト内の各値がセルに相当します。
print()関数を呼び出して現在の行番号とその行の内容を表示しています(❷)。readerオブジェクトのline_num属性で整数の行番号を取得できます。CSVファイルの最初の行が見出し行なら、line_numで最初の行かどうかを判定し、continueでヘッダー行を飛ばすことができます。Pythonのリストのインデックスとは異なり、line_numの行番号は0ではなく1から始まります。
readerオブジェクトは1回に限りループできます。CSVファイルをもう一度読み込みたければ、open()とcsv.reader()をもう一度呼び出して別のreaderオブジェクトを作成します。
csv.writerオブジェクトではCSVファイルへの書き込みができます。writerオブジェクトを作成するには、csv.writer()関数を使います。以下の式を対話型シェルに入力してみてください。
>>> import csv
❶ >>> output_file = open('output.csv', 'w', newline='')
❷ >>> output_writer = csv.writer(output_file)
>>> output_writer.writerow(['spam', 'eggs', 'bacon', 'ham'])
21
>>> output_writer.writerow(['Hello, world!', 'eggs', 'bacon', 'ham'])
32
>>> output_writer.writerow([1, 2, 3.141592, 4])
16
>>> output_file.close()
'w'を渡してopen()を呼び出し書き込みモードでファイルを開きます(❶)。それをcsv.writer()に渡してwriterオブジェクトを作成します(❷)。
Windowsでは、open()関数のnewlineキーワード引数に空文字列を渡す必要があります。技術的な理由により(本書では扱いません)、newline引数を指定しないと、output.csvの行が図18-1のように1行おきになってしまいます。
図 18-1:1行おきのCSVファイル
writerオブジェクトのwriterow()メソッドはリストを引数に取ります。リストの各値が出力CSVファイルの各セルに入ります。このメソッドの返り値は、ファイルに書き込んだその行の文字数(改行文字を含む)です。例えば、先ほどの例のコードを実行すると、以下のようなoutput.csvファイルが作成されます。
spam,eggs,bacon,ham
"Hello, world!",eggs,bacon,ham
1,2,3.141592,4
writerオブジェクトは自動的に値'Hello, world!'中のカンマをダブルクォートでエスケープすることに注目してください。csvモジュールを使うと、こうした特殊例を自分で処理しなくてすみます。
TSV(Tab-separated value)ファイルは、CSVファイルと似ていますが、カンマではなくタブで区切られており、.tsvというファイル拡張子です。カンマではなくタブでセルを区切り、1行おきにしたいとすると、対話型シェルで以下のような内容を実行します。
>>> import csv
>>> output_file = open('output.tsv', 'w', newline='')
>>> output_writer = csv.writer(output_file, delimiter='\t', lineterminator='\n\n') ❶
>>> output_writer.writerow(['spam', 'eggs', 'bacon', 'ham'])
21
>>> output_writer.writerow(['Hello, world!', 'eggs', 'bacon', 'ham'])
30
>>> output_writer.writerow([1, 2, 3.141592, 4])
16
>>> output_file.close()
このコードでは、区切りと行末記号を変更しています。delimiter(区切り)は行の中のセルとセルの間を区切る記号です。デフォルトでは、CSVファイルの区切りはカンマです。line terminator(行末記号)は行の最後に来る記号です。デフォルトでは改行記号です。csv.writer()のdelimiterとlineterminatorのキーワード引数を使えばその記号を変更できます。
delimiter='\t'とlineterminator='\n\n'を渡すと(❶)、区切りをタブに、行末記号を2つの改行記号に変更します。writerow()を3回呼び出して3行作成し、以下の内容のoutput.tsvという名前のファイルを作成します。
spam eggs bacon ham
Hello, world! eggs bacon ham
1 2 3.141592 4
タブがセルを区切っています。
見出し行を含むCSVファイルでは、readerオブジェクトとwriterオブジェクトよりも、DictReaderオブジェクトとDictWriterオブジェクトを使うほうが便利です。readerとwriterはリストでCSVファイルを読み書きするのに対し、DictReaderとDictWriterは最初の行をキーとする辞書で読み書きします。
次の例のために、本書のオンライン素材からexampleWithHeader3.csvをダウンロードしてください。このファイルは最初の行にTimestamp、Fruit、Quantityという見出し行がある以外はexample3.csvと同じです。対話型シェルに次の内容を入力してこのファイルを読み取ってください。
>>> import csv
>>> example_file = open('exampleWithHeader3.csv')
>>> example_dict_reader = csv.DictReader(example_file)
❶ >>> example_dict_data = list(example_dict_reader)
>>> example_dict_data
[{'Timestamp': '4/5/2035 3:41', 'Fruit': 'Cherries', 'Quantity': '85'},
{'Timestamp': '4/6/2035 12:46', 'Fruit': 'Pears', 'Quantity': '14'},
{'Timestamp': '4/8/2035 8:59', 'Fruit': 'Oranges', 'Quantity': '52'},
{'Timestamp': '4/10/2035 2:07', 'Fruit': 'Apples', 'Quantity': '152'},
{'Timestamp': '4/10/2035 18:10', 'Fruit': 'Bananas', 'Quantity': '23'},
{'Timestamp': '4/10/2035 2:40', 'Fruit': 'Strawberries', 'Quantity': '98'}]
>>> example_file = open('exampleWithHeader3.csv')
>>> example_dict_reader = csv.DictReader(example_file)
❷ >>> for row in example_dict_reader:
... print(row['Timestamp'], row['Fruit'], row['Quantity'])
...
4/5/2035 13:34 Apples 73
4/5/2035 3:41 Cherries 85
4/6/2035 12:46 Pears 14
4/8/2035 8:59 Oranges 52
4/10/2035 2:07 Apples 152
4/10/2035 18:10 Bananas 23
4/10/2035 2:40 Strawberries 98
DictReaderオブジェクトをlist()に渡すことにより、CSVのデータを辞書のリストとして取得します(❶)。各行がリスト中の一つの辞書に対応します。あるいは、DictReaderオブジェクトをforループ内で使うこともできます(❷)。DictReaderオブジェクトのrowは、最初の行の見出しから取ってきたキーを持つ辞書オブジェクトです。DictReaderオブジェクトを使えば、最初の行の見出し情報を飛ばすコードを書かずにすみます。DictReaderオブジェクトがその処理をしてくれます。
最初の行が見出しではないexample3.csvでDictReaderオブジェクトを使おうとすると、DictReaderオブジェクトは'4/5/2035 13:34'、'Apples'、'73'を辞書のキーに使います。これを避けるためには、DictReader()に見出しの名前を含む第二引数を渡します。
>>> import csv
>>> example_file = open('example3.csv')
>>> example_dict_reader = csv.DictReader(example_file, ['time', 'name', 'amount'])
>>> for row in example_dict_reader:
... print(row['time'], row['name'], row['amount'])
...
4/5/2035 13:34 Apples 73
4/5/2035 3:41 Cherries 85
4/6/2035 12:46 Pears 14
4/8/2035 8:59 Oranges 52
4/10/2035 2:07 Apples 152
4/10/2035 18:10 Bananas 23
4/10/2035 2:40 Strawberries 98
example3.csvの最初の行は見出しではありませんので、自分で'time'、'name'、'amount'という見出しを作りました。DictWriterオブジェクトはCSVファイルの作成に辞書を使います。
>>> import csv
>>> output_file = open('output.csv', 'w', newline='')
>>> output_dict_writer = csv.DictWriter(output_file, ['Name', 'Pet', 'Phone'])
>>> output_dict_writer.writeheader()
16
>>> output_dict_writer.writerow({'Name': 'Alice', 'Pet': 'cat', 'Phone': '555-1234'})
20
>>> output_dict_writer.writerow({'Name': 'Bob', 'Phone': '555-9999'})
15
>>> output_dict_writer.writerow({'Phone': '555-5555', 'Name': 'Carol', 'Pet': 'dog'})
20
>>> output_file.close()
作成するファイルに見出し行を含めたければ、writeheader()を呼び出します。writeheader()を呼び出さなければ、ファイルに見出し行は含まれません。writerow()メソッドを呼び出すとCSVファイルの各行を書き込めます。見出しをキーとする辞書のデータを渡して書き込みます。
このコードは次のようなoutput.csvファイルを作成します。
Name,Pet,Phone
Alice,cat,555-1234
Bob,,555-9999
Carol,dog,555-5555
カンマが2つ続いているのは、Bobのpetの値が空白であることを示しています。writerow()に渡した辞書のキーと値のペアの順番は関係ありません。DictWriter()に渡したキーの順序で書き込まれます。例えば、Carolの行のPhoneのキーと値は、NameとPetのキーと値よりも前に渡していますが、それでも電話番号は出力の最後に来ています。
{'Name': 'Bob', 'Phone': '555-9999'}の'Pet'のようにキーがなければ、CSVファイルでは空のセルになります。
プロジェクト13:CSVファイルから見出しを取り除く
数百のCSVファイルから最初の行を取り除くという退屈な作業があるとします。すでに自動化された別の工程で見出し行ではなくデータだけを読み取らせるためにその作業が必要になるのでしょう。Excelでファイルを一つずつ開き、最初の行を削除して、上書き保存することはできます。しかし数時間かかります。その作業をしてくれるプログラムを書きましょう。
現在の作業ディレクトリにある.csv拡張子のファイルをすべて開き、そのCSVファイルの内容を読み取って、最初の行以外を同じ名前のファイルに書き出します。これで元ファイルの内容から見出し行のない新しいファイルを作成できます。
警告
ファイルを変更するプログラムを書く際は、プログラムが期待通りに動かない場合に備えて、必ず先にファイルのバックアップをしてください。元ファイルを誤って消してしまいたくはないはずです。
このプログラムには以下の内容が必要です。
コードには以下の内容が必要になります。
このプロジェクト用に、新しいファイルエディタウィンドウを開いてremoveCsvHeader.pyという名前で保存してください。
このプログラムでは、最初に現在の作業ディレクトリにあるすべてのCSVファイル名のリストを反復処理します。removeCsvHeader.pyはこのようになります。
# CSVファイルから見出しを取り除く
import csv, os
os.makedirs('headerRemoved', exist_ok=True)
# 現在の作業ディレクトリのすべてのファイルを反復処理する
for csv_filename in os.listdir('.'):
if not csv_filename.endswith('.csv'):
❶ continue # 非CSVファイルは飛ばす
print('Removing header from ' + csv_filename + '...')
# TODO:CSVファイルの読み取り(見出し行は飛ばす)
# TODO:CSVファイルの書き込み
os.makedirs()呼び出しにより、見出し行のないCSVファイルを保存するheaderRemovedフォルダを作成します。os.listdir('.')についてforループを実行すればよさそうですが、これでは現在の作業ディレクトリのすべてのファイルを反復処理してしまうので、.csvで終わらないファイル名を飛ばすコードをループの冒頭に書く必要があります。非CSVファイルの場合はcontinue文(❶)でforループを次のファイル名に進めます。
プログラムを実行したときに出力を確認できるように、処理しているCSVファイルを示すメッセージを表示します。それから、プログラムの残りの部分のTODOコメントを書き加えます。
このプログラムでは、CSVファイルの最初の行を削除するのではありません。そうではなく、最初の行を除くCSVファイルの新しいコピーを作成します。このようにすれば、バグにより新しいファイルを誤って作成しても、元ファイルが残ります。
このプログラムでは最初の行を処理しているのかどうかを判定しなければなりません。removeCsvHeader.pyに次の内容を追加します。
# CSVファイルから見出しを取り除く
import csv, os
--snip--
# CSVファイルの読み取り(見出し行は飛ばす)
csv_rows = []
csv_file_obj = open(csv_filename)
reader_obj = csv.reader(csv_file_obj)
for row in reader_obj:
if reader_obj.line_num == 1:
continue # 見出し行は飛ばす
csv_rows.append(row)
csv_file_obj.close()
# TODO:CSVファイルの書き込み
readerオブジェクトのline_num属性を使えば、現在読み取っているのがCSVファイルの何行目かを判定できます。内側のforループはreaderオブジェクトが返す行を反復処理し、最初の行以外のすべての行がcsv_rowsに追加されます。
forループは各行を反復処理しますから、このコードではreader_obj.line_numが1かどうかをチェックしています。条件を満たせばcontinueを実行してcsv_rowsに追加せずに次の行に進みます。以降の行はすべて条件がFalseになるので、その行をcsv_rowsに追加します。
csv_rowsには最初の行以外のすべての行が入っていますから、このリストをheaderRemovedフォルダ内のCSVファイルに書き込みます。removeCsvHeader.pyに以下の内容を加えてください。
# CSVファイルから見出しを取り除く
import csv, os
--snip--
# 現在の作業ディレクトリのすべてのファイルを反復処理する
❶ for csv_filename in os.listdir('.'):
if not csv_filename.endswith('.csv'):
continue # 非CSVファイルは飛ばす
--snip--
# CSVファイルの書き込み
csv_file_obj = open(os.path.join('headerRemoved', csv_filename), 'w',
newline='')
csv_writer = csv.writer(csv_file_obj)
for row in csv_rows:
csv_writer.writerow(row)
csv_file_obj.close()
writerオブジェクトは、リストをheaderRemovedフォルダ内にcsv_filenameという名前(CSVを読み取るときに使った名前)でCSVファイルに書き込みます。writerオブジェクトを作成してから、csv_rowsに格納されたリストを反復処理してファイルに書き込みます。
外側のforループ(❶)は、os.listdir('.')が返した次のファイル名に進みます。このループが終了すれば、プログラムは完了です。
本書のオンライン素材からremoveCsvHeader.zipをダウンロードしてから展開してこのプログラムをテストしてください。展開したフォルダ内でremoveCsvHeader.pyを実行すると、出力は次のようになります。
Removing header from NAICS_data_1048.csv...
Removing header from NAICS_data_1218.csv...
--snip--
Removing header from NAICS_data_9834.csv...
Removing header from NAICS_data_9986.csv...
このプログラムは、CSVファイルから最初の行を取り除くたびにファイル名を表示します。
CSVファイルを操作するプログラムはExcelファイルを操作するプログラムと似ています。CSVもExcelもスプレッドシートファイルだからです。例えば、以下のようなプログラムを書くことができます。
CSVファイルはカラムが共通しているデータの行を保存するのに便利ですが、JSONやXMLフォーマットではさまざまなデータ構造を保存できます(本書ではJSONやXMLほど一般的に使われないYAMLとTOMLフォーマットは扱いません)。これらはPythonに固有のフォーマットではなく、多くのプログラミング言語の関数で読み書きできます。
これらのフォーマットでは、Pythonの辞書とリストを入れ子にしたものに相当する構造でデータを整理します。他のプログラミング言語では、Pythonで言うところの辞書を、マッピング、ハッシュマップ、ハッシュテーブル、連想配列と呼ぶことがあります(キーと値をマッピングしたり連想させたりするのでこのように呼ばれます)。同様に、他のプログラミング言語では、Pythonで言うところのリストを配列と呼ぶことがあります。呼び方は違っていても考え方は同じです。データをキーと値のペアやリストに整理します。
辞書とリストは、別の辞書やリストの内部に入れ子にして複雑なデータ構造を形成できます。しかし、そのデータ構造をテキストファイルに保存したいなら、JSONやXMLのようなデータ直列化フォーマットを選択しなければなりません。本章で紹介するPythonのモジュールは、これらのフォーマットで記述されたテキストを解析(読み取って理解)して、Pythonのデータ構造を作成できます。
これらの人間に読めるプレーンテキストのフォーマットは、ディスク容量やメモリを効率的に利用していませんが、テキストエディタで簡単に見て編集でき、プログラミング言語に中立だという利点があります。どのプログラミング言語で書かれたプログラムでもテキストファイルを読み書きはできます。対照的に、第10章で紹介したshelveモジュールは、あらゆるPythonのデータ型をバイナリshelfファイルで保存できますが、他のプログラミング言語にはこのデータをプログラムにロードできるモジュールがありません。
本章の以降の部分では、Aliceという人物についての個人情報を保存した以下のPythonのデータ構造を例に取り、JSONとXMLのフォーマットを比較対照します。
{
"name": "Alice Doe",
"age": 30,
"car": None,
"programmer": True,
"address": {
"street": "100 Larkin St.",
"city": "San Francisco",
"zip": "94102"
},
"phone": [
{
"type": "mobile",
"number": "415-555-7890"
},
{
"type": "work",
"number": "415-555-1234"
}
]
}
これらのテキストフォーマットには固有の歴史があり、コンピュータ関係の生態系の中で独自の地位を占めています。データを保存する直列化フォーマットを選ぶ必要があるなら、JSONがXMLよりもシンプルでYAMLよりも広く採用されており、TOMLは主に設定ファイル用のフォーマットとして用いられているということを踏まえるとよいでしょう。独自のデータ直列化フォーマットを編み出そうと考えることがあるかもしれませんが、それは車輪の再発明であり、その独自フォーマット用のパーサーを書かなければならなくなるので、おすすめしません。既存のフォーマットから選ぶのがベターです。
JSONは情報をJavaScriptのソースコードとして保存します。JavaScript以外の多くのアプリケーションでも利用されています。特に、ウェブサイトは、第13章で紹介したOpenWeather APIのようなAPIを通じて、JSON形式でプログラマーにデータを提供している場合が多いです。ここではJSON形式のプレーンテキストのファイルを.jsonというファイル拡張子で保存します。JSONテキストでフォーマットしたデータ構造の例を示します。
{
"name": "Alice Doe",
"age": 30,
"car": null,
"programmer": true,
"address": {
"street": "100 Larkin St.",
"city": "San Francisco",
"zip": "94102"
},
"phone": [
{
"type": "mobile",
"number": "415-555-7890"
},
{
"type": "work",
"number": "415-555-1234"
}
]
}
JSONはPythonの構文に似ていることにすぐ気づくでしょう。Pythonの辞書とJSONのオブジェクトは、どちらも、波かっこを使い、キーと値のペアをコロンで区切ります。PythonのリストとJSONの配列は、どちらも、角かっこを使い、カンマ区切りの値を含みます。JSONでは、ダブルクォートで囲まれた文字列以外でスペースは意味を持ちません。どのようにでもスペースを入れられます。とはいえ、Pythonのコードのようにブロックをインデントして入れ子のオブジェクトや配列を見やすくしたほうがよいです。先ほどの例では、電話番号のリストを2文字分インデントしています。リスト中のそれぞれの電話番号の辞書は4文字分インデントしています。
スペースの使い方以外にもJSONとPythonの違いがあります。PythonのNone値の代わりに、JSONではJavaScriptのキーワードnullを使います。ブール値はJavaScriptで小文字のキーワードtrueとfalseで表します。JSONではJavaScriptのコメントや複数行文字列を記入することができません。JSON中での文字列はすべてダブルクォートで囲まなければなりません。Pythonのリストとは異なり、JSONの配列は末尾にカンマを入れられません。["spam", "eggs"]は正しいJSONですが、["spam", "eggs",]は正しくありません。
Facebook、Twitter、Yahoo!、Google、Tumblr、Wikipedia、Flickr、Data.gov、Reddit、IMDb、Rotten Tomatoes、LinkedInその他の人気サイトではJSONデータでAPIを提供しています。これらのサイトの中には登録が要求されるものがありますが、たいてい無料です。ほしいデータを取得するためにプログラムからリクエストを送信するURLや、返されるJSONデータの構造の一般的なフォーマットなどを説明したドキュメントがあるはずです。APIを提供しているサイトに開発者ページがあれば、そこでドキュメントを探してください。
Pythonのjsonモジュールは、json.loads()関数とjson.dumps()関数で、JSON形式の文字列とそれに対応するPythonの値との間の変換の細かい処理をしてくれます。JSONにすべての種類のPythonの値を保存できるわけではなく、文字列型、整数型、浮動小数点数型、ブール型、リスト型、辞書型、None型の基本的な型しか保存できません。JSONでは、FileオブジェクトやCSVのreaderオブジェクトとwriterオブジェクトや、SeleniumのWebElementオブジェクトのような、Python固有のオブジェクトを表すことはできません。jsonモジュールのドキュメントはhttps://
JSONデータを含む文字列をPythonの値に変換するには、json.loads()関数にその文字列を渡します(“loads”ではなく“load string”という意味です)。
❶ >>> import json
>>> json_string = '{"name": "Alice Doe", "age": 30, "car": null, "programmer":
true, "address": {"street": "100 Larkin St.", "city": "San Francisco", "zip":
"94102"}, "phone": [{"type": "mobile", "number": "415-555-7890"}, {"type":
"work", "number": "415-555-1234"}]}'
❷ >>> python_data = json.loads(json_string)
>>> python_data
{'name': 'Alice Doe', 'age': 30, 'car': None, 'programmer': True, 'address':
{'street': '100 Larkin St.', 'city': 'San Francisco', 'zip': '94102'},
'phone': [{'type': 'mobile', 'number': '415-555-7890'}, {'type': 'work',
'number': '415-555-1234'}]}
jsonモジュールをインポートして(❶)、JSONデータの文字列を渡してloads()を呼び出します(❷)。JSON文字列はダブルクォートを使うことに注意してください。この関数は、Pythonの辞書としてデータを返すはずです。
Pythonの値をJSON形式の文字列に変換するには、json.dumps()関数を使います(“dumps”ではなく“dump string”という意味です)。以下の式を対話型シェルに入力してみてください。
>>> import json
>>> python_data = {'name': 'Alice Doe', 'age': 30, 'car': None, 'programmer': True, 'address':
{'street': '100 Larkin St.', 'city': 'San Francisco', 'zip': '94102'}, 'phone': [{'type':
'mobile', 'number': '415-555-7890'}, {'type': 'work', 'number': '415-555-1234'}]}
>>> json_string = json.dumps(python_data) ❶
>>> print(json_string) ❷
{"name": "Alice Doe", "age": 30, "car": null, "programmer": true, "address": {"street":
"100 Larkin St.", "city": "San Francisco", "zip": "94102"}, "phone": [{"type": "mobile",
"number": "415-555-7890"}, {"type": "work", "number": "415-555-1234"}]}
>>> json_string = json.dumps(python_data, indent=2) ❸
>>> print(json_string)
{
"name": "Alice Doe",
"age": 30,
"car": null,
"programmer": true,
"address": {
"street": "100 Larkin St.",
"city": "San Francisco",
--snip--
}
json.dumps()に渡す値(❶)には、文字列型、整数型、浮動小数点数型、ブール型、リスト型、辞書型、None型の基本的な型しか含められません。
デフォルトでは、JSONテキスト全体が1行で書き込まれます(❷)。このように詰め込まれた形でもプログラムがJSONを読み書きするには問題ありませんが、複数行でインデントされた形式のほうが人間にとっては読みやすいです。indent=2キーワード引数(❸)を指定すると、JSONテキストが複数行になり、辞書やリストの入れ子ではスペース2文字分インデントされます。JSONがメガバイトを超えるような大きさでなければ、スペースや改行を追加してサイズが増えても読みやすくする価値があります。
JSONテキストをPythonの文字列値に変換すれば、.jsonファイルに書き込んだり、関数に渡したり、ウェブのリクエストに使ったり、その他文字列でできることは何でもできます。
XMLはJSONよりも古いフォーマットですが、今でも広く用いられています。構文は、第13章で紹介したHTMLと似ており、山かっこの開始タグと終了タグの間に内容を入れます。こうしたタグのことを要素と呼びます。SVG画像ファイルはXMLで書かれたテキストで成り立っていますし、RSSとAtomのウェブフィードのフォーマットもXMLですし、Microsoft Word文書は.docxファイル拡張子のXMLファイルを含むZIPファイルです。
ここではXML形式のテキストファイルを.xmlというファイル拡張子で保存します。以下はXML形式のデータ構造の例です。
<person>
<name>Alice Doe</name>
<age>30</age>
<programmer>true</programmer>
<car xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
<address>
<street>100 Larkin St.</street>
<city>San Francisco</city>
<zip>94102</zip>
</address>
<phone>
<phoneEntry>
<type>mobile</type>
<number>415-555-7890</number>
</phoneEntry>
<phoneEntry>
<type>work</type>
<number>415-555-1234</number>
</phoneEntry>
</phone>
</person>
この例では、<person>要素には<name>、<age>その他の要素が含まれています。<name>や<age>の要素は子要素で、<person>はその親要素です。正しいXML文書にはルート要素が一つだけあり、他の要素はすべてそのルート要素に含まれます。この例では<person>要素がルート要素です。以下のように複数のルート要素がある文書は正しいXMLではありません。
<person><name>Alice Doe</name></person>
<person><name>Bob Smith</name></person>
<person><name>Carol Watanabe</name></person>
XMLはJSONのようなより新しい直列化フォーマットと比較するとかなり冗長です。各要素には、<age>と</age>のように、開始タグと終了タグがあります。XML要素はキーと値のペアです。キーは要素のタグで(この例なら<age>)値は開始タグと終了タグの間のテキストです。XMLのテキストにはデータ型がありません。開始タグと終了タグの間にあるものは、この例での94102やtrueも含めて、すべて文字列だと解釈されます。<phone>要素のようなデータのリストの個々の要素にも<phoneEntry>のような名前をつけなければなりません。慣習的に“Entry”という接尾辞をつけることが多いです。
XMLのコメントはHTMLのコメントと同じで、<!--と-->の間にあるものはコメントとして無視されます。
開始タグと終了タグの外側のスペースは意味を持たないので、好きなようにスペースを入れられます。XMLには“null”値はありませんが、タグにxsi:nil="true"とxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"という属性を入れると“null”値に近くなります。XMLの属性は、開始タグ内にkey="value"という形式で書かれたキーと値のペアです。タグは終了タグを用いずに/>で終わる自己終了タグで書きます。例えば、<car xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>のように書きます。
タグと属性の名前は大文字でも小文字でも構いませんが、慣習的には小文字を使います。属性値はシングルクォートでもダブルクォートでも囲めますが、ダブルクォートで囲むほうが一般的です。
子要素にするか属性にするかはしばしば曖昧になります。先ほどの例では住所データを以下の要素で表していました。
<address>
<street>100 Larkin St.</street>
<city>San Francisco</city>
<zip>94102</zip>
</address>
しかし、子要素を<address>要素の自己終了タグの属性で表すこともできます。
<address street="100 Larkin St." city="San Francisco" zip="94102" />
このような曖昧さがあり、タグは冗長なことも相まって、XMLはかつてほど使われなくなってきました。XMLは1990年代から2000年代にかけて広く用いられ、現役で使用されているソフトウェアもたくさんあります。しかし、今では、XMLを使う特別な理由がなければ、JSONを使ったほうがよいでしょう。
大まかに言うと、XMLソフトウェアライブラリには、XML文書を読み取る2つの方法があります。Document Object Model(DOM)アプローチでは、一気にXML文書全体をメモリに読み取ります。このアプローチではXML文書内のどこにあるデータでもすぐにアクセスできますが、一般にサイズが中程度くらいのXML文書でないとうまく動きません。Simple API for XML(SAX)アプローチでは、XML文書を要素のストリームとして読み取るので、一気に文書全体をメモリに読み取る必要はありません。このアプローチはギガバイト単位のXML文書に適していますが、文書中で反復処理しなければならないので、あまり便利ではありません。
Pythonの標準ライブラリには、XMLテキスト処理用のxml.domモジュール、xml.saxモジュール、xml.etree.ElementTreeモジュールがあります。ここでのシンプルな例には、一気にXML文書全体を読み取るPythonのxml.etree.ElementTreeモジュールを使用します。
xml.etree.ElementTreeモジュールはElementオブジェクトでXML要素と子要素を表します。以下の式を対話型シェルに入力してみてください。
❶ >>> import xml.etree.ElementTree as ET
❷ >>> xml_string = """<person><name>Alice Doe</name><age>30</age>
<programmer>true</programmer><car xsi:nil="true" xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance”/><address><street>
100 Larkin St.</street><city>San Francisco</city><zip>94102</zip>
</address><phone><phoneEntry><type>mobile</type><number>415-555-
7890</number></phoneEntry><phoneEntry><type>work</type><number>
415-555-1234</number></phoneEntry></phone></person>"""
❸ >>> root = ET.fromstring(xml_string)
>>> root
<Element 'person' at 0x000001942999BBA0>
xml.etree.ElementTreeモジュールをas ET構文でインポートしています(❶)。これにより、長いxml.etree.ElementTreeというモジュール名の代わりにETと書けるようになります。変数xml_stringには解析対象のXMLのテキストが入っています(❷)。XMLのテキストは.xml拡張子のテキストファイルから読み取ることも簡単にできます。最後に、そのテキストをET.fromstring()関数に渡すと(❸)、アクセスしたいデータを含むElementオブジェクトが返されます。このElementオブジェクトをrootという名前の変数に格納します。
xml.etree.ElementTreeモジュールにはparse()という関数もあります。XMLをロードするファイル名を渡せば、Elementオブジェクトが返されます。
>>> import xml.etree.ElementTree as ET
>>> tree = ET.parse('my_data.xml')
>>> root = tree.getroot()
Elementオブジェクトを取得したら、tag属性でタグ名を、text属性で開始タグと終了タグで囲まれたテキストを確認できます。list()関数にElementオブジェクトを渡すと、その直接の子要素のリストが返されます。対話型シェルで続けて以下のように入力してください。
>>> root.tag
'person'
>>> list(root)
[<Element 'name' at 0x00000150BA4ADDF0>, <Element 'age' at
0x00000150BA4ADF30>, <Element 'programmer' at 0x00000150BA4ADEE0>,
<Element 'car' at 0x00000150BA4ADD00>, <Element 'address' at
0x00000150BA4ADCB0>, <Element 'phone' at 0x00000150BA4ADA30>]
親要素のElementオブジェクトについての子要素のElementオブジェクトには、Pythonのリストと同じように整数のインデックスでアクセスできます。rootに<person>要素が入っているとしたら、root[0]は<name>要素でroot[1]は<age>要素です。これらのElementオブジェクトではすべてtag属性とtext属性にアクセスできます。しかし、<car/>のような自己終了タグについては、text属性がNoneになります。対話型シェルで次のように入力してみてください。
>>> root[0].tag
'name'
>>> root[0].text
'Alice Doe'
>>> root[3].tag
'car'
>>> root[3].text == None # <car/>にはtextがない
True
>>> root[4].tag
'address'
>>> root[4][0].tag
'street'
>>> root[4][0].text
'100 Larkin St.'
ルート要素からXML文書全体のデータを探ることができます。Elementオブジェクトをforループで回すと直接の子要素を反復処理できます。
>>> for elem in root:
... print(elem.tag, '--', elem.text)
...
name -- Alice Doe
age -- 30
programmer -- true
car -- None
address -- None
phone -- None
Elementの配下の子要素をすべて反復処理したければ、forループでiter()メソッドを呼び出します。
>>> for elem in root.iter():
... print(elem.tag, '--', elem.text)
...
person -- None
name -- Alice Doe
age -- 30
programmer -- true
car -- None
address -- None
street -- 100 Larkin St.
city -- San Francisco
zip -- 94102
phone -- None
phoneEntry -- None
type -- mobile
number -- 415-555-7890
phoneEntry -- None
type -- work
number -- 415-555-1234
iter()メソッドに文字列を渡してXML要素でマッチするタグを抽出することもできます。この例では、iter('number')を呼び出し、ルート要素の<number>子要素だけを反復処理します。
>>> for elem in root.iter('number'):
... print(elem.tag, '--', elem.text)
...
number -- 415-555-7890
number -- 415-555-1234
この節で紹介した属性やメソッド以外にも、XML文書でデータを確認する方法はいろいろあります。例えば、第13章で紹介したウェブページのHTMLで要素を見つけるCSSセレクタのように、XPathと呼ばれる言語でXML文書中の要素を特定できます。これは本章の範囲外ですが、https://
PythonのXMLモジュールではXMLテキストをPythonのデータ構造に変換できません。しかし、サードパーティのxmltodictモジュール(https://
>>> import xmltodict
>>> xml_string = """<person><name>Alice Doe</name><age>30</age>
<programmer>true</programmer><car xsi:nil="true" xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance”/><address><street>
100 Larkin St.</street><city>San Francisco</city><zip>94102
</zip></address><phone><phoneEntry><type>mobile</type><number>
415-555-7890</number></phoneEntry><phoneEntry><type>work</type>
<number>415-555-1234</number></phoneEntry></phone></person>"""
>>> python_data = xmltodict.parse(xml_string)
>>> python_data
{'person': {'name': 'Alice Doe', 'age': '30', 'programmer': 'true',
'car': {'@xsi:nil': 'true', '@xmlns:xsi': 'http://www.w3.org/2001/
XMLSchema-instance'}, 'address': {'street': '100 Larkin St.', 'city':
'San Francisco', 'zip': '94102'}, 'phone': {'phoneEntry': [{'type':
'mobile', 'number': '415-555-7890'}, {'type': 'work', 'number':
'415-555-1234'}]}}}
JSONと比べてXMLが脇に押しやられた一つの理由は、XMLでデータ型を表現するのが難しいことです。例えば、<programmer>要素はブール値のTrueではなく文字列値の'true'と解釈されます。<car>要素は、None値ではなく、'car': {'@xsi:nil': 'true', '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance'}という見苦しいキーと値のペアに解釈されます。XMLモジュールを使う際は、意図したとおりにデータを表現できているか、入出力をダブルチェックしなければなりません。
xml.etree.ElementTreeモジュールは少し扱いづらいですから、小さなプロジェクトではopen()関数とwrite()メソッドでXMLテキストを自分で書き出すほうがよいでしょう。xml.etree.ElementTreeモジュールでXML文書を作成するなら、ルートElementオブジェクト(先ほどの例で言うなら<person>要素)を作成してから、SubElement()関数を呼び出して子要素を作成します。set()メソッドで要素に任意のXML属性を設定できます。例えば、次のようにします。
>>> import xml.etree.ElementTree as ET
>>> person = ET.Element('person') # ルートXML要素の作成
>>> name = ET.SubElement(person, 'name') # <name>を作成して<person>の下に入れる
>>> name.text = 'Alice Doe' # <name>と</name>の間にテキストを入れる
>>> age = ET.SubElement(person, 'age')
>>> age.text = '30' # XMLの内容はすべて文字列
>>> programmer = ET.SubElement(person, 'programmer')
>>> programmer.text = 'true'
>>> car = ET.SubElement(person, 'car')
>>> car.set('xsi:nil', 'true')
>>> car.set('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')
>>> address = ET.SubElement(person, 'address')
>>> street = ET.SubElement(address, 'street')
>>> street.text = '100 Larkin St.'
長くなるので<address>要素と<phone>要素の残りの部分は省略しました。ルートElementオブジェクトについてET.tostring()関数とdecode()関数を呼び出せば、XMLテキストのPythonの文字列を取得できます。
>>> ET.tostring(person, encoding='utf-8').decode('utf-8')
'<person><name>Alice Doe</name><age>30</age><programmer>true</programmer>
<car xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
<address><street>100 Larkin St.</street></address></person>'
あいにく、tostring()関数は、文字列ではなくbytesオブジェクトを返すので、decode()メソッドを呼び出して実際の文字列を取得しなければなりません。しかし、一度XMLテキストをPythonの文字列値として取得すれば、.xmlファイルに書き込んだり、関数に渡したり、ウェブリクエストに用いたり、その他文字列でできることなら何でもできます。
CSV、JSON、XMLはデータを保存するための一般的なプレーンテキストフォーマットです。人間にとって読みやすいと同時に、プログラムにとっても解析しやすいです。そのため、単純なスプレッドシートやウェブアプリのデータを扱うのによく使われます。Pythonの標準ライブラリにあるcsvモジュール、jsonモジュール、xml.etree.ElementTreeモジュールにより、これらのファイルの読み書きは非常に単純化されています。open()関数でファイルを開いて自分で解析する必要はありません。
これらのフォーマットはPythonに固有のものではなく、他の多くのプログラミング言語やアプリケーションでも用いられます。これらのフォーマットを使用するアプリケーションとやり取りするPythonプログラムを書くのに、本章で学んだことが役立つことでしょう。
1. ExcelにはあってCSVにはない機能は何ですか?
2. readerオブジェクトやwriterオブジェクトを作成するためにcsv.reader()やcsv.writer()に渡すものは何ですか?
3. readerオブジェクトやwriterオブジェクトを使用するためにはFileオブジェクトをどのモードで開く必要がありますか?
4. リストを引数に取り、CSVファイルに書き込むメソッドは何ですか?
5. キーワード引数のdelimiterとlineterminatorはそれぞれどのような働きをしますか?
6. CSV、JSON、XMLのうち、テキストエディタで編集しやすいのはどれですか?
7. JSONデータの文字列を取りPythonのデータ構造を返す関数は何ですか?
8. Pythonのデータ構造を取りJSONデータの文字列を返す関数は何ですか?
9. 山かっこで囲まれたタグがあるHTMLと似ているデータ直列化フォーマットは何ですか?
10. JSONでNone値はどのように書きますか?
11. JSONでブール値はどのように書きますか?
Excelで数クリックすればスプレッドシートをCSVファイルとして保存できます。しかし、数百ものExcelファイルをCSVファイルに変換しなければならないとしたら、クリックするのに何時間もかかってしまいます。第14章で紹介したopenpyxlモジュールを使用して、現在の作業ディレクトリのExcelファイルをすべて読み取ってCSVファイルとして出力するプログラムを書いてください。
一つのExcelファイルに複数のシートが存在する可能性がありますから、CSVファイルはシートごとに作成しなければなりません。CSVファイルの名前は<excel filename>_<sheet title>.csvとしてください。<excel filename>は拡張子なしのExcelファイルの名前で(例えば、spam_data.xlsxではなくspam_data)、<sheet title>はWorksheetオブジェクトのtitle属性に入っている文字列です。
このプログラムではforループが多重の入れ子になります。プログラムの骨組みは以下のようになります。
for excel_file in os.listdir('.'):
# 非xlsxファイルは飛ばし、workbookオブジェクトをロード
for sheet_name in wb.sheetnames:
# ワークブックのすべてのシートを反復処理
# Excelのファイル名とシート名からCSVファイル名を作成
# CSVファイル用のcsv.writerオブジェクトの作成
# シートのすべての行を反復処理
for row_num in range(1, sheet.max_row + 1):
row_data = [] # 各セルとこのリストに追加
# 行内の各セルを反復処理
for col_num in range(1, sheet.max_column + 1):
# row_dataに各セルのデータを追加
# CSVファイルにrow_dataリストを書き込み
csv_file.close()
本書のオンライン素材からZIPファイルのexcelSpreadsheets.zipをダウンロードして、スプレッドシートをプログラムと同じディレクトリに展開してください。このファイルを使ってプログラムをテストできます。