今回は、ARIM事業で実際に開発を行っている材料研究データの構造化コードの中から一部をご紹介します。
はじめに、本講義で使用するファイルを皆さんの環境にダウンロードするため、次のコードを実行してください。
!wget https://github.com/tendo-sms/python_beginner_2023/raw/main/files_6/meta.txt
!wget https://github.com/tendo-sms/python_beginner_2023/raw/main/files_6/data1.csv
!wget https://github.com/tendo-sms/python_beginner_2023/raw/main/files_6/data2.csv
!wget https://github.com/tendo-sms/python_beginner_2023/raw/main/files_6/data3.CSV
!wget https://github.com/tendo-sms/python_beginner_2023/raw/main/files_6/sample.json
ARIM事業の中で活用しているRDE(Research Data Express)は、材料研究のために研究現場で日々創出される材料データを効率的に収集するために、IoT データ転送機能、データ構造化機能、データ登録機能を備えたデータ収集・管理・提供システムのことです。
データ構造化機能は、Pythonで開発が進められています。
きっと、大きなシステムで動くプログラムはコーディングが複雑で難しいのでは?という印象を持たれたかもしれません。
実は、基本的な構成は今回のセミナーを通して聴講いただいた方であれば、十分に理解できる内容となっています。
それでは実際にコードで利用しているテクニックなどを一部ですがご紹介します。
気を付けないといけないポイントを順番に説明します。
自分のためだけに使うプログラムを作成する場合、ユーザは自分ひとりなので意識することが少ないのですが、
できるだけ汎用的に作っておくというのがコーディングのコツです。
特に複数のユーザが利用するのであれば、その柔軟性の重要度は増します。
まずは、自分だけが使う場合にファイルを読み込むのであれば次のようなシンプルなコードでもよいでしょう。
# 読み込むファイル名を指定 変更したい場合は書き直す
input_file = "meta.txt"
# ファイルを読み込む
with open(input_file, "r") as f:
data = f.read()
# データの表示
print(data)
しかし、RDEに登録したいユーザは必ずしも決められた名前のファイルを一つだけアップロードするとは限りません。
そのため、1つ以上のファイルも読み込めるようにしておく必要があります。
次のようにして、そのディレクトリにある全てのファイルをループで処理してみます。
from pathlib import Path
# 現在のディレクトリを取得
cwd = Path.cwd()
# globを使ってパターンをワイルドカードの*にして全てを対象にする
for f in cwd.glob("*"):
# ファイル名を表示
print(f.name)
# エラーが出る
with open(f, "r") as f:
print(f.read())
エラーが出てしまいました。
これは、globを使ってそのディレクトリにある全てを対象にしたため、ディレクトリも処理対象に含まれてしまったため発生しました。
open文はファイルに対して行うものであり、ディレクトリを指定すると(この場合は/content/.config)エラーになってしまいます。
では、次のように修正してcsvファイルだけを対象にしてみましょう。
from pathlib import Path
# 現在のディレクトリを取得
cwd = Path.cwd()
for f in cwd.glob("*.csv"):
# ファイル名を表示
print(f.name)
with open(f, "r") as f:
print(f.read())
今度はちゃんと読み込めました!
ですが、人によっては以下のように表示されていないでしょうか?
data2.csv
C,D
3,4
5,6
data1.csv
A,B
1,2
3,4
これはdata2.csvを先に読み込んで、後からdata1.csvが読み込まれています。
globというのは、パターンに該当したものを見つけて列挙してくれる便利な命令ですが、その順番は決まっていません。
読み込む順番を気にしないのであればこのままでもよいですが、読み込みの順番が決まっていないのは不便なことがあります。
print文で表示させている例でも、表示順序が違うと戸惑ってしまうかもしれません。
では順番も意識して修正してみましょう。
from pathlib import Path
# 現在のディレクトリを取得
cwd = Path.cwd()
# srotedを追加してソートする
for f in sorted(cwd.glob("*.csv")):
# ファイル名を表示
print(f.name)
with open(f, "r") as f:
print(f.read())
今度はdata1.csvを先に読み込めました!
これで、汎用性を確保でき、ユーザの戸惑いもなくなるだろう…と思った方もいらっしゃるかもしれませんが、まだやれることがあります。
このディレクトリにどのようなファイルがあるか確認してみましょう。
# ディレクトリの一覧を確認
!ls
処理対象になっていなかったdata3.CSVというものがありますね…。
同じCSVファイルのはずなのに、なぜ読み込まれなかったのでしょうか。
よく見るとファイルの拡張子は小文字(.csv)と大文字(.CSV)の違いがあるようです。
画像でも.pngや.PNGとする場合があるように、皆さんにもこのようなファイルを見た経験がないでしょうか?
では、これも考慮して修正してみましょう。
from pathlib import Path
# 現在のディレクトリを取得
cwd = Path.cwd()
# .csvでも.CSVでも.Csvでも・・・処理対象にする
for f in sorted(cwd.glob("*.[Cc][Ss][Vv]")):
# ファイル名を表示
print(f.name)
with open(f, "r") as f:
print(f.read())
やっとCSVファイルを3つとも読み込むことに成功しました!
globの引数はパターンを記述しますが、実は正規表現ではなく標準Unixパス拡張ルールに準拠というのが採用されています。
興味のある方は確認してみてください。
さて、これで終わり…と思ったかもしれませんが、まだ想定される懸念点があります。
ファイルのエンコーディングです。
アップロードするエンコーディングを固定して設計(取り決め)してもよいですし、try文で頑張る方法もあります。
ARIMでは、chardetパッケージを使って自動判定させています。
chardetは標準パッケージではないので、pipでインストールしてください。
!pip install chardet
import chardet
from pathlib import Path
# 現在のディレクトリを取得
cwd = Path.cwd()
# .csvでも.CSVでも.Csvでも・・・処理対象にする
for f in sorted(cwd.glob("*.[Cc][Ss][Vv]")):
# エンコーディングを取得
enc = chardet.detect(open(f, "rb").read())["encoding"]
# ファイル名とエンコーディングを表示
print(f"{f.name}のファイルのエンコーディング: {enc}")
with open(f, "r", encoding=enc) as f:
print(f.read())
エンコーディングによる問題は、日本語が含まれる場合によく発生します。
WindowsユーザがExcelで作成したcsvファイルは通常、Shift-JISと呼ばれる形式になります。
なおここではShift-JISと呼んでいますが、実はこれは正確ではなく、Microsoftが勝手にWindows独自のcp932というものを作っています。
Shift-JISをベースに、いくつか文字を追加したものなのでShift-JISとしてもほとんどは問題がありません。
ですが、問題になるときもあるのでShift-JISと決めつけない方がよいときもありますのでご注意ください。
RDEに登録される材料測定装置のデータファイルには、日付情報が含まれていることがあります。
しかし、ご存じの通り日付の表現方法は様々なものがあります。
もし表示形式がわかっていれば、datetimeモジュールを使用することで文字列から日付のオブジェクトに変換することが簡単にできます。
from datetime import datetime
# 年.月.日の場合
date1 = "2021.05.29"
print(f"{date1} -> {datetime.strptime(date1, '%Y.%m.%d')}")
# 年/月/日の場合
date2 = "2021/05/29"
print(f"{date2} -> {datetime.strptime(date2, '%Y/%m/%d')}")
文字列から日付のオブジェクトに変換することができました。
ここで、%Yや%mというのはディレクティブと呼ばれ、それぞれに応じた意味があります。
代表的な例をいくつかご紹介します。
ディレクティブ | 意味 |
---|---|
%Y | 西暦 ( 4桁) の 10 進表記を表します。 |
%m | 0埋めした10進数で表記した月。 |
%d | 0埋めした10進数で表記した月中の日にち。 |
%H | 0埋めした10進数で表記した時 (24時間表記)。 |
%M | 0埋めした10進数で表記した分。 |
%S | 0埋めした10進数で表記した秒。 |
さて、datetime.strptimeで指定する場合は、その文字列の形式がすでにわかっている場合にのみ有効な方法です。
ではもし、形式がわからない場合はどうすればよいでしょうか。
ここで活躍するのが、dateutilパッケージです。
実際に上記の例で使ってみましょう。
from dateutil import parser
# 年.月.日の場合
date1 = "2021.05.29"
print(f"{date1} -> {parser.parse(date1)}")
# 年/月/日の場合
date2 = "2021/05/29"
print(f"{date2} -> {parser.parse(date2)}")
うまく変換できましたね!
それでは、どんなものでも変換可能なのでしょうか。
次のように色んなパターンを確かめてみましょう。
from dateutil import parser
date_lst = [
"2021.05.29",
"2021-05-29T14:17:27Z",
"2021-05-29 15:17:27.133860",
"2021-05-29 16:17:27.133860+00:00",
"2021-05-29 17:17:27.133860+05:00",
"2021/05/29",
"2021/05/29T14:17:27Z",
"2021/05/29 15:17:27.133860",
"2021/05/29 16:17:27.133860+00:00",
"2021/05/29 17:17:27.133860+05:00",
"May 29 2022 12:17PM",
"May 29 2022 at 9:17PM",
"May 29, 2022, 19:17:27",
"Tue, 05-29-2022, 1:17AM",
"Tue, 29 May, 2022",
"Tuesday, 29th May, 2022 at 12:17p",
"2022/02/ 7 14:04:45"
]
for d in date_lst:
print(f"{d} -> {parser.parse(d)}")
ほとんどの場合、うまくいったようですが「"2022/02/ 7 14:04:45"」はエラーが出てしまったようです。
何も問題なく読み取れる気がしますが、実はこの文字列は日の前に空白スペースがあります。
それが原因で日付として認識できなかったようです。
このような場合は次のように細工してから実行してみましょう。
from dateutil import parser
date = "2022/02/ 7 14:04:45"
# 文字列を修正 2022/02/ 7 14:04:45 -> 2022/02/07 14:04:45
new_date = date.replace("/ ", "/0")
print(f"{date} -> {new_date} -> {parser.parse(new_date)}")
今度はうまく変換できたようです。
このように、ARIMにおける構造化コードでは基本的にはdateutilに頼っていますがイレギュラーなものだけは別途加工してから利用しています。
辞書の取り扱いの中で、JSONファイルについて少し触れていたかと思います。
JSONはその扱いやすさから設定ファイルやWeb上でのデータのやり取りなどに利用されています。
RDEでも、JSONは利用されていてその構文は「キー:値」というPythonの辞書のような形式です。
サンプルのファイル「sample.json」の中身は以下のように記述されています。
{
"A":1,
"B":2
}
これらのJSONファイルを読み込むには、カッコ{}を自分で解釈しながら変数に入れる必要はありません。
Pythonが標準で提供しているjsonモジュールを使うと簡単に読むことができます。
import json
# 読み込むファイル名を指定
input_file = "sample.json"
# ファイルを読み込む
with open(input_file, "r") as f:
data = json.load(f)
# データの表示
print(data)
print(type(data))
json.loadという関数を呼び出すだけで、Pythonの辞書型として解釈されて読み込むことができました。
では、このdataに何か追加して保存してみましょう。
# 辞書にデータを追加
data["あいうえお"] = "かきくけこ"
# JSONで出力
with open("new_sample.json", "w") as f:
json.dump(data, f)
ではどのような出力データになったのか、左のメニューからnew_sample.jsonを確認してみましょう。
{"A": 1, "B": 2, "\u3042\u3044\u3046\u3048\u304a": "\u304b\u304d\u304f\u3051\u3053"}
JSONではありますが、何かおかしな点があると思います。
横長であり、日本語がよくわからない形式になっています。
横長は人が見づらいだけで、JSONとしては間違ってはいません。
日本語は\u〇〇〇というように変換されてしまっています。
jsonモジュールでは、不十分なのでしょうか。
いえ、これらを解消するためには2つオプションを追加すれば大丈夫です。
# 辞書にデータを追加
data["あいうえお"] = "かきくけこ"
# JSONで出力 オプションindentとensure_asciiを追加
with open("new_sample.json", "w") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
今度は、いかがでしょうか?
次のように書き込まれていることが確認できたと思います。
{
"A": 1,
"B": 2,
"あいうえお": "かきくけこ"
}
このように、各種ライブラリには様々なオプションがあり、何も指定しない場合は予期せぬ動作になったりします。
ですが、ライブラリが提供しているオプションによって解決する場合が多いです。
まずは、ライブラリの使い方を調べることをお勧めいたします。
テキストデータから、特定の行を見つけて処理をしたいことがあります。
RDEではメタデータを抽出する際に必要です。
ここでは、どうやって特定の行を探してきて処理するのか確かめてみましょう。
まずは、先ほども使用したmeta.txtを表示させてみます。
# シェルコマンドでテキストの中身を表示
!cat meta.txt
このファイルには、「項目名:値」という形式でデータが保存されています。
では、"説明"という項目を抽出してみたいと思います。
まずは、その文字列が含まれているかどうかをinを使って判定してみようと思います。
# 読み込むファイル名を指定 変更したい場合は書き直す
input_file = "meta.txt"
# ファイルを読み込む
with open(input_file, "r") as f:
data = f.readlines()
# ループで行ごとに調査する
for d in data:
# もし説明という文字が入っていたら表示する
if "説明" in d:
print(d.rstrip())
メモの項目も抽出されてしまったようです。
原因は、値の方に説明という文字が入っているからです。
「ガイダンスで説明するもの」
inは、その文字列(この場合は行全体)で含まれているかをチェックしています。
当然、値の方であっても該当文字列があればヒットしてしまいます。
では、項目はかならず左側にあると考えることができそうなので、startswithを使ってみます。
# 読み込むファイル名を指定 変更したい場合は書き直す
input_file = "meta.txt"
# ファイルを読み込む
with open(input_file, "r") as f:
data = f.readlines()
# ループで行ごとに調査する
for d in data:
# もし説明という文字から開始していたら表示する
if d.startswith("説明"):
print(d.rstrip())
今度はうまく抽出できたようです!
このように、ある程度そのデータの特徴をつかんでから適切な処理を考えないとうまくいかないケースが出てきます。
inを使えば文字列を抽出できると安易に考えがちですが、十分でないことも多いです。
ARIMでも、どんな内容のデータが送られてくるかわからない、ということを念頭に置いて開発を進めています。
とはいえ、実際に遭遇してからでないと気が付かないことも多いです。
あとから「そういうデータが含まれることもあるのか~・・・」と、修正しながら日々精進しています。
いかがでしたでしょうか?
RDEのように、複数のユーザの利用が想定されるプログラムでは、色々なケースに対応できる工夫が必要になってきます。
しかし、その内容を紐解けば大部分は皆様が今回習得した基本的な記述で構築されていることがわかっていただけたのではないでしょうか。
もちろん数値計算などの難しいこともありますが、そこはPythonのライブラリの豊富さに頼ってしまえば、難しいアルゴリズムを作ることなく数行で目的を実現することも可能です。
今回の講義は、ここまでとなります。このあと、最後の講義としてクラスについてご紹介します。以下のURLから、資料をご参照ください。
https://colab.research.google.com/drive/1sWuHBJ91hvNdufc73_J3yTebRbeXx3Lr?usp=sharing