連携先のシステムがgzip対応してなくてもやりくりする方法

# はじめに

システム間の結合後をおさえるために、処理に必要なデータをファイルでやりとりするケースがあります。
それに加えて、やりとりするデータが大量の場合、ディスク使用量やIOをおさえるために、そのファイルをgzipなどで圧縮したいこともあります。

しかし、連携先のシステムがgzip圧縮したファイルでのやりとりを対応していない場合も、ままあります。
そういった場合に、自分のシステムのところだけではgzip圧縮させつつ、連携ファイルは圧縮なしのものでやりとりするための小技を集めたのが、この記事です。

# 基本方針

* 連携先からファイルをもらう場合は、自分のサーバに、gzip圧縮したコピーを作成
* 連携先にファイルを渡す場合は、自分のサーバにgzip圧縮したものを作成して、それを解凍しながらコピー

これによって、自分のサーバ上は圧縮したファイルのみを扱い、連携先には圧縮していない状態でやりとりできます。

Pythonはgzipファイルの操作は[gzip.open](https://docs.python.org/ja/3.9/library/gzip.html#gzip.open) で、比較的簡単にできます。

# NFSで連携する場合

## ファイルをもらう場合

`/nfs/input/data.csv` が、NFSに配置されるとします。
`/input/data.csv.gz` として、圧縮コピーします。

```python

input_file = Path('nfs', 'input', 'data.csv')
compressed_file = Path('input', 'data.csv.gz')

with open(input_file, 'rb') as src, gzip.open(compressed_file, 'w') as dst:
     shutil.copyfileobj(src, dst)

```

単純なコピーなのでバイナリモードとshutil.copyfileobjを使います。

やってることは `cat /nfs/input/data.csv | gzip > /input/data.csv.gz` と同等です。

## ファイルを渡す場合

バッチで、連携するデータをgzip圧縮したファイルとして、自分のサーバ上の `/output/result.csv.gz` に作成するとします。
これを、NFSの `/nfs/output/result.csv` となるように、解凍コピーします。

入力のときのコードの役割を逆にするだけです。

```python

compressed_file = Path('output', 'result.csv.gz')
output_file = Path('nfs', 'output', 'result.csv')

with gzip.open(compressed_file, 'r') as src, open(output_file, 'wb') as dst:
     shutil.copyfileobj(src, dst)


```

やってることは `zcat /output/result.csv.gz > /nfs/output/result.csv` と同等です。

# SFTPで連携する場合

セキュリティの関係で、SFTPでやりとりするケースもあります。
Pythonの場合、ParamikoというSFTPクライアントライブラリがあります。
このライブラリでは、リモート上のファイルに対して、file-like object相当のものを返してくれるメソッドがあります。

https://docs.paramiko.org/en/stable/api/sftp.html#paramiko.sftp_client.SFTPClient.file

これを使えば、NFSのときとほとんど同じコードで、同じことができます。


## ファイルをもらう場合

```python

sftp_client = ...
remote_file = sftp_client.file('data.csv')
compressed_file = Path('input', 'data.csv.gz')

with open(remote_file, 'rb') as src, gzip.open(compressed_file, 'w') as dst:
     shutil.copyfileobj(src, dst)

```

## ファイルを渡す場合

```python

compressed_file = Path('output', 'result.csv.gz')

sftp_client = ...
output_file = sftp_client.file('result.csv')

with gzip.open(compressed_file, 'r') as src, open(output_file, 'wb') as dst:
     shutil.copyfileobj(src, dst)

```


# Appendix

## gzipファイルを解凍せずに、DBに取り込む

大量データの場合、RDBMSが提供するファイル取り込みの機能（MySQLだとLoad data構文）を使いたくなります。
このファイル取り込みの機能が、gzip圧縮されたものをサポートしていないケースがあります。

こういった場合、

1. ファイルを解凍
1. 解凍したファイルに対してファイル取り込みを実施

とやると、折角、圧縮してディスク使用量をおさえたのが、なかったことになります。

これを回避するために、

1. 別プロセスで、named pipeに解凍した内容を書き込み
1. named pipeに対してファイル取り込みを実施

とすれば、いったん、解凍したファイルをつくらずにすみます。
シェルだと、下記のようになります。

```
mkfifo /tmp/named_pipe
zcat data.csv.gz > /tmp/named_pipe &
mysql -e "LOAD DATA INFILE '/tmp/named_pipe' INTO TABLE sample_table"
```

Pythonでも、以下の２つと、これまでの内容を組み合わせることで同様のことができます。

* [os.mkfifo](https://docs.python.org/ja/3.9/library/os.html)
* [multiprocessing.Process](https://docs.python.org/ja/3.9/library/multiprocessing.html#the-process-class)

## 運用時につかえるコマンド

zcatなど、gzipファイル用のコマンドのあとに、awkなどのやりたいコマンドにパイプしてつなげば、たいがいのことはできます。

https://qiita.com/n_slender/items/e5784c66415b53a3f185

# まとめ

知ってしまえば、gzip圧縮したまま、様々な操作をする方法があり、そのやり方も、通常のファイルとさほど変わらない（gzipを扱うためのコマンドやクラスを一段かませるだけ）ことが多いです。

データが大きいほど、圧縮したときの差がでてくるので、大量データを扱うプロジェクトでは、地味に効いてくるテクニックです。