開眼シェルスクリプト2013年10月号

Pocket
LINEで送る

出典: 技術評論社SoftwareDesign

22. 開眼シェルスクリプト 第22回 シェルでドキュメントを操る

 皆様、今年の夏は暑かったでしょうか?
執筆時点で夏の入り口にいますが、
急に暑くなったので脳が溶けております。

 そんな季節の挨拶とは全く関係ありませんが、
今回、次回はメモ書きや原稿など、
不定形のテキストファイルのハンドリングを扱います。
今回はシェルスクリプトというよりは、
便利なコマンドの使い方を羅列していきます。

 ドキュメントを扱う方法は、筆者のようにテキストファイルで
html、reStructuredText、TeXを書く玄人気取りのやり方と、
表計算ソフトに方眼紙を作って書く
(脚注:ピンと来ない人は「エクセル方眼紙」で検索を)、
別の意味で玄人っぽい方法に二分されます。
いや、嘘です。ワープロソフトを使う人が多数派のような気がします。

 どの方法が良いかはその人に依るのですが、
おそらく、これらのやり方の違いが大きく増幅されるのは検索するときです。
基本、ワープロソフトや表計算ソフトに
テキストを書いておいて後から複数のファイルから文字を探すときは、
そのソフトのベンダが作った親切なツールを使う事になります。
しかし、ベンダの気まぐれで何が起こるか分からない怖い部分もあります。
一方、テキストファイルで持っていれば、ベンダの干渉を受けないでしょうが、
findgrep といったコマンドを使いこなす必要があります。

 この議論、やり出すとキリがないので、これ以上はやめておきます。
とにかく今回は、「テキストファイルならお手のもの」
とタイトルに書いてあるように、
テキストファイルを便利に使うスキルを上げないとね!
という立場で話を進めます。

 この連載の主張に呼応するように(嘘)、
国もこういう流れになってきたようです。

(2) オープンデータ推進の意義
これまでも政府は、各府省のホームページ等を通じて保有するデータを公開して
きており、情報提供という観点では一定の成果が出ている。

ただ、これまでのホームページによる情報提供は、基本的に、人間が読む(画面
上で又は印刷して)という利用形態を念頭に置いた形で行われており、検索も難し
く、大量・多様なデータをコンピュータで高速に、横断的に又は組み合わせて処理・
利用することが難しい。

: 二次利用の促進のための府省のデータ公開に関する基本的考え方(ガイドライン)
(仮称)(案)より
http://www.kantei.go.jp/jp/singi/it2/densi/dai3/siryou6.pdf

いつもと違ってお役所文章ですが、
これを格言代わりに本編に進みます。
余談ですが、これを引用したウェブの記事の一つに
「これを受けてデータはExcelで公開すべきだ。」
というものがあってひっくり返りました。そうじゃないでしょう・・・。
人が読むのは最終出力だけで原本はテキストで、
というのが大事だと筆者は考えています。

22.1. 環境

 今回もMacです。 gsed, gawk
のバージョンと共にリスト1にバージョンを示します。
コマンドの用例では gsed, gawk で統一してありますが、
Linuxの多くのディストリビューションでは、
gsedsedgawkawk で大丈夫です。

  • リスト1: 環境
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
uedamac:~ ueda$ uname -a
Darwin uedamac.local 12.2.1 Darwin Kernel Version 12.2.1: Thu Oct 18 16:32:48 PDT 2012; root:xnu-2050.20.9~2/RELEASE_X86_64 x86_64
uedamac:~ ueda$ gawk --version
GNU Awk 4.0.2
Copyright (C) 1989, 1991-2012 Free Software Foundation.
(以下略)
uedamac:~ ueda$ gsed --version
GNU sed version 4.2.1
Copyright (C) 2009 Free Software Foundation, Inc.
(以下略)

22.2. 日本語原稿の文字数を数える

 まずは簡単なところから。
作文をしていて、何文字書いたか調べたいときがありますね。
え?無い?・・・あるということにしてください。

 例えば、リスト2のファイル mistery
内の文字数は、全角スペースを
入れてちょうど60文字です。

  • リスト2: 例題ファイルその1
1
2
3
4
5
uedamac:MEMO ueda$ cat mistery
 朝目覚めると、私は全身を繭で
覆われた蛹になっていたのです。
私は大変困ってしまいました。「
会社に休みの連絡ができない。」

 こういうときは、リスト3のようにやります。

  • リスト3: 文字数を数える
1
2
uedamac:MEMO ueda$ cat mistery | wc -m
      64

リスト3のように、 wc にオプション -m をつけると、
今のロケールに合わせて文字数を数えてくれます。
ロケールを変えるとリスト4のように出力に違いが出ます。

  • リスト4: ロケール(環境変数 LANG )で挙動が変わる
1
2
3
4
5
6
uedamac:MEMO ueda$ echo $LANG
ja_JP.UTF-8
uedamac:MEMO ueda$ cat mistery | LANG=C wc -m
     184
uedamac:MEMO ueda$ cat mistery | LANG=ja_JP.UTF-8 wc -m
      64

 しかし、これだと改行も記号も文字数に
カウントされてしまっています。
次のように trsed
で字を削っておくと正解が出るので、
正確に数えたいならこのようにします。

  • リスト5: 文字数を正確に数える
1
2
3
4
5
6
uedamac:MEMO ueda$ cat mistery | tr -d '\n' | wc -m
      60
#全角スペースも数えたくない場合
uedamac:MEMO ueda$ cat mistery | tr -d '\n' |
gsed 's/ //g' | wc -m
      59

 もう1,2数え方をリスト6に紹介しておきます。
gsed を使う方法は、私の手癖になっているものです。
gawk の方法は、ロケールが日本語でも
AWKのコマンドの種類によっては
バイト数になってしまうので注意が必要です。

  • リスト6: 文字数を正確に数える
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
uedamac:MEMO ueda$ cat mistery | gsed 's/./&\n/g' |
wc -l
      64
uedamac:MEMO ueda$ cat mistery | gsed 's/./&\n/g' |
gawk 'NF!=0' | wc -l
      60
uedamac:MEMO ueda$ cat mistery |
gawk '{a+=length($0)}END{print a}'
60
#Mac等ではgawkを明示的に指定しないと
#このようになってしまうので注意
uedamac:MEMO ueda$ cat mistery |
awk '{a+=length($0)}END{print a}'
180

 もっと長い文章について、
どれだけ書いたかざっくり知りたい場合は、
バイト数で考えてもよいでしょう。
例えば筆者はこの原稿を毎月6ページずつ書くのですが、
他の月と比較してどれだけ書いたか、
wc コマンドでリスト7のように調査しています。

  • リスト7: どれだけ書いたかバイト数や行数でざっくり調べる
1
2
3
4
5
6
7
8
9
uedamac:SD_GENKOU ueda$ wc 201???.rst
     549     803   24653 201201.rst
     428    1064   19469 201202.rst
     (中略)
     514     945   20703 201307.rst
     554     948   18805 201308.rst
     482     905   20520 201309.rst
     165     314    6616 201310.rst
   11016   21368  448677 total

・・・あと4ページくらい書かなければ原稿料を頂けないようです。
これでバイトあたりの原稿料が計算できますが、
雑念が入るので計算しないでおきます。

22.3. 文章の抜き出し

 次に扱うのは、テキストファイルの一部分を抜き出すテクニックです。
例えば、次のような連絡先メモがあるとします。

  • リスト8: 例題ファイルその2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
uedamac:201310 ueda$ cat address
<幹事会>

- 鎌田
        - 略称: ()
        - TEL: 090-1234-xxxx
        - email: kama@kama.gov

- 濱田
        - 略称: ()
        - TEL: 080-5678-xxxx
        - email: ha@haisyou.ac.jp

 例えば濱田さんの電話番号が知りたいとします。
このようなとき、普通に grep を使おうとしても、

uedamac:201310 ueda$ grep 濱田 address
- 濱田

という残念な目にあったことのある人もいると思います。

 実は、 grep には -A というオプションがあります。
これを使うとリスト9のように、
検索で引っかかった行の後ろも出力してくれます。
これでいちいち less を使ったりエディタ開いたりしなくて済みます。
less を使うのはそこまで面倒くさがることでもないですが・・・。

  • リスト9: grep-A オプション
1
2
3
4
5
uedamac:201310 ueda$ grep 濱田 -A 3 address
- 濱田
        - 略称: ()
        - TEL: 080-5678-xxxx
        - email: ha@haisyou.ac.jp

 逆に、電話番号から人の名前を検索してみましょう。
リスト10のようにします。

  • リスト10: grep-B オプション
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
uedamac:201310 ueda$ grep 080-5678-xxxx -B 2 address
- 濱田
        - 略称: ()
        - TEL: 080-5678-xxxx
#補足:-Aと-Bを併用することも可能
uedamac:201310 ueda$ grep 080-5678-xxxx -B 2 -A 1 address
- 濱田
        - 略称: ()
        - TEL: 080-5678-xxxx
        - email: ha@haisyou.ac.jp

 次はHTMLファイルを扱ってみましょう。
HTMLから狙ったところをワンライナーで切り出してみましょう。
これから扱うような処理は、
ブラウザでソースを表示してマウスでコピペでもよいのですが、
何十、何百も同じ処理を繰り返すことになったらそうもいきません。

 まず、筆者のブログからコードの部分だけ切り取るということをやってみます。
2013年7月14日現在で、筆者のブログのトップページにはいくつかコードが
掲載されているのですが、コードはHTML上で <pre>
</pre> に囲まれています。
リスト11のリストのように curl コマンドでHTMLを取得して
less で読んでみましょう。
このような部分がいくつか出現します。

  • リスト11: 例題のHTML
1
2
3
4
5
6
7
8
9
uedamac:~ ueda$ curl http://blog.ueda.asia | less
(略)
<pre class="brush: bash; title: ; notranslate" title="">
Python 2.7.2 (default, Oct 11 2012, 20:14:37)
(略)
&gt;&gt;&gt; round(-1.1,-1)*1.0
-0.0
</pre>
(略)

 このような抽出は sed の得意技で、
リスト12のようにコマンドを書けばコード
(pre要素)だけ抽出することができます。

  • リスト12: コードだけ取り出す
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
...
uedamac:~ ueda$ curl http://blog.ueda.asia 2> /dev/null |
nkf -wLux | gsed -n '/<pre/,/<\/pre/p' > ans
uedamac:~ ueda$ less ans
(略)
<pre class="brush: bash; title: ; notranslate" title="">
Python 2.7.2 (default, Oct 11 2012, 20:14:37)
(略)
</pre>
<pre class="brush: bash; title: ; notranslate" title="">
uedamac:~ ueda$ cat hoge.sh
#!/bin/bash -xv
(略)
</pre>
...

 ここでのポイントは、 sed の使い方と、
curl したらすぐに nkf をすることの2点でしょう。

  sed については、
本連載では文字列の置換で使うことがほとんどですが、
/<正規表現1>/,/<正規表現2/p (pコマンド)で、
正規表現1にマッチする行から正規表現2
にマッチする行まで抜き出すことができます。
この処理は正規表現2のマッチが終わると再度実行されるので、
上の例ではいくつもpre要素を抜き出す事ができています。
オプション -n は、 sed はデフォルトで全行を出力するので、
それを抑制するために使います。
-n をつけておかないと、pコマンドの出力対象行が2行ずつ、
その他の行が1行ずつ出力されてしまいます。

  curl の出力は、例え読み取ったHTMLがUTF-8
で書いてあっても改行コードが
UNIX標準のものと違っている可能性があるので、
このようなときは必ず通します。
オプションは -wLux が私の場合は手癖になっており、

  • w : UTF-8に変換
  • Lu : 改行コードをLF(0x0a)に
  • x : 半角カナから全角カナへの変換を抑制

という意味があります。

 ただ、このようにHTMLがきれいに
改行されていればあまり苦労もないのですが、
実際はそうもいきません。
リスト13のようなHTMLもあるでしょう。

  • リスト13: 例題ファイルその3
1
2
3
4
5
6
7
8
uedamac:201310 ueda$ cat kitanai.html
<pre>#!/bin/bash

echo "きたない"</pre>あははは<pre>
#!/bin/bash

echo "きたなすぎる"
</pre>

こういうときは、リスト14のように自分で掃除するしかありません。
このsedのワンライナーはお世辞にもきれいとは言えないので、
ちゃんとプログラムを書いた方がいいかもしれません。
ただ、結局この方が早いことが多いです。

  • リスト14: きたないHTMLを掃除するワンライナー
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
uedamac:~ ueda$ cat kitanai.html |
#<pre>の後に何か文字があると改行を差し込む
gsed 's;\(<pre[^>]*>\)\(..*\);\1\n\2;g' |
#<pre>の前に何か文字があると改行を差し込む
gsed 's;\(..*\)\(<pre[^>]*>\);\1\n\2;g' |
#</pre>の前に何か文字があると改行を差し込む
gsed 's;\(..*\)</pre>;\1\n</pre>;g' |
#</pre>の後に何か文字があると改行を差し込む
gsed 's;</pre>\(..*\);</pre>\n\1;g'
<pre>
#!/bin/bash

echo "きたない"
</pre>
あははは
<pre>
#!/bin/bash

echo "きたなすぎる"
</pre>

 この例題の最後に便利な小ネタを。
さきほどpreで抜き出したHTMLには

&gt;&gt;&gt; round(-1.1,-1)

などと、記号の一部が文字実体参照に変換されています。
例えば &gt;> が置き換わったものです。

 また、次のように数値参照になっているときもあります。

&#x4e0a;&#x7530;&#x53c2;&#x4e0a;

HTMLから抜き出して来たら、
このままにするより元に戻した方がよいでしょう。

 数値参照の方はリスト15のように nkf でできます。

  • リスト15: 数値参照を nkf でデコードする
1
2
3
uedamac:~ ueda$ echo '&#x4e0a;&#x7530;&#x53c2;&#x4e0a;' |
nkf --numchar-input
上田参上

 文字実体参照の方は nkf でできません。残念。
しかし、 "&<> とスペース程度ならあまり個数がないので
リスト16のようにsedスクリプトを書くとよいでしょう。
コマンド化してもいいですね。

  • リスト16: 文字実体参照を置換するsedスクリプトを作って使う
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#このようなsedスクリプトを作る
uedamac:~ ueda$ cat ref.sed
s/&lt;/</g
s/&gt;/>/g
s/&quot;/"/g
s/&amp;/\&/g
s/&nbsp;/ /g
uedamac:~ ueda$ curl http://blog.ueda.asia 2> /dev/null |
sed -n '/<pre/,/<\/pre/p' | gsed -n '1,/<\/pre/p' |
sed -f ./ref.sed
<pre class="brush: bash; title: ; notranslate" title="">
(略)
>>> round(-1.1,-1)*1.0
-0.0
</pre>

 他の文字実体参照も変換しなければならないときは、
他の言語のライブラリを使って
変換コマンドを書くのが一番簡単な方法です。
・・・しかし、皆さんには文字実体参照の一覧を掲載した
ウェブサイトからtableを抜き出し、
ref.sed のようなスクリプトを
ワンライナーで作ることをおすすめしておきます。

22.4. find, grep, xargsの組み合わせ

 最後にファイルの検索をやってみます。
ディレクトリの中から何かテキストを探すときは、
findxargs
を組み合わせると自由自在な感じになります。

  find については、
名前で誤解を受けやすいのですが、
単に指定したディレクトリの下のファイルや
ディレクトリを延々と出力するだけです。
find はオプションが多い事でも知られていますが、
リスト17のような使い方だけ知っておけばよいと思います。
オプションの . はカレントディレクトリ、
-type f はファイルだけ表示しろということです。

  • リスト17: find を使う
1
2
3
4
5
uedamac:SD_GENKOU ueda$ find . -type f | head -n 4
./.201203.rst.swp
./.201310.rst.swp
./.DS_Store
./.git/COMMIT_EDITMSG

 出力は1レコード1ファイルorディレクトリと、
UNIXの教科書通りなので、
ファイル名で検索するときはパイプで grep
をつなげばよいということになります。

 例えば、ミーティング中にとっさにとったメモ
をどこに保存したか忘れたが、何を書いたかはうっすら覚えている場合、
(そしてメモを取るときは必ずファイル名に
memoMEMO を入れている場合、)
リスト18のようなワンライナーで探し出すことができます。

  • リスト18: findgrepxargs を組み合わせる
1
2
3
4
5
uedamac:SD_GENKOU ueda$ find ~ -type f | fgrep -v "/." | grep -i memo | xargs grep 徹夜 | gsed 's/:.*//' > hoge
uedamac:SD_GENKOU ueda$ cat hoge
/Users/ueda/Dropbox/USP/memo/memo
uedamac:SD_GENKOU ueda$ cat hoge | xargs cat
徹夜で仕事しろと言われた(このメモはフィクションです。)

 これが自在にできれば、
某OSで検索のときに出て来る犬に頼る必要はありません。
findgrep で検索をかけるときによく使うイディオムを挙げておきます。

  • find . | grep hoge : ファイル名の検索
  • grep -r hoge ./ : ディレクトリ下の全ファイルの中身を検索
  • grep -r hoge ./ | gsed 's/:.*/' | uniq : ディレクトリ下の全ファイルの中身を検索し、ファイル名のリストを抽出
  • grep -r hoge ./ | gsed 's/:.*/' | uniq | xargs cat : ディレクトリ下の全ファイルの中身を検索し、ファイル名のリストを抽出し、抽出したファイルの中身を表示

22.5. おわりに

 今回はテキストファイルをハンドリングする
ノウハウをいくつか紹介しました。
この手のノウハウは無数にあり、ほんの一部分を
つまみ食いでだらだら紹介してしまった感もありますが、
CUIで自分の文章を管理するときに実際にどんな
コマンドの使い方をしているのか、
雰囲気くらいはお伝えできたかと思います。

 次回は表記揺れに的をしぼって、何か作り物をしてみる予定です。
表記ゆれのテストスクリプトを書きます。
原稿書きもテストファーストの時代へ・・・(大げさ)

Pocket
LINEで送る