目次


共通テーマ

実例でわかる sed 第 3 回

次のレベルへ: sed 流のデータ加工

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: 共通テーマ

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:共通テーマ

このシリーズの続きに乞うご期待。

力強いsed

実例でわかるsed第2回では、sedの仕組みを説明する例をいくつかお見せしました。しかしそれらの例のほとんどは、あまり実用的 なものではありませんでした。連載の最後に当たる今回は、これまでとはやり方を変え、参考になるsedの使用例を示すことにしましょう。sedの能力を示すだけでなく、実にすばらしい (しかも便利な) ことができるいくつかの例を説明します。たとえば、この記事の後半では、Intuit社の財務プログラムQuickenで使用される .QIFファイルを、見ばえ良くフォーマットされたテキスト・ファイルに変換するsedスクリプトを、私がどのように設計したかを説明します。その前に、それほど複雑ではなく、しかも実用的なsedスクリプトをいくつか見てみましょう。

テキストの変換

最初の実用的なスクリプトは、UNIXスタイルのテキストを、DOS/Windowsフォーマットに変換するものです。ご存じのとおり、DOS/Windowsベースのテキスト・ファイルでは、各行の末尾にCR (復帰) とLF (改行) が付いていますが、UNIXテキストには改行しか付いていません。UNIXテキストを、Windowsシステムへ移す必要がある場合など、このスクリプトで必要なフォーマット変換を行えます。

$ sed -e 's/$/\r/' myunix.txt > mydos.txt

このスクリプトでは、正規表現 '$' が行末と一致し、'\r' の指定によりその直前に復帰 (CR) が挿入されます。改行 (LF) の前に復帰 (CR) を挿入すれば、各行の末尾はCR/LFになります。GNU sed 3.02.80、またはそれ以後のバージョンでのみ、'\r' がCRと置換される点に注意してください。GNU sed 3.02.80をまだインストールしていない場合、実例でわかるsed第1回にある、インストールの指示を参照してください。

例示されているスクリプトやC言語のコードをダウンロードした後で、それらがDOS/Windowsフォーマットで書かれていることが分かる、といったケースは数知れません。ほとんどのプログラムは、CR/LFがついたDOS/Windowsフォーマットのテキスト・ファイルでも何の問題も起こしませんが、支障をきたす顕著な例はbashシェルです。このシェルを使用している場合、復帰 (CR) が現われた途端に処理が停止してしまいます。次のようにsedを起動すると、DOS/Windowsフォーマットのテキストを、UNIXフォーマットへ確実に変換できます。

$ sed -e 's/.$//' mydos.txt > myunix.txt

このスクリプトの仕組みは単純です。置換対象の正規表現は行の最後の文字と一致し、その文字がまさに復帰 (CR) なのです。置換後のストリングがないので、一致した文字は出力からは完全に削除されます。このスクリプトを使って、各行の最後の文字が出力から削除されてしまう場合は、すでにUNIXフォーマットになっているテキスト・ファイルを指定し ていたわけです。そのファイルにはこの処理は必要ありません!

行を逆順にする

これもまた便利なスクリプトです。これはファイル中の行の順序を逆にするもので、ほとんどのLinuxディストリビューションに含まれている "tac" コマンドと同様のものです。"tac" という名前は少々誤解を生じさせるかもしれません。なぜなら "tac" は、行の中の文字の位置 (左右) を逆にするのではなく、ファイル中の行の位置 (上下) を逆にするものだからです。次のファイル、

foo
bar
oni

を "tac" すると、次の出力が得られます。

oni
bar
foo

これと同じことを、次のsedスクリプトで行うことができます。

$ sed -e '1!G;h;$!d' forward.txt > backward.txt

FreeBSDシステムにログインされている場合、"tac" コマンドがないので、このsedスクリプトが役に立ちます。便利だというだけでなく、なぜこのスクリプトがそのような働きをするのか知っておくと役に立つでしょう。詳しく分析してみましょう。

逆順にする方法

まずこのスクリプトには、セミコロンで区切られた3個の別々のsedコマンド、'1!G'、'h'、および '$!d' が含まれています。さてここで、1番目と3番目のコマンドに使われているアドレスをきちんと理解しましょう。1番目のコマンドが '1G' ならば、'G' コマンドは1行目にだけ適用されます。しかし、'!' 記号が追加されています。この '!' 記号はそのアドレスを否定 します。つまり、'G' コマンドは、1行目を除くすべての 行に適用されるのです。'$!d' コマンドの場合も同様です。コマンドが '$d' ならば、'd' コマンドはファイルの最終行にだけ適用されます (アドレス '$' は、最終行を指定する簡単な方法です)。しかし '!' が付いた '$!d' では、'd' コマンドは最終行を除くすべての 行に適用されます。さあ、あとはコマンド自体がどのような働きをするのか理解するだけです。

行を逆順にするスクリプトを上のテキスト・ファイルに対して実行した時、最初に実行されるコマンドは 'h' です。このコマンドは、パターン・スペース (処理対象となっている現在行が保存されているバッファー) の内容をホールド・スペース (一時バッファー) へコピーするよう指示します。次に、'd' コマンドが実行され、"foo" がパターン・スペースから削除されます。その結果、この行に対しすべてのコマンドが実行された後、この行は出力されません。

次は2行目です。"bar" がパターン・スペースへ読み込まれた後、'G' コマンドが実行され、ホールド・スペースの内容 ("foo\n") がパターン・スペース ("bar\n") へ追加されます。その結果、パターン・スペースの内容は "bar\n\foo\n" となります。'h' コマンドにより、この内容がホールド・スペースへ戻されて保管されます。次に 'd' コマンドがパターン・スペースからこの行を削除するので、出力はされません。

最後の行 "oni" に対しても同じステップが繰り返されますが、前と違ってパターン・スペースの内容 (3行) は削除されないで ('d' の前にある '$!' 指定のため)、標準出力へ出力されます。

それではいよいよsedを使用して、より高度なデータ変換をしてみましょう。

sed QIFマジック

わたしはここ数週間、銀行口座の決算用にQuicken を購入しようかどうか迷っていました。Quickenは非常にすばらしい財務プログラムで、完璧に仕事をこなしてくれることは、間違いありません。しかし考えた末、小切手帳の決算をしてくれるソフトウェアなら自分で容易に作れるのではないか、という結論に達しました。なんと言っても、私は、ソフトウェア開発者なのですから!

まず、すべての取引記録が入っているテキスト・ファイルを分析しながら収支ごとの計算を行う、ちょっとした小切手帳決算プログラム (awkを使用) を開発しました。それをほんの少し調整して、Quicken同様、貸方と借方のそれぞれの項目ごとに追跡できるように改良しました。それでも、あと1つだけ追加したい機能がありました。私は最近、オンライン・バンキング・サービスを行っている銀行へ口座を移しました。ある日、その銀行のWebサイトからQuickenの .QIFフォーマットで、口座情報をダウンロードできることに気がつきました。すぐさま、この情報をテキスト・フォーマットに変換できたら、どんなにすばらしいだろうかと考えたのです。

2つのフォーマットの話

QIFのフォーマットを眺めるまえに、私が作ったcheckbook.txtのフォーマットをご紹介します。

28 Aug 2000     food    -       -       Y     Supermarket             30.94
25 Aug 2000     watr    -       103     Y     Check 103               52.86

このファイルでは、すべてのフィールドが1つまたはそれ以上のタブで区切られ、各行が1つの取引となっています。日付の次にくるフィールドは、支出の種類 (収入の場合は "-" ) を表します。3番目のフィールドは、収入の種類 (支出の場合は "-" ) を表します。その後にはそれぞれ、小切手番号 (空の場合は "-")、取引完了フィールド ("Y" または "N")、コメント、および金額 (ドル) が続きます。さて、QIFのフォーマットを見る準備が整いました。ダウンロードしたQIFファイルをテキスト・ビューアーで開くと、次のように表示されました。

!Type:Bank
D08/28/2000
T-8.15
N
PCHECKCARD SUPERMARKET
^
D08/28/2000
T-8.25
N
PCHECKCARD PUNJAB RESTAURANT
^
D08/28/2000
T-17.17
N
PCHECKCARD SUPERMARKET

ファイルをざっと眺めた後で、フォーマットを理解するのはそれほど難しくありませんでした。最初の行は無視するとして、次のようなフォーマットです。

D< 日付 >
T< 取引金額 >
N< 小切手番号 >
P< 取引種類 >
^ (フィールド・セパレーター)

処理の開始

このように処理が複雑なsedプロジェクトに取り組むからといって、しり込みしないでください。sedは、徐々にデータをもみほぐして最終的な形にしてくれます。上達するにつれ、意図したとおりの出力が得られるまで、sedスクリプトを改良していくことができます。最初の挑戦で、すべてを完成させる必要はありません。

始めに "qiftrans.sed" というファイルを作成し、データを分解してみました。

 1d
/^^/d
s/[[:cntrl:]]//g

最初の '1d' コマンドが1行目を削除し、2番目のコマンドが余計な '^' 記号を出力から除去してくれます。最後の行がファイルに存在するすべての制御文字を除去します。未知のファイル・フォーマットを取り扱っているので、いろいろな制御文字と遭遇するリスクを排除しておきたいのです。ここまでは、順調です。次はこの基本的なスクリプトにいくつかパンチの効いた処理を追加しましょう。

1d
/^^/d
s/[[:cntrl:]]//g
/^D/ {
	s/^D\(.*\)/\1\tOUTY\tINNY\t/
        s/^01/Jan/
        s/^02/Feb/
        s/^03/Mar/
        s/^04/Apr/
        s/^05/May/
        s/^06/Jun/
        s/^07/Jul/
        s/^08/Aug/
        s/^09/Sep/
        s/^10/Oct/
        s/^11/Nov/
        s/^12/Dec/
        s:^\(.*\)/\(.*\)/\(.*\):\2 \1 \3: 
}

最初に、'/^D/' アドレスを追加して、QIFの日付フィールドである 'D' が先頭文字として出現した時だけ、処理を開始するようにします。それに該当する行をパターン・スペースへ読み込み次第、中括弧 ({) の中のすべてのコマンドが、順次、実行されます。

中括弧の中の1行目は、

D08/28/2000

を次の形に変換します。

08/28/2000	OUTY	INNY

もちろん、このフォーマットはまだ完全ではありませんが、今はこのままで構いません。この先、パターン・スペースの内容はだんだんと完成されていきます。続く12行はすべて日付を3文字のフォーマットに変換するためのものであり、最後の行が日付から3つのスラッシュを除去します。この行は、最終的に次のようになります。

Aug 28 2000	OUTY	INNY

プレースホルダーの役目を果たしている、OUTYとINNYフィールドは後で置換します。ここではまだ指定することができません。なぜなら、金額がマイナスの場合はOUTYとINNYに "misc" と "-" を、金額がプラスの場合は "-" と "inco" をそれぞれに設定したいからです。まだこの時点では金額を読み込んでいないので、一時的にプレースホルダーを使う必要があります。

改良を加える

ここでは、さらに改良を加えていきます。

1d 
/^^/d
s/[[:cntrl:]]//g 
/^D/ { s/^D\(.*\)/\1\tOUTY\tINNY\t/ s/^01/Jan/ s/^02/Feb/ s/^03/Mar/ s/^04/Apr/ 
s/^05/May/ s/^06/Jun/ s/^07/Jul/ s/^08/Aug/ s/^09/Sep/ s/^10/Oct/ s/^11/Nov/ 
s/^12/Dec/ s:^\(.*\)/\(.*\)/\(.*\):\2 \1 \3: N N N 
s/\nT\(.*\)\nN\(.*\)\nP\(.*\)/NUM\2NUM\t\tY\t\t\3\tAMT\1AMT/ 
s/NUMNUM/-/ s/NUM\([0-9]*\)NUM/\1/ s/\([0-9]\),/\1/ 
}

ここで追加された7行のコマンドは多少複雑なので詳細に説明しましょう。まず連続した3つの 'N' コマンドがあります。'N' コマンドは、入力データの後続行 を読み込み、現在のパターン・スペースにその行を追加するよう指示します。3つの 'N' コマンドは、後続の3行を現在のパターン・スペース・バッファーに追加するので、この時点の行は次のようになります。

28 Aug 2000	OUTY	INNY	\nT-8.15\nN\nPCHECKCARD SUPERMARKET

パターン・スペースは不格好になってしまいました。余計な改行記号を除去して、さらにフォーマット変換をする必要があります。これを実行するには、置換コマンドを使用します。一致させようとしているパターンは次のものです。

'\nT.*\nN.*\nP.*'

このパターンは、改行記号の後に順に、'T'、長さ0または1以上の文字列、改行記号、'N'、任意の長さの文字列と改行記号、'P'、任意の長さの文字列、が続いている時に一致します。ふぅ! この正規表現は、たった今パターン・スペースに追加した3行の内容と完全に一致します。しかし、この領域の全体を置換するのではなく、領域のフォーマットを変換します。金額、小切手番号 (もしあれば)、および取引種類は、置換後のストリングにも残す必要があります。これを実行するには、「必要な部分」をバックスラッシュ付き括弧 で囲み、置換後のストリングでも参照できるようにしておきます ('\1'、'\2\'、および '\3' を使用して挿入先を指示します)。これがコマンドの最終形です。

s/\nT\(.*\)\nN\(.*\)\nP\(.*\)/NUM\2NUM\t\tY\t\t\3\tAMT\1AMT/

このコマンドは、現在の行を次のように変えます。

28 Aug 2000  OUTY  INNY  NUMNUM    Y	   CHECKCARD SUPERMARKET	 AMT-8.15AMT

だいぶ良くなってきました。しかし一見して、まだいくつか気になるところがあります。まずあの無意味な "NUMNUM" という文字列です。いったい、何の目的があるのでしょう。sedスクリプトの次の2行を調べれば、その訳がお分かりいただけると思います。その行は、"NUMNUM" を "-" に置換し、"NUM"<番号>"NUM" を <番号> に置換します。小切手番号を無意味なタグで囲っておけば、そのフィールドが空の場合、うまい具合に "-" を挿入することができます。

仕上げ

最後の行は数字のあとに続くコンマを除去します。こうして金額は "3,231.00" から "3231.00" に変換されます。これは私が使用しているフォーマットです。それでは実用的スクリプトの最終形を見てみましょう。

QIFからテキストへの変換スクリプトの最終形

 1d
/^^/d
s/[[:cntrl:]]//g
/^D/ {
	s/^D\(.*\)/\1\tOUTY\tINNY\t/
	s/^01/Jan/
	s/^02/Feb/
	s/^03/Mar/
	s/^04/Apr/
	s/^05/May/
	s/^06/Jun/
	s/^07/Jul/
	s/^08/Aug/
	s/^09/Sep/
	s/^10/Oct/
	s/^11/Nov/
	s/^12/Dec/
	s:^\(.*\)/\(.*\)/\(.*\):\2 \1 \3:
	N
	N
	N
	s/\nT\(.*\)\nN\(.*\)\nP\(.*\)/NUM\2NUM\t\tY\t\t\3\tAMT\1AMT/
	s/NUMNUM/-/
	s/NUM\([0-9]*\)NUM/\1/
	s/\([0-9]\),/\1/
	/AMT-[0-9]*.[0-9]*AMT/b fixnegs
	s/AMT\(.*\)AMT/\1/
	s/OUTY/-/
	s/INNY/inco/
	b done
:fixnegs
	s/AMT-\(.*\)AMT/\1/
	s/OUTY/misc/
	s/INNY/-/
:done
}

追加されている11行のコマンドは、出力を完全なものにするため、置換機能といくつかの分岐機能を使用しています。まず、このコマンドから見てみましょう。

/AMT-[0-9]*.[0-9]*AMT/b fixnegs

この行には、"/正規表現/bラベル" という書式の分岐コマンドが含まれています。パターン・スペースが正規表現と一致した場合、sedはfixnegsというラベルへ分岐します。このラベルは簡単に見つけられるはずです。それはコードの中で ":fixnegs" と表示されています。正規表現と一致しない場合、通常どおり次のコマンドへ処理が続行します。

コマンド自体の働きについては、理解していただけたと思うので、次は分岐を見てみましょう。分岐対象の正規表現は、'AMT' の後に '-'、任意の桁数の数字と '.'、さらに任意の桁数の数字と 'AMT' が続くストリングと一致します。ご推察のとおり、この正規表現はマイナスの金額だけを処理対象とします。先ほど、あとで容易に検索できるよう、金額を文字列 'AMT' で囲んでおきました。正規表現は '-' で始まる金額とだけ一致するので、支払い取引の場合だけ分岐が起きます。支払い取引の場合、OUTYには 'misc' を、INNYには '-' を設定し、支払金額の前のマイナス記号を除去する必要があります。コードを良く見ていただければ、このとおりの処理を行っていることが、お分かりいただけると思います。分岐が起きない場合、OUTYは '-' に、INNYは 'inco' にそれぞれ置換されます。これですべて終わりました! 出力行は完全です。

28 Aug 2000	misc	-	-       Y     CHECKCARD SUPERMARKET  -8.15

途方に暮れずに

ご理解いただけたように、問題に対して徐々にアプローチしていけば、sedを使用したデータ変換は、決して難しいものではありません。たった1つのsedコマンドですべての処理を行おうとしたり、一度にすべての作業をしようとしないことです。そうではなく、ゴールに向けて1歩ずつ作業を進め、期待したとおりの出力が得られるまでsedスクリプトの能力を高めていってください。sedにはたくさんの力が詰められています。この連載で読者がsed内部の働きに精通 され、この先さらにsedの達人となるべく成長されることをお祈りしています!


ダウンロード可能なリソース


関連トピック

  • Daniel氏のdeveloperWorks でのこれまでのsed連載記事: 共通テーマ: 実例でわかるsed第1回および第2回をお読みください。
  • Eric Pement氏の優れたsedのFAQ を調べてみてください。
  • sed 3.02のソースはftp.gnu.org にあります。
  • 品質のよい、新らしいsed 3.02.80はalpha.gnu.org にあります。
  • Eric Pement氏の便利なリストsed one-liners は、向上心に燃えているsedの教師にも一見の価値があります。
  • もし良い古い様式の本が好きなら、O'Reilly社から出版されているsed & awk, 2nd Edition がよいでしょう。
  • 多分7th edition UNIX's sed man page(1978年ごろ!) を読むのもよいでしょう。.
  • この無料のdW-exclusive tutorialのusing regular expressions で、テキスト中のパターンを検索したり変更する技術に磨きをかけてください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=230690
ArticleTitle=共通テーマ: 実例でわかる sed 第 3 回
publish-date=11012000