#pyconjp 発表「知ろう!使おう!HDF5ファイル!」の落ち穂拾い

PyCon JP 2019にて発表をした際、いくつか質問をいただきました。

前回の記事にもあるように、当時きちんと回答できたか不安だったため、今回落ち穂拾いとしてまとめてみます。

なお、誤りや過不足などがありましたら、ご指摘いただけるとありがたいです。

   
目次

 

そもそも、何のデータを入れるためにHDFができたのか

The HDF Groupの History of HDF Group ページに記載がありましたので、引用します。
History of HDF Group

In 1987, the Graphics Foundations Task Force (GFTF) at the National Center for Supercomputing Applications (NCSA) at the University of Illinois at Urbana-Champaign, set out to create an architecture-independent software library and file format to address the need to move scientific data among the many different computing platforms in use at NCSA at that time. Additional goals for the format and library included the ability to store and access large objects efficiently, the ability to store many objects of different types together in one container, the ability to grow the format to accommodate new types of objects and object metadata, and the ability to access the stored data with both C and Fortran programs.

 

クロスプラットフォーム間で、Excelファイルの罫線や内容が壊れないか

発表時点でも検証していましたが、もう少し詳しく検証してみました。

用意したExcelファイルは、以下の(a)〜(d)の4種類です。

 
これらのファイルをHDF5ファイルにDatasetとして保存して、Mac/Windowsの双方で読み込んでみます。

 

自作ファイルの作成について

自作ファイルは

  • 罫線
  • 画像
  • 罫線
  • シートのロック

を持つExcelファイルを、 Mac + openpyxlで作成するものとします。

excel_creator.py として用意します。

import datetime
import pathlib

import openpyxl
from openpyxl.comments import Comment
from openpyxl.styles.borders import Border, Side, BORDER_THIN
from openpyxl.styles.colors import BLUE

BASE_DIR = pathlib.Path(__file__).resolve().parents[0]
INPUT_DIR = BASE_DIR.joinpath('input')
OUTPUT_DIR = BASE_DIR.joinpath('output', 'h5py')


wb = openpyxl.Workbook()

# grab the active worksheet
ws = wb.active

# セルにデータを入れる
ws['A1'] = 'Hello, world!'

# 日付データを入れる
ws['A2'] = datetime.datetime.now()

# 罫線を引く
side = Side(style=BORDER_THIN, color=BLUE)
border = Border(top=side, bottom=side, left=side, right=side)
a4 = ws['B3'].border = border

# コメントを入れる
ws['B4'].comment = Comment('ham', 'myauthor')

# 画像ファイル差し込み
img_path = INPUT_DIR.joinpath('shinanogold.png')
img = openpyxl.drawing.image.Image(img_path)
ws.add_image(img, 'C2')

# シートの保護
ws.protection.enable()

# 保存
output = OUTPUT_DIR.joinpath('original.xlsx')
wb.save(output)

 

公開ファイルの利用について

公開ファイルは xls 形式でした。

そのため、Mac上で手動で xlsx へと変換・保存したものを使用します。

 

HDF5ファイルとして保存するプログラム

h5pyを使い、 gleaning.py として作成します。

なお、いずれのExcelファイルも gzip で圧縮するものとします (create_dataset()の引数 compression を利用)。

import hashlib
import pathlib

import h5py

import numpy as np

BASE_DIR = pathlib.Path(__file__).resolve().parents[0]
INPUT_DIR = BASE_DIR.joinpath('input')
OUTPUT_DIR = BASE_DIR.joinpath('output', 'h5py')
TYPE_OF_BINARY = h5py.special_dtype(vlen=np.dtype('uint8'))

ORIGINAL_EXCEL_PATH = OUTPUT_DIR.joinpath('original.xlsx')
GLEANING_HDF5_PATH = OUTPUT_DIR.joinpath('gleaning.h5')

STATISTICS_3MB = OUTPUT_DIR.joinpath('statistics_kenporonbun-toukei201604.xlsx')
STATISTICS_LINE = OUTPUT_DIR.joinpath('statistics_nenpou09.xlsx')
STATISTICS_IMAGE = OUTPUT_DIR.joinpath('statistics_2017_1_02.xlsx')


def calculate_md5(path):
    return hashlib.md5(path.read_bytes()).hexdigest()


def compare_excel(from_path):
    print(f'{from_path.name} --------->')

    # 元ExcelのMD5を見る
    from_md5 = calculate_md5(from_path)
    print(f'from MD5: {from_md5}')

    dataset_name = f'excel_{from_path.stem}'

    # gzip圧縮して、ExcelをHDF5ファイルに入れる
    with h5py.File(GLEANING_HDF5_PATH, mode='a') as file:
        with from_path.open(mode='rb') as excel:
            excel_binary = excel.read()

        excel_data = np.frombuffer(excel_binary, dtype='uint8')
        ds_excel = file.create_dataset(
            dataset_name, excel_data.shape, dtype=TYPE_OF_BINARY, compression='gzip')
        ds_excel[0] = excel_data

    # 取り出して、MD5を見る
    with h5py.File(GLEANING_HDF5_PATH, mode='r') as file:
        dataset = file[dataset_name]
        export_path = from_path.parents[0].joinpath(f'{from_path.stem}_after{from_path.suffix}')
        with export_path.open('wb') as w:
            w.write(dataset[0])

    to_md5 = calculate_md5(export_path)
    print(f'to MD5  : {to_md5}')

    assert from_md5 == to_md5


if __name__ == '__main__':
    # きれいなHDF5ファイルを使うため、事前に削除しておく
    if GLEANING_HDF5_PATH.exists():
        GLEANING_HDF5_PATH.unlink()

    compare_excel(ORIGINAL_EXCEL_PATH)
    compare_excel(STATISTICS_3MB)
    compare_excel(STATISTICS_LINE)
    compare_excel(STATISTICS_IMAGE)

 

HDF5からExcelの入ったDatasetを読み込み、ローカルに保存するプログラム

こちらも h5py を使って excel_reader.py として作成します。

なお、Mac/Windows間で保存したファイルに差異がないか検証するため、MD5ハッシュをprintします。

import hashlib
import pathlib

import h5py

BASE_DIR = pathlib.Path(__file__).resolve().parents[0]
OUTPUT_DIR = BASE_DIR.joinpath('output', 'h5py')
ORIGINAL_EXCEL_PATH = OUTPUT_DIR.joinpath('original.xlsx')
GLEANING_HDF5_PATH = OUTPUT_DIR.joinpath('gleaning.h5')

STATISTICS_3MB = OUTPUT_DIR.joinpath('statistics_kenporonbun-toukei201604.xlsx')
STATISTICS_LINE = OUTPUT_DIR.joinpath('statistics_nenpou09.xlsx')
STATISTICS_IMAGE = OUTPUT_DIR.joinpath('statistics_2017_1_02.xlsx')


def calculate_md5(path):
    return hashlib.md5(path.read_bytes()).hexdigest()


def print_md5(path):

    dataset_name = f'excel_{path.stem}'
    with h5py.File(GLEANING_HDF5_PATH, mode='r') as file:
        dataset = file[dataset_name]
        export_path = path.parents[0].joinpath(f'{path.stem}_reader{path.suffix}')
        with export_path.open('wb') as w:
            w.write(dataset[0])

    to_md5 = calculate_md5(export_path)
    print(f'{path.name} MD5  : {to_md5}')


if __name__ == '__main__':
    print_md5(ORIGINAL_EXCEL_PATH)
    print_md5(STATISTICS_3MB)
    print_md5(STATISTICS_LINE)
    print_md5(STATISTICS_IMAGE)

 

MacでHDF5ファイルからExcelを取得した結果

MD5は以下の通りでした。

$ python excel_reader.py 
original.xlsx MD5  : af366474fc7f09cc5bcfdff764acf01d
statistics_kenporonbun-toukei201604.xlsx MD5  : bb82c493c80e362da47b96b9e8b28385
statistics_nenpou09.xlsx MD5  : 4de608b9452a34caa8a76f1fe8c4b265
statistics_2017_1_02.xlsx MD5  : 838d003d262f05f7e50585feab229f5d

 
また、取得した各ファイルを開いてみましたが、壊れた様子は特にありませんでした。

 

WindowsでHDF5ファイルからExcelを取得した結果

Windowsで実行した結果は以下のスクショの通りです。

f:id:thinkAmi:20190925222701p:plain:w400

 
テキストに落としてみた結果はこちら。MD5自体の差異はなさそうです。

>python excel_reader.py
original.xlsx MD5  : af366474fc7f09cc5bcfdff764acf01d
statistics_kenporonbun-toukei201604.xlsx MD5  : bb82c493c80e362da47b96b9e8b28385
statistics_nenpou09.xlsx MD5  : 4de608b9452a34caa8a76f1fe8c4b265
statistics_2017_1_02.xlsx MD5  : 838d003d262f05f7e50585feab229f5d

 
また、実際にWindowsExcelで開いてみましたが、「ファイルが壊れています」等の表示もなく、見た感じ大丈夫そうでした。

 

圧縮したDatasetを読み込む時は、展開しないとダメか

上記のExcelファイル検証で行いましたが、Datasetを作成する際、圧縮方法としてgzipを指定しました。

一方、読み込む時のソースコードでは、gzipからの展開は特に指定していません。

そのため、h5pyを使う限りは、展開する必要はなさそうです*1。  
 

zipファイルとの違い

今回の発表は、「NumPy方面でなくてもHDF5ファイルは使える」が主題でした。そのため、zipファイルとの機能の違いは確かに気になります。

調べたところでは、

  • HDF5ファイルには、パスワード設定機能がない

がありました。

ただ、プログラムから扱うことを考えると、以下のような機能を利用できるため、HDF5ファイルの方が扱いやすいのかなという印象です。

  • Datasetに紐づくAttributeに対して検索ができる
    • HDF5ファイルを作成する段階で、後から検索しやすいようなAttributeを色々と付けておく
  • (今回の主題とは外れますがが) NumPy方面と相性が良い
    • 上記の History of HDF Group からもうかがえる。
  • 圧縮して格納したDatasetを取り出す場合、展開する必要がない

 
もし、この点について詳しい方がいらっしゃいましたら、ご指摘いただけるとありがたいです。

 

圧縮アルゴリズムについて

What kind of compression methods does HDF5 support? より引用します。

HDF5 supports gzip (deflate), Szip, n-bit, scale-offset, and shuffling (with deflate) compression filters.

 
また、その他にも以下のページに記載がありました。

 

複数人で利用する時のロックはどうなるのか

HDF5 1.10.0以降、 SWMR (Single-Writer/Multiple-Reader) が導入されています。

詳細は以下のリンクとなります。

 
ちなみに、スレッドセーフについては、こちらで触れられています。

 

HDF5ファイルをSQLライクに扱いたい

HDF5ファイルでは、SQLを知らなくてもデータを階層的に保存することができます。

一方で、慣れたSQLライクにHDF5ファイルを保存したいことがあるかもしれません。

その場合は、HDFql というライブラリがあります。
The easy way to manage HDF5 data

 
そのページでは、こんな感じの例が挙げられています。

create file myFile.h5
use file myFile.h5
create dataset myGroup/myDataset as float enable zlib values(12.4)

 
以降は、発表ではふれなかった、スライドの内容です。

   

次のステップについて

HDF5ファイルについて詳しく知りたい場合は、以下の書籍が参考になります。
Python and HDF5 - O'Reilly Media | Andrew Collette著

 
また、h5pyやPyTablesの使い方については、以下の記事や発表を見るのが良いと思います。

あるいは、SciPy 2017のチュートリアルもあります。
tomkooij/scipy2017: SciPy 2017 tutorial: HDF5 take 2: h5py and PyTables

 

公式情報

The HDF Groupでは、いろいろな公式情報があります。

HDF Forumでは質疑応答が色々とありましたので、困ったら覗いてみるのもよいかもしれないです。

 

h5servを使うための準備

h5servを使うためには、少々手間がかかりましたので、メモ代わりに記載します。

自分の端末で使う場合、hostsファイルに設定

hostsファイルのIPアドレスに対する値は、 <ファイル名>.<適当なドメイン> という形で設定します。

例えば、h5servが動作している example.com にアクセスすると、 test.h5 ファイルを操作できるようにするには、以下のような設定となります。

127.0.0.1 test.example.com

 
hostsファイルの設定が終わったら、domain オプションでドメインを指定した上で、h5servを起動します。

$ python h5serv --port=5000 --toc_name=mytoc.h5 --domain=example.com

 

操作対象のファイルを作成

中身は空で問題ないですので、作成します。

作成先は、h5servのデータディレクトリである data の中になります。

また、ファイル名は test.h5 です。

p = Path(__file__).resolve().parents[0].joinpath('h5serv', 'data', 'test.h5')
with h5py.File(p, mode='w') as f:
    pass

 

この時点での data ディレクトリ構成
$ tree
.
├── public/
├── readme.txt
└── test.h5

 

動作確認

では、実際にHDF5ファイルを扱ってみます。今回は型定義を作成するところまでです。

# 型定義を作成
str_type = {'charSet': 'H5T_CSET_UTF8',
            'class':   'H5T_STRING',
            'strPad':  'H5T_STR_NULLTERM',
            'length':  'H5T_VARIABLE'}
payload = {'type': str_type, 'shape': 1}

# h5servにPOST
res1 = requests.post(
    URL_BASE + 'datasets',
    json=payload
).json()

 
結果を確認します。

新しく test.h5 ファイルが作成されました。

├── mytoc.h5
├── public/
├── readme.txt
└── test.h5

 
また、Datasetを見ると、型定義がされていることが分かります。

f:id:thinkAmi:20190925230419p:plain:w300

 
 

HDF5ファイルのバージョンについて、もう少し

HDF5 Library Release Version Numbers より、もう少し詳しく見てみます。

HDF5ファイルのバージョンが HDF5 x.y.z となっているとすると、x, y, zのそれぞれは以下を示します。

  • x: major version number
    • ライブラリやファイルフォーマットの大きな変更あり
  • y: minor version number
    • 新しい機能によるファイルフォーマットの変更
    • 偶数はstable、奇数はdevelop
  • z: release number
    • バグ修正等ライブラリの改善、フォーマットの変更なし

 

ソースコード

この記事で使ったソースコードについては、リポジトリに追加してあります。
https://github.com/thinkAmi/PyCon_JP_2019_talk/commit/df88400b033bdd64beecb8453b1f2542d7bc33a1

*1:他の言語、他のライブラリでは異なるかもしれませんが