UNIX基礎演習

権藤研 全体ゼミ 2020/04/16
新山

0. ウォームアップ問題

ここでは、以下の常識をチェックします:

( ) 内はおおまかな目安時間です。

  1. Python 3.8 をインストールせよ。3.7でもよい。(5分)
  2. 以下のようなCSVファイルから単語と出現回数を読み込み、 回数が多い順にソートして表示するプログラムを書け。 なお、同じ単語が複数回出てきた場合は、それらの合計を使用するものとする: (15分)
    foo,3
    baa,1
    john,5
    foo,1
    
    出力例:
    john,5
    foo,4
    baa,1
    
  3. SQLite3 をインストールせよ。(5分)
  4. 以下の表を作成し値を入力する SQL を書け。(10分)
    IdNameScore
    1Alice100
    2Bob50
    3Carol75
  5. 上の表から、Scoreが50点であるような行を表示する SQL を書け。(5分)
  6. a001.txt, a002.txt, ... という名前のついた 複数のファイルがあるとする。これらの名前を一括して a001.html, a002.html, ... に変更するような シェルスクリプトを書け。 (5分)
  7. 上のスクリプトで、ファイル名の一覧があらかじめ決まっておらず、 あるテキストファイル files.txt 中に一行ずつ書かれている場合は スクリプトをどう変更すべきか。(5分)

1. UNIXスクリプト処理の基本

1.1. シェルの基本

特殊な記号 ({, }, $, *, ;) を 引数に渡す場合は '〜' で囲む。

演習. カレントディレクトリの中にある「-t」というファイル中から * という文字列を検索する grep コマンドを書け。

1.2. 変数の活用

a="foo"
echo "$a"
b="$a $a"
c='$a $a'
d='$a '"$a"
演習. シェル変数と環境変数の違いは何か?

1.3. Historyを活用する

「こんなコマンドを実行したはずなんだけど、なんだっけ」

$ history | grep なんか

注意: historyファイルはときどき消されることがある。

過去のコマンドラインをすべて記録しておく

function _prompt_cmd {
    local s=$?
    echo "`date '+%Y-%m-%d %H:%M:%S'` $HOSTNAME:$$ $PWD ($s) " \
         "`history 1`" >> $MYHISTFILE
}
PROMPT_COMMAND=_prompt_cmd

コマンドラインの記録は、そのまま実験ノートにもなる。 あとで実験手順を再現したいときに参考になる。

1.4. パイプの使い方

演習. 以下のコマンドラインを順に実行し、 つぎの表現が何をするか予測せよ。
$ ls -l
$ ls -l | wc
$ ls -l | sort
$ ls -l | sort -k4
$ ls -l | sort -k4 -r
$ ls -l | sort -k4 -r -n
$ ls -l | awk '{print $5;}'
$ ls -l | awk '{a+=$5;}'
$ ls -l | awk 'BEGIN{a=0;} {a+=$5;} END{print(a);}'
$ ls -l | awk '/euske/ {print $4;}' | uniq
$ ls -l | awk '/euske/ {print $4;}' | uniq -c
$ ls -l | awk '/euske/ {print $4;}' | uniq -c | wc
$ find ~
$ find ~ -type f
$ find ~ -type f | grep test
$ find ~ -type f | grep -i test
$ find ~ -type f | grep -i test | wc
$ find ~ -type f -name '*test*'
$ find ~ -type d -ctime -3
$ find ~ -type d -mtime +3
$ find ~ -type d -mtime -3

1.5. shスクリプトの基本

shスクリプトはこの行から始める。

#!/bin/sh
  または
#!/bin/bash
$ chmod 755 foo.sh

コマンドライン引数の扱い

echo "$0"
a=$1
shift
b=$1
c="$@"

あるプロセスの標準出力を値として使う

`コマンド`
  または
$(コマンド)

よく使う制御構造

if ; then ...; else ...; fi
if ; then
    ...
else
    ...
fi
for 変数 in ; do
    ...
done
while read 変数; do
    ...
done
case  in
パターン1)
    ...
    ;;
パターン2)
    ...
    ;;
*)
    ...
    ;;
esac

1.6. xargs を使う

$ cat files.txt
a.txt
b.txt
c.txt
$ cat files.txt | xargs echo
$ cat files.txt | xargs cat
$ find -type d | xargs ls
ウォームアップ演習. 与えられた引数を1行ずつ表示するシェルスクリプトを書け。 ただし、その行が foo であるときのみ、 bar と表示すること。

1.7. 実験パイプラインの設計

UNIXプログラムのお約束:

a. フィルタとして設計する場合

$ コマンド [オプション] < 入力ファイル > 出力ファイル

b. 1つのファイルを入力する場合

$ コマンド [オプション] 入力ファイル

c. 可変個のファイルを入力する場合 (理想形)

$ コマンド [オプション] 入力ファイル1 入力ファイル2 ...
ファイル名が与えられない場合は標準入力を使用する。

こうしておくと

$ find ... | xargs コマンド [オプション]
のようにできる。

また、実験パラメータの変更はコードをじかに変更するのではなく、 コマンドラインオプションとして処理すること。

$ exp1 -a1 -p2 -k0 input.txt > output_a1_p2_k0.txt
$ exp1 -a2 -p3 -k5 input.txt > output_a2_p3_k5.txt
...

ログに関する注意

ログは、たとえ人間が読む場合でも、 なるべく機械的に処理できるようにしておくこと。 (grep/awk 等でのおおまかな統計が簡単にとれる。)

長く走らせるスクリプト

#!/bin/sh
exec </dev/null
exec >log
exec 2>&1
renice +20 -p $$

echo "*** START `date` ***"
/usr/bin/time -v 実際のコマンド
echo "*** END `date` ***"

1.8. Pythonスクリプトの定石

Pythonスクリプトは慣例によりこの行から始める。

#!/usr/bin/env python

だいたい以下のようなパターンで書くと、 上に示した「お約束」に沿ったコマンドになる。

import sys
import fileinput

def doit(args):
    for line in fileinput.input(args):
        print(line)
    return

def main(argv):
    import getopt
    def usage():
        print(f'usage: {argv[0]} [-d] [-o output] [file ...]')
        return 100
    try:
        (opts, args) = getopt.getopt(argv[1:], 'do:')
    except getopt.GetoptError:
        return usage()
    debug = 0
    output = None
    for (k, v) in opts:
        if k == '-d': debug += 1
        elif k == '-o': output = v
    return doit(args)

if __name__ == '__main__': sys.exit(main(sys.argv))
$ python test.py input.txt
  または
$ ./test.py input.txt
  または
$ cat input.txt | test.py
  または
$ cat files.txt | xargs test.py
演習.
  1. 上のプログラムを書き換え、 変数 debug により doit() 内の なんらかの挙動が変わるようにせよ。
  2. 引数をとる -p オプションを追加せよ:
    $ test.py -p 4 input.txt
    

2. 実験データの管理

たいていの研究では、複数の対象を異なる条件で実験する。 このような実験条件・実験対象はよく記録し忘れるため、 実験プロセス全体をシェルスクリプトにし、さらにそれを git で管理するのがおすすめ。 (スクリプトとその履歴が実験ノートになる)

2.1. ファイル名のつけ方

基本戦略は、シェルのワイルドカード (*) で ある条件をもったファイルだけを簡単に指定できるようにすることである。

2.2. ディレクトリ構造

基本的にUNIXのファイル名は逐次探索である。 したがって、あまり1個のディレクトリに沢山のファイルを置くと遅くなる (せいぜい1000個程度)。

それからパス名が長くなりすぎると見にくいし、入力も大変。

  1. データの種類・用途ごとにまとめる (input/, output/)
  2. 日付ごとにまとめる (s201909121012/, ...)
  3. 実験条件ごとにまとめる (data_seg01_p3_q4/, ...)
  4. 1., 2., 3. の混合 (data_201909121012_seg01_p3_input, ...)

3. 大量のデータを蓄積・処理する場合のTips

  1. 可能なかぎりストリーム処理を可能にする (データ形式が重要)。 たとえば「1行に1項目」
  2. なるべく高速に parse できる形式にする。 (しかし自己流バイナリ形式はおすすめしない)
  3. 変更頻度が少ないものはディスク上に置いてもよい。
  4. 参照頻度が多くても、シーク可能なら (OSが自動的にメモリ上にキャッシュするので) ディスク上に置けるかもしれない。

高速化のためのよくある手段

4. データのSerializationについて

実験結果は、たいていの場合あとで解析可能な形式で記録しておく必要がある。 実験に時間がかかる場合・実験が複数のステージに分かれている場合などは、 その中間的な状態を記録しておく必要がある。

4.1. 考慮する要素

重要: できるだけ既存のツール・ライブラリで処理できるようにする。

4.2. テキストファイル (自分フォーマット)

おすすめしない。 もしやるとしたら、parseが簡単にできるようにすること。

新山がときどき使っているフォーマット

# コメント
+キー1 バリュー1
+キー2 バリュー2
(空行がレコード区切り)
rec = {}
for line in fp:
    line = line.strip()
    if line.startswith('#'): continue
    if line.startswith('+'):
        (k,_,v) = line.partition(' ')
        rec[k] = v
    elif not line:
        yield rec
        rec = {}

4.3. バイナリファイル (自分フォーマット)

おすすめしない。

簡単なデータだけならいいかも (たとえば 32ビット列の羅列ひたすら1G個とか)。

「SQLite は fopen() に対抗するために作られた」

4.4. よく知られている形式 (おすすめ)

CSV

JSON

XML

SQLite

SQLite + JSON

複雑な構造 × 膨大な数があるときに使う方法。

4.5. もっと高度な方法

ProtocolBuffer, HDF, MongoDB, ...

導入に手間がかかりすぎて、個人でやる実験には向かない。

5. Python から CSV/JSON/SQLite を使う

5.1. CSV

書き込み

import csv
with open('output.csv', 'w') as fp:
    writer = csv.writer(fp)
    writer.writerow(['a', 'b', 'ccc'])

読み込み

import csv
with open('input.csv') as fp:
    for row in csv.reader(fp):
        print(row)

5.2. JSON

書き込み

import json
with open('output.json', 'w') as fp:
    data = {'a':123, b:['x','y']}
    fp.write(json.dumps(data))

読み込み

import json
with open('input.json') as fp:
    for line in fp:
        data = json.loads(line)

5.3. SQLite

C から SQLite を使う場合は SQLite C/C++ Interface を参照。

書き込み

import sqlite3
db = sqlite3.connect('data.db')
cur = db.cursor()
cur.executescript('''
CREATE TABLE Student (
    Id INTEGER PRIMARY KEY,
    Name TEXT,
    Score INTEGER);
''')
for (name,id,score) in scores:
    cur.execute('INSERT INTO Student VALUES (?, ?, ?);', (id, name, score))

読み込み

import sqlite3
db = sqlite3.connect('data.db')
cur = db.cursor()
for row in cur.execute('SELECT Name,Id FROM Student;'):
    (name,id) = row

6. (おまけ) SVG形式とは

SVG (Scalable Vector Graphics) 形式とは、テキスト形式の一種で、 図形を文字によって記述する。

first.svg
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='300' height='200'>
<rect x='10' y='10' width='200' height='100' stroke='#000000' fill='#ff0000' />
<circle cx='200' cy='100' r='50' stroke='#000000' fill='#0000ff' />
</svg>

作成したファイル first.svg は、ブラウザで開くことができる。 すると、以下のような図が表示される:

上の SVG は、以下のような情報を表している:

単位はすべてピクセルである。また、色は #RRGGBB のように 赤 (R)、緑 (G)、青 (B) の各原色が 16進数 00 (0) 〜 ff (255) で表されている。 つまり、黒は #000000 であり、白は #ffffff となる。 座標のような数値は '〜' または "〜" で囲む。

演習.
  1. 上の first.svg を実際に入力し画面に描画せよ。
  2. ファイルを変更し、長方形を黄色で、円をグレーで表示するようにせよ。 色の指定 (#…) にはどのような値を指定すればよいか?
  3. 各座標を変更し、矩形と円の位置を入れ換えて表示するようにせよ。

6.1. SVG形式の基本構造

SVG の基本構造は以下のようになっている。 まず <svg></svg> で 囲まれる文字列があり、その中に描画コマンドが並んでいる。 <svg> のような文字列を タグ (tag) という。 最初の <svg> タグでは、図形全体の幅と高さをピクセル単位で指定する。 「xmlns='http://www.w3.org/2000/svg' version='1.1'」の部分は固定である。

<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='' height='高さ'>
...描画コマンド...
</svg>

SVGの描画コマンド

矩形 (rect)
<rect x='10' y='10' width='100' height='80' fill='none' stroke='#000000' stroke-width='2' />
<rect x='80' y='60' width='50' height='40' fill='#ffcc00' stroke='#0000ff' stroke-width='4' />
直線 (line)
<line x1='10' y1='10' x2='100' y2='80' stroke='#000000' stroke-width='2' />
多角形 (polygon)
<polygon points='10,90 50,10 90,90' fill='#00ff00' stroke='#000000' stroke-width='2' />
円と楕円 (circle, ellipse)
<circle cx='50' cy='50' r='40' fill='none' stroke='#000000' stroke-width='2' />
<ellipse cx='200' cy='50' rx='80' ry='40' fill='#ff00ff' stroke='#000000' stroke-width='2' />
文字 (text)
<rect x='10' y='10' width='200' height='80' fill='none' stroke='#000000' />
<text x='10' y='40' text-anchor='start'>左寄せ</text>
<text x='110' y='60' fill='red' text-anchor='middle'>中央寄せ</text>
<text x='210' y='80' fill='white' stroke='#000000' text-anchor='end'>右寄せ</text>
左寄せ 中央寄せ 右寄せ

グループ化

すべての描画コマンドにいちいち strokefill を 書くのは面倒くさいので、このような場合は <g> タグによるグループ化を使う。 <g></g> で囲んだ部分には、 同じ色・線幅が適用される。

<g fill='none' stroke='#0000ff' stroke-width='2'>
  <rect x='10' y='10' width='50' height='30' />
  <line x1='35' y1='25' x2='100' y2='50' />
  <circle cx='100' cy='50' r='30' />
</g>

6.2. 応用例


Yusuke Shinyama