シェルのパイプでpythonを使いたい

# :bath: シェルのパイプでpythonを使いたい

パイプは[Unix哲学](https://en.wikipedia.org/wiki/Unix_philosophy)に基づいた素晴らしい発明です。pythonも素晴らしい言語です。しかしながらパイプとpythonの相性は悪いです。この記事はコマンドの出力をパイプ経由でpythonに渡して処理する方法について記載します。

## コマンドをパイプで繋ぐ
```sh
$ echo "私は東京近郊の生まれです" |sed 's/東京近郊/埼玉/g'
私は埼玉の生まれです
```
こういうやつですね。便利です。コマンドを組み合わせて色々なことができます。

## 2022年東京都自転車盗難件数データ（CSV）を処理してみる
例としてCSVデータを標準的なコマンドとパイプを駆使して処理してみます。
サンプルデータとして警視庁が公開している2022年の東京で発生した自転車盗難の発生件数データを使います。

https://www.keishicho.metro.tokyo.lg.jp/about_mpd/jokyo_tokei/jokyo/hanzaihasseijyouhou.files/tokyo_2022zitensyatou.csv

データ件数: 21612, 改行コード: CRLF, 文字コード: ShiftJIS

| No  | 項目                       | 値の例     |
| --- | -------------------------- | ---------- |
| 1   | 罪名                       | 窃盗       |
| 2   | 手口                       | 自転車盗   |
| 3   | 管轄警察署（発生地）       | 中央       |
| 4   | 管轄交番・駐在所（発生地） | 八丁堀交番 |
| 5   | 市区町村コード（発生地）   | 131024     |
| 6   | 都道府県（発生地）         | 東京都     |
| 7   | 市区町村（発生地）         | 中央区     |
| 8   | 町丁目（発生地）           | 日本橋兜町 |
| 9   | 発生年月日（始期）         | 2022/8/25  |
| 10  | 発生時（始期）             | 9          |
| 11  | 発生場所                   | 道路上     |
| 12  | 発生場所の詳細             | その他     |
| 13  | 被害者の年齢               | 65-69歳    |
| 14  | 被害者の職業               | その他     |
| 15  | 施錠関係                   | 施錠せず   |
|    |                            |            |

こういったデータ処理に関しては世の中に便利なツール(pandasとか)が沢山あると思いますが、とりあえず簡単に処理したいときやデータが大きい時にコマンドは便利です。今回の自転車盗難件数データは21612件なのでエクセルでも処理できますが、データが数千万件～数億件などになってくると他のツールでは扱えない場合もあります。パイプを使ってストリーム的に処理すればメモリが不足することもなく大きなデータを扱えます。ちなみにcsvデータの扱いおいては[xsv](https://github.com/BurntSushi/xsv)が便利ですが、標準的なコマンドでとパイプを使った場合について説明したいのでここでは触れません。

まず文字コードがShift-JISなのでUTF-8に変換しましょう (F●ck!)。これもパイプで処理します。
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w
```

「新宿区」で発生した盗難のみ抽出してます。（3件のみ表示）
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w |grep '新宿区' |head -n 3
窃盗,自転車盗,牛込,八幡前交番,131041,東京都,新宿区,市谷砂土原町３丁目,2022/10/7,18,一戸建住宅,駐車（輪）場,50歳代,その他,施錠せず
窃盗,自転車盗,牛込,八幡前交番,131041,東京都,新宿区,市谷田町１丁目,2022/1/22,16,駐車（輪）場,駐車（輪）場,20歳代,その他,施錠した
窃盗,自転車盗,牛込,八幡前交番,131041,東京都,新宿区,市谷田町２丁目,2022/3/17,18,その他,駐車（輪）場,20歳代,大学生,施錠した
```
何件あるのかカウントします。
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w |grep '新宿区' |wc -l
915
```
「中央区」について調べてみます。
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w |grep '中央区' |wc -l
256
```
やはり中央区の方が少ないですね。納得できる結果です。
面倒なので市区町村ごとの発生件数を集計します。

市区町村ごとの発生件数TOP10
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w |cut -d , -f 7|sort |uniq -c |sort -nr|head -n 10
   1400 世田谷区
   1295 江戸川区
   1246 大田区
   1062 練馬区
   1059 足立区
    922 板橋区
    915 新宿区
    827 中野区
    799 江東区
    760 葛飾区
```
世田谷区がトップです。これは人口が多いためでしょう。中野区、江戸川区が人口の割に多めです。

発生場所で集計してみます。
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w |cut -d , -f 11|sort |uniq -c |sort -nr
   5338 ４階建て以上共同住宅
   4510 道路上
   3937 その他
   2991 その他の住宅（３階建て以下共同住宅等）
   2840 駐車（輪）場
   1995 一戸建住宅
```
大きいアパート、マンションで盗難が多いようです。自分のアパートだからといって安心できないですね。

施錠関係と組み合わせてみましょう。
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w |cut -d , -f 11,15|sort |uniq -c |sort -nr
   2949 ４階建て以上共同住宅,施錠せず
   2617 道路上,施錠せず
   2498 その他,施錠せず
   2389 ４階建て以上共同住宅,施錠した
   1893 道路上,施錠した
   1767 その他の住宅（３階建て以下共同住宅等）,施錠せず
   1570 駐車（輪）場,施錠せず
   1532 一戸建住宅,施錠せず
   1439 その他,施錠した
   1270 駐車（輪）場,施錠した
   1224 その他の住宅（３階建て以下共同住宅等）,施錠した
    463 一戸建住宅,施錠した
      1 発生場所,施錠関係
```
施錠していても結構やられています。これはどういった施錠であったか詳細をデータに追加してほしいところです。

何曜日の何時くらいに件数が多いのか気になったので調べてみます。
項目には日付しかありませんので日付から曜日を取得する必要があります。そろそろ辛い感じになってきました。少し処理に時間がかかりますが、標準的なコマンドとパイプだけで集計できます（パイプ万歳）。

曜日、時間ごとの発生件数（TOP30）
```sh
tail -n +2 tokyo_2022zitensyatou.csv |nkf -w |cut -d , -f 9,10|grep -v 不明|  while IFS=, read d h; do echo $(date '+%A' -d $d) $h ;done |sort |uniq -c |sort -nr |head -n 30
    323 Friday 18
    319 Saturday 18
    275 Wednesday 17
    268 Tuesday 18
    268 Friday 19
    266 Sunday 18
    266 Monday 18
    265 Friday 17
    263 Thursday 18
    260 Friday 20
    250 Sunday 17
    245 Saturday 17
    243 Thursday 17
    241 Wednesday 18
    230 Tuesday 17
    230 Saturday 20
    229 Monday 17
    224 Saturday 19
    223 Saturday 16
    213 Thursday 19
    205 Sunday 12
    201 Sunday 16
    198 Friday 21
    196 Wednesday 19
    195 Friday 22
    194 Saturday 12
    193 Monday 19
    193 Friday 16
    190 Wednesday 20
    190 Friday 8
```
時間帯は夕方18時前後が多いようです。上記の結果には出てきていないですが、深夜帯はそれほど多くないです。計画的に人目のつかない時間帯に犯行するというよりも、帰宅の際に出来心で犯行に及ぶという感じですね。

念のため（？？）、被害者の職業や年齢で集計してみます。
```sh
tail -n +2 tokyo_2022zitensyatou.csv |nkf -w |cut -d , -f 13,14|sort |uniq -c |sort -nr
   4239 20歳代,その他
   3639 30歳代,その他
   2924 40歳代,その他
   2120 50歳代,その他
   1732 10歳代,高校生
   1460 20歳代,大学生
   1195 10歳代,中学生
   1131 70歳以上,その他
    880 10歳代,大学生
    573 60-64歳,その他
    477 10歳代,小学生
    457 65-69歳,その他
    410 10歳代,その他
    199 法人・団体、被害者なし,法人・団体、被害者なし
    162 10歳未満,小学生
      8 30歳代,大学生
      3 20歳代,高校生
      1 40歳代,高校生
      1 40歳代,大学生
```
これは単純に自転車を乗っていることが多い年齢に相関がありそうです。`162 10歳未満,小学生` は可哀そうなのでやめてほしいです。というか誰が盗むのでしょう。

## パイプでpythonに繋いでみる

ここまで自転車盗難事件の分析記事をお送りしておりますが、そういえばこれはpythonアドベントカレンダー7日目の記事のはずです。

方向性を変えて入力データから次の項目を記載の順番通りに抽出してみます。

- 発生年月日（始期）
- 市区町村（発生地）
- 町丁目（発生地
- 施錠関係

cutでは項目順を前後させることができないのでawkを使います。
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w |awk -F , 'OFS="," { print $9,$7,$8,$15; }'|head -n 5
発生年月日（始期）,市区町村（発生地）,町丁目（発生地）,施錠関係
2022/8/25,中央区,日本橋兜町,施錠せず
2022/3/29,中央区,八丁堀１丁目,施錠せず
2022/5/30,中央区,八丁堀２丁目,施錠せず
2022/1/26,中央区,八丁堀３丁目,施錠せず
```

私はawkをそれなりに使うのですが、全くその文法を覚えられません。manを見ても良く分かりません。今回もネットで使い方を調べました。いつもawkを使う必要が出てきた時に「pythonにパイプで繋いで処理できれば良いのに」と考えるわけです。
そこでpythonにつないで同じことを処理してみます。ワンライナーで書くと以下のようになります。

```sh
$ tail -n +2 tokyo_2022zitensyatou.csv |nkf -w |python -c "import sys; [print(rec[8], rec[6], rec[7], rec[14], sep=',') for line in sys.stdin for rec in (line.rstrip().split(','),)]"|head -n 5
2022/8/25,中央区,日本橋兜町,施錠せず
2022/3/29,中央区,八丁堀１丁目,施錠せず
2022/5/30,中央区,八丁堀２丁目,施錠せず
2022/1/26,中央区,八丁堀３丁目,施錠せず
2022/3/14,中央区,八丁堀３丁目,施錠した
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<string>", line 1, in <listcomp>
BrokenPipeError: [Errno 32] Broken pipe
```

または改行を使って以下のようにします。

```sh
$ tail -n +2 tokyo_2022zitensyatou.csv |nkf -w |python -c "
import sys
for line in sys.stdin:
  rec = line.rstrip().split(',')
  print(rec[8], rec[6], rec[7], rec[14], sep=',')
" |head -n 5
2022/8/25,中央区,日本橋兜町,施錠せず
2022/3/29,中央区,八丁堀１丁目,施錠せず
2022/5/30,中央区,八丁堀２丁目,施錠せず
2022/1/26,中央区,八丁堀３丁目,施錠せず
2022/3/14,中央区,八丁堀３丁目,施錠した
Traceback (most recent call last):
  File "<string>", line 5, in <module>
BrokenPipeError: [Errno 32] Broken pipe
```

ワンライナーはかなり厳しいですね。改行を使う場合も、これなら普通にpythonスクリプトを書いて処理した方が良いです。またheadにつないでBrokenPipeErrorが発生しているのも鬱陶しいです。残念ながらpythonの`-c`オプションはパイプから接続して使うものではありません。ちなみにperlならワンライナーを各行に適用するオプション(`-n`)があり便利です。

## pypipe

シェルとpythonを組み合わせて使いたいと思っている人は少なくないようです。[xonsh](https://xon.sh/)というシェルがあります。pythonとシェルスクリプトが融合した魅力的なシェルですが、個人的にbashやzshを使いたいので自分には向いていませんでした。そこでpypipeというツールを書きました。

https://github.com/bugen/pypipe

少し前にzennにも紹介記事を書きました。
https://zenn.dev/bugen/articles/82677e047891f9

### :shower: pypipeの機能　
- pythonコードを自由に記述
- 行単位での処理
- csv, tsvサポート
- json, jsonlinesサポート
- カウント機能
- 自動型変換
- 自動モジュールインポート
- viewモード
- Pagerサポート
- カスタムコマンド


pypipeを使った場合、これまでの例は以下のようになります。

パイプの例
```sh
echo "私は東京近郊の生まれです" |ppp 'line.replace("東京近郊", "埼玉")'
```
※`ppp`はpypipeのエイリアスです。

新宿区での発生件数
```sh
cat tokyo_2022zitensyatou.csv |nkf -w |ppp csv --filter 'f7 == "新宿区"' |head -n 5
```
市区町村ごとの発生件数集計
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w |ppp csv --counter f7
```
発生場所、施錠関係での集計
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w |ppp csv --counter f11,f15
```
曜日、時間での集計
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w|ppp csv -H --counter -f 'f9!="不明"' 'date=datetime.datetime(*map(int, f9.split("/")))' 'date.strftime("%A"), f10'
```
項目抽出
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w|ppp csv f9,f7,f8,f15
```

データを確認するためのview機能もあります。
```sh
$ cat tokyo_2022zitensyatou.csv |nkf -w|head -n 3 |ppp csv --view --header
[Record 1]
1  | 罪名                       | 窃盗
2  | 手口                       | 自転車盗
3  | 管轄警察署（発生地）       | 中央
4  | 管轄交番・駐在所（発生地） | 八丁堀交番
5  | 市区町村コード（発生地）   | 131024
6  | 都道府県（発生地）         | 東京都
7  | 市区町村（発生地）         | 中央区
8  | 町丁目（発生地）           | 日本橋兜町
9  | 発生年月日（始期）         | 2022/8/25
10 | 発生時（始期）             | 9
11 | 発生場所                   | 道路上
12 | 発生場所の詳細             | その他
13 | 被害者の年齢               | 65-69歳
14 | 被害者の職業               | その他
15 | 施錠関係                   | 施錠せず

[Record 2]
1  | 罪名                       | 窃盗
2  | 手口                       | 自転車盗
3  | 管轄警察署（発生地）       | 中央
4  | 管轄交番・駐在所（発生地） | 八丁堀交番
5  | 市区町村コード（発生地）   | 131024
6  | 都道府県（発生地）         | 東京都
7  | 市区町村（発生地）         | 中央区
8  | 町丁目（発生地）           | 八丁堀１丁目
9  | 発生年月日（始期）         | 2022/3/29
10 | 発生時（始期）             | 9
11 | 発生場所                   | その他の住宅（３階建て以下共同住宅等）
12 | 発生場所の詳細             | その他
13 | 被害者の年齢               | 40歳代
14 | 被害者の職業               | その他
15 | 施錠関係                   | 施錠せず
```

本記事ではcsvを例にしましたが、pypipeは別にcsvに特化したツールではありません。パイプからの入力をpythonで処理するためのツールです。使い方の詳細やインストール方法は[github](https://github.com/bugen/pypipe)で。

興味ある方はぜひ使ってみてください。
フィードバックもお待ちしております。

## まとめ

- 自転車は鍵をしていても盗まれる
- 特に夕方に盗まれやすい
- パイプは良いものである
- パイプとpythonの相性が悪い
- pypipeを使うとパイプ経由でpythonが簡単に使える
- この記事はpypipeの提灯記事