CSV変形のお供に。テキストの一部分にだけコマンド適用するツールを作ってみた。

コマンドラインで、テキストの一部分にだけ変換コマンドを適用するためのツールを作りました。
2020.01.29

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、ターミナル住人の平野です。

ここの所Pythonでちょっとしたツールを作る系の記事を書きましたが、 今回はその合わせ技的な感じで、テキストの一部だけを変換するCLIツールを作ってみました。

https://github.com/cm-hirano-shigetoshi/partial_transform

過去の記事

何ができる?

標準入力で入ってきたテキストの、N番目要素だけにコマンドを適用させることができます。

例えば以下のように、「tsvファイルの2番目の要素にのみrev(stringを逆にする)を適用する」などが実現できます。

$ cat sample.tsv
policyID    statecode   county  eq_site_limit   hu_site_limit
119736  FL  CLAY COUNTY 498960  498960
448094  FL  CLAY COUNTY 1322376.3   1322376.3
206893  FL  CLAY COUNTY 190724.4    190724.4
333743  FL  CLAY COUNTY 0   79520.76
172534  FL  CLAY COUNTY 0   254281.5
785275  FL  CLAY COUNTY 0   515035.62

$ cat sample.tsv | partial_transform 2 'rev'
policyID    edocetats   county  eq_site_limit   hu_site_limit
119736  LF  CLAY COUNTY 498960  498960
448094  LF  CLAY COUNTY 1322376.3   1322376.3
206893  LF  CLAY COUNTY 190724.4    190724.4
333743  LF  CLAY COUNTY 0   79520.76
172534  LF  CLAY COUNTY 0   254281.5
785275  LF  CLAY COUNTY 0   515035.62

statecodeの列が全て逆順になりました。

第一引数に適用するインデックス、第二引数に変換コマンドを指定します。 デリミタは、デフォルトではawkと同じようにスペースとタブで区切ります。

適用させるコマンドの中にはパイプ(|)なども記述できます。

$ cat sample.tsv | partial_transform 2 'rev | tr "A-Z" "a-z"'
policyID    edocetats   county  eq_site_limit   hu_site_limit
119736  lf  CLAY COUNTY 498960  498960
448094  lf  CLAY COUNTY 1322376.3   1322376.3
206893  lf  CLAY COUNTY 190724.4    190724.4
333743  lf  CLAY COUNTY 0   79520.76
172534  lf  CLAY COUNTY 0   254281.5
785275  lf  CLAY COUNTY 0   515035.62

-Fでデリミタを指定できます。 タブ区切り指定をすることでCLAY COUNTRYという文字列全体を変換できます。 なお、デリミタに指定できるのは今の所1文字だけです。

$ cat sample.tsv | partial_transform -F '\t' 3 'rev'
policyID        statecode       ytnuoc  eq_site_limit   hu_site_limit
119736  FL      YTNUOC YALC     498960  498960
448094  FL      YTNUOC YALC     1322376.3       1322376.3
206893  FL      YTNUOC YALC     190724.4        190724.4
333743  FL      YTNUOC YALC     0       79520.76
172534  FL      YTNUOC YALC     0       254281.5
785275  FL      YTNUOC YALC     0       515035.62

基本的に上記が全機能です。単純ですね。

こだわった点としては、パイプライン的に処理が完了した行から出力されるようにしたことです。 というか、この要件を満たさなくて良ければ何も難しいことがないので、 ここを頑張りましたというのがこの記事の主題ですw

インストール

Pythonで実装しているのでpipでインストールできます。 Python3.7で動作確認しています。

pip install git+https://github.com/cm-hirano-shigetoshi/partial_transform

プログラムの解説

本体は単一のPythonファイルになっていますが、 内部は完全に2つのプログラムに別れています (もともとちゃんとしたコマンドにしていなかったので、1ファイルの方が使い勝手が良かったので)し、 サブプロセスを2つ使って非同期的に動くのでちょっと複雑です。 Pythonファイルを便宜的にプログラム1,プログラム2に分けると、 こんな構成になっています。

プログラム1

  • 2つのサブプロセスを起動します
    • テキスト変換するサブプロセス1
    • プログラム2を動かすサブプロセス2
  • 標準入力を受け取ります
  • 標準入力を1行ずつ読み、先頭部分、変換対象部分、末尾部分に分ける
    • 先頭部分、末尾部分には、何行目という情報などを付加して名前付きパイプ1へ
    • 変換対象部分は名前付きパイプ2へ

プログラム2

  • 名前付きパイプ1から読み込み、1つの行を再構成する
    • 1つの行を構成する3要素が揃うまではメモリにプールしておく

課題

  • 指定インデックスに該当がないとき、変換を行わないようにしたい
    • 5を指定したけど要素が3つしかないとき、今はカラ文字列を変換した結果を格納しています。
    • これは実装で修正できるはずなので、そのうち実装しようと思います。

他の実装方法での失敗例

蛇足ながら、いくつかのロジックを試した上での失敗例を振り返ります。

Pythonでの変換じゃだめなの?

文字列の変換の部分をPythonで変換するなら何も難しいことはないです。 1行ずつ処理を施しては出力して、を繰り返すだけの何の変哲もないプログラムです。 ただ今回はコマンドライン上で普通のパイプっぽく変換したかったのでそもそも想定外です。

1行ずつサブプロセスで変換してみる

確かにこれでやりたいことはできなくはないです。 ただし、ものすごく遅いです。 1行ごとにサブプロセスを作成して、その処理終了を待って、サブプロセスを破棄して、と行うため、 数十行でも如実に遅くなるので実用はできません。

擬似的にパイプライン処理風を装う

一定の行数ごとに出力を行うことで、擬似的にパイプラインに見せかける方法です。 これは特に失敗ではなく、実際使用上の不具合もなさそうです。 事実、後続のコマンドにパイプで渡す場合、このコマンドからの出力は完全バッファリングなので、 適切なバッファ量を設定すれば実用上の問題はないのではないかと思います。

実際最初はこの実装で満足していましたが、 バッファ量の調節という要素を残したくなかったのと、 なんとかうまくやる方法が思いついたので、今回のような実装に直してみました。

複数の名前付きパイプ

最終的な形としては、変換後のテキストを変換しないテキストと同じ名前付きパイプに渡し、 行に付けたインデックスを頼りに行の再構成をしていますが、 最初は、それぞれ別の名前付きパイプを用意して、それらを開いて1行ずつ読み込めば良いのでは?と考えました。 このほうが行の再構成がシンプルになるのでできればこちらが良かったのですが、 結果的にこれだと上手くいきませんでした。

きちんとした原因がわかっている訳ではないのですが、 名前付きパイプを単一プロセスから複数開いた場合、 その名前付きパイプへの書き込みが途中で止まってしまうようでした。

何かもっと詳細な原因がわかる方がいれば教えて頂きたいです。

まとめ

ターミナルでパイプを流れるテキストの一部分だけを変換するスクリプトをPythonで作ってみました。

以前から、一部だけいい感じに変換できないもんかな? と思っていたので、それが形になって満足です。 これでCSVファイルの一部だけ変換とかがやりやすくなりました!!
よろしければご活用ください。