2006年8月30日水曜日

sedを使ったテキスト書き換え処理を覚えよう

ここでいうsedというのは「Stream EDitor」の略で、UNIXでは大昔から使われている、テキストファイルを書き換える処理を自動化できる、プログラムのことです。たぶん、最近では、SEDというと、薄型ディスプレイの表示方式のことを思い浮かべることが多いかもしれません。



現在では、perlのほうがsedよりも、ずっと高度な処理ができるので、テキスト整形処理なんかも、sedよりはperlを使う人が多いんじゃないかと思います。でも、シェルスクリプト(sh, bash)で高度なことをやるときには、sedがよく使われます。プログラムをコンパイルするときにおなじみのconfigureスクリプトでも、sedがたくさん使用されているはずです。



・・・で、このsedですが、「sedを使いこなせる人って、なんかカッコイイ!」と思わせる、魔性の魅力があったり、なかったり・・・ないのかよ?!



sedってのは、どんなものかというと、

「テキストを整形するルール」をsedのコマンドで書いておき、そのコマンドと、入力データとなるテキストファイルをsedに与えると、テキスト整形処理をsedが自動で行ってくれる

ってものです。簡単な例を挙げます。



sample.txtというテキストファイルがあったとして、そのなかに「ABCDEFG」と書いてあったとします。

% sed 's/ABC/abc/' sample.txt > output.txt

というコマンドを実行すると、output.txtというファイルができて、その中身は、「abcdDEFG」となります。



s/ABC/abc/」というのがsedのコマンドです。「s」は文字列置換処理のコマンドであり、この場合、ABCという文字列をabcに置換しなさい、という意味になります。



ファイル名を指定しない場合は、標準入力から読み込んで、標準出力へ書き出します。



こんな感じになります。

% sed 's/ABC/abc/'
ABCDEFG  ← と文字列を入力すると
abcDEFG   ← sedが、このように出力する
(最後に[ctrl]+[d]を押して、終了してください)



以下では、実用的な課題を示して、sedの活用事例を紹介します。



■ 課題:  RSSのリンクにある「?ref=rss」を削除したい!



最近、RSSがあちこちのWebサイトで提供されるようになりました。



さて個人的に、ちょっと困ったことがあります。RSSの中に書かれているリンクが、「http://ほげほげ~/なんとか/かんとか?ref=rss」となっていて、なんだかお尻に「?ref=rss」というのがついてます。



例をあげると、Impress Watchのサイトのひとつ、

PC WatchのRSS
http://pc.watch.impress.co.jp/sublink/pc.rdf

にも、“?ref=rss”がくっついています。



たぶん、Webサイトの運営者側にとっては、RSSリーダからリンクをたどってきた、という情報を知りたいという希望があるかもしれないし、または、RSSからたどってきたときには、表示内容を変化させる、ということもできるんだと思うわけですが、現状、そのようなことをやっているのを見たこともきいたこともありません。



別についててもいいじゃん、と思うかもしれませんが、Webブラウザでは、?ref=rssがついているリンクと、ついていないリンク、この2つを別物として扱っています。そのため、1度、RSSのほうから見たウェブページでも、ウェブページ内にならんでいる記事の見出し一覧のリンク(?ref=rssはついていない)は、「既読状態」にならないので、

すでに見たことのあるページなのに、見たことになっていないので、うっとうしい

と思ってしまうのです。まちがえて、1度見たものを、また見てしまうとか・・・ あ~時間がもったいない。



というわけで、RSSのリンク中の「?ref=rss」を削除する処理を、sedでやってみることにします。



● 方針



  • wgetコマンドでRSSをダウンロードします


  • ダウンロードしたRSSを、sedで書き換えます(?ref=rssを削除)


  • 書き換え後のファイルを、自前の(ローカルな)Webサイトにおいておき、Webブラウザ(RSSリーダ)では、その書き換え後のRSSを参照するようにします


  • cronなどで、1,2の処理を自動実行するようにすると、より実用的になりますね。




今回は、sedの紹介がメインなので、Webサイトに置くとか、cronとかの話は、割愛します。



■ wgetでRSSをダウンロード



本筋からずれるので、簡単に触れるだけにしておきますが、「wget URL」と実行すれば、URLのページをダウンロードして、ファイルに保存できます。



というわけで、こうなります。



% wget http://pc.watch.impress.co.jp/sublink/pc.rdf
--XX:XX:XX--  http://pc.watch.impress.co.jp/sublink/pc.rdf
           => `pc.rdf'
Resolving pc.watch.impress.co.jp... 210.173.173.73
Connecting to pc.watch.impress.co.jp|210.173.173.73|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 7,113 (6.9K) [text/plain]



100%[====================================>] 7,113         --.--K/s



XX:XX:XX (200.41 KB/s) - `pc.rdf' saved [7113/7113]



pc.rdfというファイル名で保存されました。



ちなみに、すでにカレントディレクトリに「pc.rdf」というファイルがあるときは、pc.rdf.1のように、別の名前で保存されます。



■ sedで書き換える



最初に例を示したように、sedで文字列を置換するときは、「sコマンド」を使います。



sコマンドの基本的な書式は、こうなります。

s/置換対象の文字列/置換後の文字列/フラグ

フラグ」は、置換処理のときの挙動の詳細を指定するものです。sedは、行単位で処理をしていくのですが、通常は、1行中で、最初にマッチした文字列のみを置換します。フラグとして「g」を指定すると、マッチするものすべてが置換されます。



「/」は、デリミタ(delimiter、区切り文字)というものです。「/」を使いましたが、実は、「:」とか「-」とか、自由に選ぶことができます。



% sed 's-ABC-abc-g'
ABCDEFG ABCDEFG ABCDEFG
abcDEFG abcDEFG abcDEFG



「?ref=rss」を削除するには、置換対象を「?ref=rss」にして、置換後は空っぽの文字列にします。



よって、こうなります。

% sed 's/?ref=rss//g' pc.rdf > pc-new.rdf

これで、pc-new.rdfというファイルができます。



■ うまく処理できているかdiffで確認



2つのテキストファイルを比較する「diff」コマンドを使って、書き換えがうまくいっているか、確認してみます。



なおRSSファイルは、UTF-8という文字コードで書かれているので、操作環境によっては正常に表示されません。「nkf」コマンドで文字コードを変換するとよいでしょう。「nkf -e」ならEUC日本語、「nkf -s」なら日本語シフトJISに変換されます。



% diff -u pc.rdf pc-new.rdf | nkf -e



(一部を抜粋)



-<item rdf:about="http://pc.watch.impress.co.jp/docs/2006/0829/nec3.htm?ref=rss">
+<item rdf:about="http://pc.watch.impress.co.jp/docs/2006/0829/nec3.htm">
<title>NEC、ワンセグ搭載のエンタメノート「LaVie A」</title>
- <link>http://pc.watch.impress.co.jp/docs/2006/0829/nec3.htm?ref=rss</link>
+ <link>http://pc.watch.impress.co.jp/docs/2006/0829/nec3.htm</link>
<dc:date>2006-08-29T11:15:00+09:00</dc:date>
</item>



行頭に「-」マークがついているほうが、pc.rdfの行(置換前)、そして、「+」マークがついているほうが、pc-new.rdfの行(置換後)です。「+」も「-」もどちらもついていない行は、2つのファイル間で同一な行です。通常、diffでは、違いのある範囲の、前後数行だけが表示されます。同一の行は、省略され、表示されません。



diffの結果を見てみると、どうやらうまく処理できたようです。?ref=rssが削除されてますね。





■ ちょっと高度なsedの使い方



sコマンドの使い方、なんとなく、理解できてきましたか?さらに、sedの正規表現とか覚えると、もっともっとsedを活用できるようになります。



さてさて、ここでは、より高度なsedの使い方を紹介します。perlを使わなくても、sedで、これくらいできちゃう、っていう、少しだけ技巧的な使用例です。ちょっとパズルみたいで、難しいかもしれないんですが・・・



● 課題2 このRSSの書き換えはどうすればいいのか?



同じImpress Watchのサイトなんですが、「INTERNET Watch」の場合、なぜか、RSSの作り方が違います。どうやら、RSSで広告配信を行う会社経由で、RSS配信されるようになっています。



INTERNET WatchのRSS
http://internet.watch.impress.co.jp/cda/rss/internet.rdf


このRSSの中身は、こんな風になっています。



<item rdf:about="http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html">
<title>サイボウズ製品に脆弱性、修正プログラムが公開</title>
<link>http://www.pheedo.jp/click.phdo?i=4fba32a5b7d9db0e6a5be8003a294eeb</link>
<pheedo:origLink>http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html</pheedo:origLink>
<dc:date>2006-08-28T14:21:47+09:00</dc:date>
<description>&lt;br style=&quot;clear: both;&quot;/&gt;  &lt;img alt=&quot;&quot; style=&quot;border: 0;&quot; border=&quot;0&quot;src=&quot;http://www.pheedo.jp/img.phdo?i=4fba32a5b7d9db0e6a5be8003a294eeb&quot;/&gt;
</description>
</item>



実際にRSSリーダでみると、リンクのURLは、「http://www.pheedo.jp/click.phdo?i=4fba32a5b7d9db0e6a5be8003a294eeb」というものになっていて、こちらの場合は、「?ref=rss」を削除すればいい、っていう単純なものではないようです。



さて~、このURLでアクセスすると、Webサーバからは、



Location: http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html



というように、Locationヘッダによって、別のURLへリダイレクトが行われます。



おや?よく見ると、これって、さっきの、<item rdf:about="http://~~~ の部分に書かれているURLですよね?



ということは、<link>の部分にあるURLを、<item rdf:about=~~~>の部分にあるURLで置き換えてやればいいじゃないか!、ということになります。



これを、sedでやってみましょう、ということです。


■ パターンスペースと、ホールドスペース



さきほどsedは、行単位で処理をする、と少しだけ触れたのですが、そのsedが読み込んだ行は、一度、「パターンスペース」というところに保存されます。実は、置換を行う「s」コマンドは、パターンスペースに対して、置換処理を行うコマンドです。



パターンスペースとは別に、ホールドスペースという、データ置き場もあります。この2つのデータ置き場の間で、データを入れ替るコマンドがあり、2つの置き場をうまく利用することで、今回の課題を解決できるようになります。



■ sedのコマンドファイル



sedは、いくつものコマンドを列挙して指定することができます。たくさんコマンドを指定するときは、そのコマンドを記述したファイルを用意しておくと便利です。



たとえば、abc.sedというファイルを作成しておき、内容は

s/ABC/abc/g
s/XYZ/xyz/g

とでもしておきましょう。



sedコマンドを記述したファイルは、sedには「-f ファイル」オプションで指定します。



こんな感じになります。

% sed -f abc.sed
ABCDEF XXXYZZZ ABCD XXYYZZ  (入力した文字列)
abcDEF XXxyzZZ abcD XXYYZZ  (sedの処理結果)


■ sedのアドレス指定



これまで示したsコマンドの例は、入力されたすべての行に対して、sedは処理を行います。



「アドレス指定」というのを追加すると、特定の行だけ、処理するようにできます。



例: 1行目から、3行目までだけを、処理する

% sed '1,3s/ABC/abc/'
ABCDEF  (入力の1行目)
abcDEF
ABCDEF  (入力の2行目)
abcDEF
ABCDEF  (入力の3行目)
abcDEF
ABCDEF  (入力の4行目)
ABCDEF
ABCDEF  (入力の5行目)
ABCDEF

「1,3」というのが、「1行目から3行目まで」というアドレス指定です。4行目、5行目は、確かに置換されていません。



行番号ではなく、パターンマッチする文字列で指定することもできます。



例:行頭がXの行だけ、処理する

% sed '/^X/s/ABC/abc/'
XXX ABCDEF  (入力)
XXX abcDEF
XYZ ABCDEF  (入力)
XYZ abcDEF
YYY ABCDEF  (入力)
YYY ABCDEF

「^X」というのは、「行頭がXである」という意味の正規表現です。「/^X/」が、アドレス指定になります。たしかに「YYY ABCDEF」の行頭はXではないので、置換処理が行われていません。



■ 課題2の解答例



もういきなり答を出してしまいますが、こうなります。



sedのコマンドファイルは、こうなりました。



「internet.sed.txt」をダウンロード



% cat internet.sed
/<item rdf:about="http:\/\/internet.watch.impress.co.jp/ {
        h
        s/<item rdf:about="/<link>/
        s/">/<\/link>/
        s/?ref=rss//
        x
}
/<link>http:\/\/www.pheedo.jp\// {
        g
}



実行例



% wget http://internet.watch.impress.co.jp/cda/rss/internet.rdf
(index.htmlというファイル名で保存されました)



% sed -f internet.sed index.html >index-new.html



diffで比較してみます。
% diff -u index.html index-new.html



(一部抜粋)



<item rdf:about="http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html">
<title>サイボウズ製品に脆弱性、修正プログラムが公開</title>
-<link>http://www.pheedo.jp/click.phdo?i=4fba32a5b7d9db0e6a5be8003a294eeb</link>
+<link>http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html</link>

<pheedo:origLink>http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html</pheedo:origLink>
<dc:date>2006-08-28T14:21:47+09:00</dc:date>



うまく置換されたようです。


● 解説



sedのコマンドファイルを見ていきましょう。



/<item rdf:about="http:\/\/internet.watch.impress.co.jp/ {
        h
        s/<item rdf:about="/<link>/
        s/">/<\/link>/
        s/?ref=rss//
        x
}
/<link>http:\/\/www.pheedo.jp\// {
        g
}



まずは1行目。



/<item rdf:about="http:\/\/internet.watch.impress.co.jp/ {



これは、先ほどあった、sedのアドレス指定を、文字列のパターンマッチで行っている記述です。
RSSファイル中に、似たような文字列が別の箇所にも登場していたため、やや長めのパターンを指定しています。



「http:\/\/」のところが、ちょっと奇妙に見えるかもしれません。これは、もともと「http://」だったのですが、アドレス指定の「/~~~/」の中に、/を書くと、アドレス指定を閉じる「/」記号になってしまうため、「\」マークをつけて、エスケープ処理を行っています。つまり「/」をここに書きたいので、これは閉じるの「/」じゃないですよ、と指示しているのです。



「{」ですが、sedでは、{ ~ }で囲まれた部分が、一塊のものとして、連続して実行されていきます。



2行目の「h」は、hコマンド。ホールドスペースの内容を、パターンスペースの内容で置き換えるコマンドです。コピー&ペーストみたいな処理だと思ってください。あとで詳しく解説します。



3行目は、「s」なので、おなじみの置換コマンドですね。「<item rdf:about="」という文字列を、「<link>」へ置換します。



4行目も置換。「">」という文字列を「</link>」へ置換しています。ただ、ここでも置換後の文字列に「/」という文字が含まれてしまっているので、代わりに「\/」と書いて、エスケープ処理を行っています。



5行目も置換。「?ref=rss」という文字列を削除しています。



6行目の「x」は、xコマンド。パターンスペースの内容と、ホールドスペースの内容を、交換します。最初にhコマンドで、置換前の文字列をホールドスペースにコピーしてあったので、結局、パターンスペースには最初の文字列が入り、ホールドスペースに、置換後の文字列が入ることになります。



7行目の「}」は、ここで一連の処理がおしまい、の意味。



8行目。/<link>http:\/\/www.pheedo.jp\// {
これは、1行目と同様で、アドレス指定です。
もしも「<link>http://www.pheedo.jp/」という文字列があったら・・・の意味です。



8行目のgコマンドは、パターンスペースの内容を、ホールドスペースの内容で置き換えます。「h」コマンドの逆の働きですね。


以上の処理の流れを、わかりやすく見ていきましょう。



(備考: なぜかブラウザによっては、右端のほうが切れてしまい、表示されないことがあるようです。申し訳ありませんが、その点、ご容赦ください)



今、次のような行が、入力されてきました。



<item rdf:about="http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html">



2行目のアドレス指定の条件にマッチしますので、{~}内の処理が開始されます。
hコマンドにより、ホールドスペースへテキストがコピーされ、以下のようになります。



パターンスペースの内容
<item rdf:about="http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html">



ホールドスペースの内容
<item rdf:about="http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html">



sコマンドで、パターンスペースの内容が置換されます。



パターンスペースの内容
<link>http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html">



ホールドスペースの内容
<item rdf:about="http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html">



次のsコマンドで置換され、こうなります。



パターンスペースの内容
<link>http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html</link>



ホールドスペースの内容
<item rdf:about="http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html">



次のsコマンド、「s/?ref=rss//」ですが、ここでは、「?ref=rss」がないので、何も変化しません。ちなみに、



Enterprise WatchのRSS
http://enterprise.watch.impress.co.jp/cda/rss/enterprise.rdf



には、「?ref=rss」がついていました。こちらをsedで処理する場合には、「?ref=rss」が、この段階で削除されます。



さて、sedコマンドの動きの解説のつづきに戻りましょう。



xコマンドがあるので、2つの内容が交換されます。



パターンスペースの内容
<item rdf:about="http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html">



ホールドスペースの内容
<link>http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html</link>



これまで説明していませんでしたが、sedでは、デフォルトの挙動として、一連のsedコマンドの処理が完了すると、パターンスペースの内容が、標準出力へ書き出されます。



よって、標準出力へは



<item rdf:about="http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html">



が出力されます。注意!ホールドスペースの内容は、まだそのまま残っていますよ!



入力ファイルがどんどんすすんでいき、



<link>http://www.pheedo.jp/click.phdo?i=4fba32a5b7d9db0e6a5be8003a294eeb</link>



という行を、sedが読み込みました。こうなります。



パターンスペースの内容
<link>http://www.pheedo.jp/click.phdo?i=4fba32a5b7d9db0e6a5be8003a294eeb</link>



ホールドスペースの内容
<link>http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html</link>



パターンスペースの内容が、/<link>http:\/\/www.pheedo.jp\// というアドレス指定にマッチします。よって、gコマンドが実行されます。



パターンスペースの内容
<link>http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html</link>



ホールドスペースの内容
<link>http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html</link>



そして、最終的に、標準出力へは、パターンスペースの内容である、



<link>http://internet.watch.impress.co.jp/cda/news/2006/08/28/13096.html</link>



が出力されます。



どうです?理解できましたか?


なお、sedに「-n」というコマンドラインオプションを指定すると(たとえば、sed -n 's/ABC/abc/'のように)、“最後にパターンスペースの内容を出力する”というデフォルトの処理が行われなくなります。これに関連するsedコマンドとして、sedコマンドには「p」というコマンドがあります。これは、パターンスペースの内容を、標準出力へ書き出すコマンドです。



また、sコマンドのフラグにも「p」があります。sed -n 's/ABC/abc/p' とすれば、置換した行だけが出力されるようになります。



「-n」オプションと「p」コマンドを覚えておけば、grepのように特定の行だけ抽出して加工する、といった処理が、sedだけで行えますね。

【ちょっとしたネタ】



「grep」ってコマンド、英語にはそんな単語はないのですけれども、どういう意味か知ってます?昔はそういったコネタが雑誌記事で紹介されていたりしたものですが、最近はどうなんでしょうか。



grepは、global regular expression printの略だと、私は昔教わりました。



かつて、まだコンピュータの性能がずっと低かったころ、「ed」という名前のテキストエディタが使われていました。MS-DOSのedlinみたいなもの・・・って知ってる人少ないか・・・ラインエディタと呼ばれる種類の、原始時代のような太古のテキスト編集ツールです。有名なviというエディタ~viも原始時代ほどではないですが室町時代程度に古い~このviにもedモードというのが残っています。そのedで、文字列検索を行う機能だけを抜き出して、1個のコマンドとしたのが、grepだそうです。



その意味は、global=テキストファイル全体に対して、regular expression=正規表現で、検索処理を行い、その結果をprint=表示しなさい、ということだそうです。



ついでなので、もう1つオマケのネタ。sedは、stream editorの略だと最初に言いましたが、editorとは、すなわちその太古のエディタ「ed」のことです。



edは、ファイルを読み込んで、いろいろな編集コマンドを実行して、テキストを書き換えて、ファイルに保存するツールです(・・・っていっても、実は使ったことないのですが)。



一方、sedは、標準入力からテキストファイルを1行ずつ読み込んで、指定された編集コマンドを実行したのち、標準出力へ出力します。sedは、edでのテキスト編集処理を、フィルタとして実行きるようにしたものです。ところでフィルタとは・・・?標準入力から読み込んで標準出力へ書き出す動作を行うプログラムを、フィルタと呼びます。Unixにはたくさんのフィルタがあり、これらを組み合わせることで、自動でテキスト処理を行うシェルスクリプトを書いたりできます。なにしろ、もともと、Unixは、テキスト処理システムとして開発されたのですから・・・とあるゲームを実行するためのOSとして開発されたという説もありますが・・・)。sedのsであるstreamとは、「流れ」という意味で、フィルタをデータが絶えず流れている様子をさしているのでしょう。



というわけで、実は、sedで置換を行うsといったハナモゲラなコマンドは、edで使われていたコマンドだったりするわけです。sedのpコマンドのpや、sコマンドのフラグのpは、grepのpと同じ由来だったんですねぇ。



■ おまけ: シェルスクリプトにしてみよう



「internet.sh.txt」をダウンロード



#! /bin/sh



DIR="/tmp"



/usr/local/bin/wget -q -O - \
    http://internet.watch.impress.co.jp/cda/rss/internet.rdf \
    | /usr/bin/sed '
/<item rdf:about="http:\/\/internet.watch.impress.co.jp/ {
    h
    s/<item rdf:about="/<link>/
    s/">/<\/link>/
    s/?ref=rss//
    x
}
/<link>http:\/\/www.pheedo.jp\/click.phdo?i=/g' \
    > ${DIR}/internet.rdf


このシェルスクリプトを実行すると、RSSをダウンロードして、書き換え処理を行って、「/tmp/internet.rdf」に保存するまでが、全部行われます。



sedコマンドは、別ファイルにせずに、そのまま書き込んでしまいました。ファイルが分かれていると、見晴らしが悪い気がしたからです。



'~'で囲んで書くのが、ちょっとしたポイントですね。'がはじまったら、次に'がくるまで、囲まれた範囲すべての文字列が、そのまま受理されます。ということは・・・そう、'と'で囲まれた範囲内には、'を書くことができません。今回は、それでもかまいませんね。



行末の「\」は、まだコマンドがつづいていますよ、の意味の、シェルスクリプトでのお約束の書き方です。'と'で囲んだ部分では、改行があっても\をつける必要はありません(つけてはいけません。\がそのまま受け取られてしまいますので)。







■ 参考文献



今回、この記事を書くのに、「man wget」でwgetの使い方を調べたり、「man sed」でsedの使い方を調べました。



sedだけの話ではないですが、何かで上達する一番の方法は



  1. まずは、いいお手本を見つけてきて、理解し、暗記する


  2. そのお手本を、自分なりに少し書き換えて、実行できるようになる


  3. マニュアル(manやinfo)を自分で調べて、自分で新しい使い方を発見する


ということじゃないかなと思います。最後の「自分で調べて、自分で解決できるようになる力」・・・これを身につけると、もうあとは、どんなものでも、すぐに上達できるようになりますよね。



■ sedへの招待 全3話 +1



 



0 件のコメント:

コメントを投稿