

この講座では,初歩的な話は他の入門書に譲って,私(岸 和孝)が実務で出会った様々なテキスト加工処理での事例についてPerlスクリプティングを説明します。
Perlを習い始めた頃は,スクリプトで処理対象のファイルを指定する場合に,キーボードからの入力,例えば“$file = <>”のように表わしていました。この方法は,カレントディレクトリにあるファイルであれば,そのファイル名だけをタイプするだけですが,別のディレクトリにあるファイルとなると,長いフルパス名をタイプすることになり,なかなか面倒です。
私はMacJPerlを使うことが多いので,ある時,MacOSの拡張機能に“&MacPerl'Choose()”という組み込みのサブルーチンがあることに気が付きました。それはダイアログを使った入出力ファイル選択ができるものです。それを使うと,いかにもMac風のGUIになりますので,今度は馬鹿の一つ覚えで,そればかり使ってスクリプトを書いていました。
ところが,そうしたMacOSの拡張機能を使ったスクリプトは,当然ながらWin95では使えません。幾つかのPerlスクリプトをフリーウェアで公開するために,その移植を簡単にしようと,Win95用の入出力ファイル選択サブルーチンを作ったり,OSの違いを区別するスクリプト専用インストーラーを作ったりしました。これはスクリプトを保守する上でやはり面倒なことでした。
そのうちに,Win95のJPerl for Win32でPerlスクリプトを書くことが多くなってきました。JPerlではダイアログを使った入出力ファイル選択はできませんので,相変わらずキーボードからの入力に頼っていました。
ある時,コマンドラインの引き数“@ARGV”を使ってみました。つまり,コマンドラインで“PERL スクリプト名 引き数…”と指示し,スクリプトでその引き数の値を参照する方法です。ここで,引き数は複数個指定できます。例えば,“PERL PROCESS.PL FILE1.TXT FILE2.TXT”と指示した場合は,“PROCESS.PL”というスクリプトには,アレイ変数の$ARGV[0]に“FILE1.TXT”,同$ARGV[1]に“FILE2.TXT”が引き渡されます。[図1を参照]
|
▲図1 @ARGVによる引き数の受け取り
コマンドラインでファイル名をタイプすることと,Perlスクリプトが動いてキーボードからの入力でファイル名をタイプすることは,同じ手間ですから,ここまではどうという話ではありません。ここからが肝心なところです。Win95では,PerlインタプリタはMS-DOSの制御下で動きます。ですから,バッチファイルでも起動できるわけです。しかも,Win95ではバッチファイルに対してファイルのアイコンをドラッグアンドドロップすると,それがバッチファイルへの引き数となります。
ここで,“PERL PROCESS.PL %1 %2 %3 %4 %5”という内容のバッチファイルを作り,それを“PROCESS.BAT”と名付けます。そして,“PROCESS.BAT”のアイコンへ“FILE1.TXT”と“FILE2.TXT”のアイコンをドラッグアンドドロップすると,バッチファイルの内容は“PERL PROCESS.PL FILE1.TXT FILE2.TXT”へ変換されます。これで引き数をタイプする手間から解放されたわけです。[図2を参照]
|
▲図2 バッチファイルによる起動と引き数の受け取り
今度は,MacJPerlでのDroplet形式のPerlスクリプト(つまり,アプリケーション)へファイルのアイコンをドラッグアンドドロップした場合を調べてみました。すると,全く同じように引き数が渡されます。つまり,コマンドラインの引き数“@ARGV”を使ったPerlスクリプトはWin95とMacOSで同じ動きをするわけです。
もう一つ問題が残っていました。それはディレクトリをドラッグアンドドロップした場合です。この解決方法については次のスクリプトを参照して下さい。
コマンドラインの引き数を参照するスクリプトの骨組み
[1] if ($#ARGV >= 0) {
[2] foreach $df (@ARGV) {
[3] if (-d $df) {
[4] opendir(DIR, $df) ;
[5] @files = readdir(DIR) ;
[6] closedir(DIR) ;
[7] if ($df =~m!^.+:.+$!) { $deli = ':' ; }
[8] else { $deli = "\\" ; }
[9] foreach $f (@files) {
[10] if ($f eq '.' || $f eq '..' || $f eq "Icon\n") { next ; }
[11] if (-d $f) { next ; }
[12] “$df.$deli.$f”の指すファイルに対する処理
[13] }
[14] }
[15] else { “$df”の指すファイルに対する処理 }
[16] }
[17] }
[18] else { 引き数がない時の処理 }
注)スクリプトの左側の番号は説明のためのものです。
最初に,コマンドラインの引き数があるかどうかを調べます[1]。
引き数がある場合[2-17],その引き数がディレクトリかどうかを調べます[3]。
ディレクトリの場合[4-13]は,ファイルリストを取り出し[4-6],そのファイルの一つ一つについて処理を行います[9-13]。ファイルの処理に先立って,引き数のディレクトリ名の形式がMacOSかWin95を調べ,パスの区切り記号を設定します[7-8]。というのも,バッチファイルへファイルやディレクトリをドラッグアンドドロップすると,そのフルパス名が引き渡されますので,それを解析すればOSの種類を決定できるからです。また,ファイルリストのファイル名はパス付きではありませんので,ファイルへのフルパスを合成する[12]ためにも必要なことです。
ファイルの場合[15]は,そのファイルについて処理を行います。この場合,引き数はフルパスのファイル名ですので,そのまま参照できます。
(1997年11月記)

Perlは,Windows95やMacOSなどのプラットフォームで利用できます。私(岸 和孝)は,主にコーパス(言語資料)のテキスト処理で,MacPerlをツールとしてよく使っています。そこで扱うテキストデータはやはりWindows95で作成されたものが多く,持ち込まれる都度,MacOSのテキストデータへ変換しなければなりません。
テキストデータの内容確認や16進数での編集には,HexEdit(ver.1.0.3)を使っています。又,テキストデータの行末符号や符号系の変換には,変漢(ver.2.4.0)を使っています。この二つは共にフリーウェアで,実に重宝しています。
私は,そうした編集の仕事の他に,日本印刷技術協会(JAGAT)で定期的にPerlのセミナーを担当しています。ある時,教材として「Windows95とMacOSとの間でテキストデータを相互に変換するスクリプト」を作ろうと思い立ちました。それはちょうど変漢のように行末符号を変換するツールです。
ここで,処理対象となる行末符号については,テキスト処理の基本ですから周知のこととは思いますが,一応復習しておきましょう。
テキストデータの行末符号は,OSによって異なります。Windows95では復帰符号と改行符号(CR-LF)が並んでいます。MacOSでは復帰符号(CR)だけです。又,UNIXでは改行符号(LF)だけです。
さて,Perlではそうした符号を次のように表現します。
| 略号 | 記号表記 | 16進数表記 |
|---|---|---|
| LF | \n | \x0a |
| CR | \r | \x0d |
最初に,Windows95のテキストデータからMacOSのテキストデータへ変換するスクリプトを次のように書いてMacOSのMacPerlで実行してみました。
[1] open(WIN, "<WinData.txt") ;
[2] open(MAC, ">MacData.txt") ;
[3] while ($text = <WIN>) {
[4] if ($text =~m!^\x0a!) { $t = substr($text, 1) ; }
[5] print MAC ("$text") ;
[6] }
[7] close(WIN) ;
[8] close(MAC) ;
[9] exit ;
注)スクリプトの左側の番号は説明のためのものです。
先ず,入力と出力のファイルをそれぞれ開きます[1-2]。Windows95のテキストデータを変数$textに読み込み[3],変換・出力[4-5]を行ないます。これをファイルの終了まで繰り返します[3-6]。MacPerlではCRまで読み込みますので,LFは次のデータの先頭に読み込まれることになります。したがって,データの先頭にLFがあれば,それを取り除きます[4]。その場合でも,CRは残りますので,そのまま出力できます[5]。ファイルを閉じて[7-8],処理を終えます[9]。
この実行結果は期待通りでした。そこで,このスクリプトをWindows95のJPerlで実行してみましたが,変換が全く行なわれませんでした。ちょっと考えてみれば,それは当然のことでした。JPerlでは,CR-LFまで読み込んでしまうので,うまくいかないわけです。
そこで次のように一部を書き換えました。
[1] open(WIN, "<WinData.txt") ;
[2] open(MAC, ">MacData.txt") ;
[3] while ($text = <WIN>) {
[4] chop($text) ;
[5] print MAC ("$text") ;
[6] }
[7] close(WIN) ;
[8] close(MAC) ;
[9] exit ;
注)スクリプトの左側の番号は説明のためのものです。
この改良したスクリプトでは,読み込んだデータの最後のLFを取り除くようにしました[4]。しかし意外なことに,実行結果は期待外れでした。JPerlはchop($text)でCR-LFを一つの行末符号として取り除いてしまうことが分かりました。つまり,MacPerlとJPerlのスクリプトには互換性がないのです。
そこで,次のような,さらに改良したスクリプトに書き換えました。
[1] open(WIN, "<WinData.txt") ;
[2] open(MAC, ">MacData.txt") ;
[3] if ($os eq 'MacOS') { $/ ="\x0a" ; }
[4] while ($text = <WIN>) {
[5] chop($text) ;
[6] if ($os eq 'Windows95') { $text .= "\x0d" ; }
[7] print MAC ("$text") ;
[8] }
[9] close(WIN) ;
[10] close(MAC) ;
[11] exit ;
注)スクリプトの左側の番号は説明のためのものです。
変数$osは,OSの種類を'MacOS'又は'Windows95'で表わしているものとします。この設定はドラッグ・アンド・ドロップによる起動で得られるファイルのパスの中の区切り子の種類を解析すれば容易に得られます[<note> No.2 P.25を参照]。MacPerlの場合でも,CR-LFまで読み込むように,特殊変数$/で読み込み時の行末符号の種類の設定を行ないます[3]。そしてchop($text)の後で,MacPerlの場合はCRが残っていますが,JPerlの場合はCRを行末に付加しておきます[6]。これで,スクリプトの互換性が実現したわけです。
では,MacOSのテキストデータからWindows95のテキストデータへ変換する逆の場合はどうなるのでしょうか。
次がその変換を行なうスクリプトですが,かなり複雑になりました。
[1] open(MAC, "<MacData.txt") ;
[2] $gdataBuff = '' ;
[3] open(WIN, ">WinData.txt") ;
[4] while (1) {
[5] if (&getData(*text) && $text eq '') { last ; }
[6] chop($text) ;
[7] if ($os eq 'MacOS') { print WIN ("$text\x0d\x0a") ; }
[8] if ($os eq 'Windows95'){ print WIN ("$text\n") ; }
[9] }
[10] close(MAC) ;
[11] close(WIN) ;
[12] exit ;
[13] sub getData {
[14] local(*data) = @_[0] ;
[15] local($c, $eof) ;
[16] $data = '' ;
[17] while(1) {
[18] if ($gdataBuff eq '') {
[19] if ((sysread(MAC,$gdataBuff,256)) == 0) { return 1 ; }
[20] }
[21] $c = substr($gdataBuff, 0, 1) ;
[22] $gdataBuff = substr($gdataBuff, 1) ;
[23] $data .= $c ;
[24] if ($c eq "\x0d") { return 0 ; }
[25] }
[26] }
注)スクリプトの左側の番号は説明のためのものです。
JPerlは,行末符号がCRだけのデータを読み込めません。そのために,サブルーチン[13-26]においてsysread(MAC,$gdataBuff,256)で直接入力し,バッファリングするようにしました[19]。
また,JPerlは出力における行末符号の扱いが次のようにMacPerlと異なります。
| 出力符号 | \r | \n |
|---|---|---|
| JPerl | \x0d | \x0d\x0a |
| MacPerl | \x0a | \x0d |
\r(復帰符号,CR)の値は,Perlでは\x0dと定義されていますが,MacPerlでは\x0aになっています。また,\n(改行符号,LF)の値は,Perlでは\x0aと定義されていますが,JPerlでは\x0d\x0aに,MacPerlでは\x0dになっています。
一般にPerlスクリプトでは出力データ文字列の終端に\nを置きます。これはUNIXLの行末符号がLFだからです。これを基準に考えれば,\nはMacPerlでは\x0d(CR)に,JPerlでは\x0d\x0a(CR-LF)になるわけです。
このスクリプトは,MacOSとWindows95との間で互換です。
(1998年1月記)

Perlは,ソーステキストデータとESISデータの加工だけでなく,文書実現値(SGMLタグの付いたテキストデータ)を直接に,しかも簡単に処理できそうです。しかし,私(岸 和孝)は,今までいろいろなケースでPerlを使ってきましたが,文書実現値の処理だけは失敗続きでした。その最大の原因は,Perlの正規表現の働きをしっかりマスターしていなかったせいです。今回は,Perlによる文書実現値の処理での注意点について説明します。
例えば,文書実現値の中に“<H1>これはPerlスクリプトです。</H1>”のように,開始タグ“<H1>”と終了タグ“</H1>”に挟まれたデータ“これはPerlスクリプトです。”があるとします。正規表現式“^<H1>(.+)</H1>$”を使ったパターン照合によって,データ“これはPerlスクリプトです。”の部分だけを特殊変数$1で抽出できます。
次のスクリプトで確かめてみましょう。
$text = '<H1>これはPerlスクリプトです。</H1>' ;
if ($text =~m/^<H1>(.+)<\/H1>$/) { print "s1='$1'\n" ; }
exit ;
結果は,次のように表示されるはずです。
s1='これはPerlスクリプトです。'
では次に,“<H1>これは<EM>Perl</EM>スクリプトです。</H1>”のように,文書実現値の中に入れ子になったタグがある場合はどうなるでしょうか。先ず,外側のタグだけを外して,“これは<EM>Perl</EM>スクリプトです。”の部分だけを抽出できるでしょうか。次のスクリプトで試してみましょう。
$text = '<H1>これは<EM>Perl</EM>スクリプトです。</H1>' ;
if ($text =~m/^<H1>(.+)<\/H1>$/) { print "s1='$1'\n" ; }
exit ;
結果は,次のように表示されるはずです。
s1='これは<EM>Perl</EM>スクリプトです。'
このように同じスクリプトでも,うまく働くことが確かめられます。ここで,外側のタグ“<H1>”と内側のタグ“<EM>”を同時に抽出するために,パターン照合の正規表現式を“^<H1>(.+)<EM>(.+)</EM>(.+)</H1>$”に替えた,次のスクリプトで試してみましょう。
$text = '<H1>これは<EM>Perl</EM>スクリプトです。</H1>' ;
if ($text =~m/^<H1>(.+)<EM>(.+)<\/EM>(.+)<\/H1>$/) { print "s1='$1' s2='$2' s3='$3'\n" ; }
exit ;
結果は,次のように表示されるはずです。
s1='これは' s2='Perl' s3='スクリプトです。'
これも,うまく働くことが確かめられます。
さて,実際に文書実現値を扱う場合には,前述したようにタグ名があらかじめ分かることは稀です。したがって,何が出現しても扱える方法が必要です。タグ名,データ共に抽出できるように,パターン照合の正規表現式を“^(<.+>)(.+)(<\/.+>)$”に替えた,次のスクリプトで試してみましょう。
$text = '<H1>これはPerlスクリプトです。</H1>' ;
if ($text =~m/^(<.+>)(.+)(<\/.+>)$/) { print "s1='$1' s2='$2' s3='$3'\n" ; }
exit ;
結果は,次のように表示されるはずです。
s1='<H1>' s2='これはPerlスクリプトです。' s3='</H1>'
では再び,“<H1>これは<EM>Perl</EM>スクリプトです。</H1>”のように,文書実現値の中に入れ子になったタグがある場合はどうなるでしょうか。これを同じスクリプトで試してみましょう。
$text = '<H1>これは<EM>Perl</EM>スクリプトです。</H1>' ;
if ($text =~m/^(<.+>)(.+)(<\/.+>)$/) { print "s1='$1' s2='$2' s3='$3'\n" ; }
exit ;
結果は,次のように表示されるはずです。
s1='<H1>これは<EM>Perl</EM>' s2='スクリプトです。' s3='</H1>'
今度はなぜかうまくいきません。不思議なことに,正規表現の“<(.+)>”は“<H1>これは<EM>Perl</EM>”と一致し,肝心な特殊変数$2は“スクリプトです。”となってしまいました。
行頭を合わせる“^”と行末を合わせる“$”は,この場合は働いていないように見えます。しかし,Perlは行の文字列を左から右へ単純に走査していますから,“(<.+>)”は,文字“<”があって,次に「任意の文字」が1個以上並んで,文字“<”が現われるような文字列に一致します。正規表現の“<.+>”が私たちが期待するように,「任意の文字」が“<”以外の文字が出てくるまで1個以上並ぶというわけではないのです。そこで,タグ名の書式を単なる「任意の文字」の並びではなく,「英数字」の並びとして与えるとどうなるでしょうか。正規表現式を“.+”から“[a-zA-Z0-9]+”に替えた,次のスクリプトで試してみましょう。
$text = '<H1>これは<EM>Perl</EM>スクリプトです。</H1>' ;
if ($text =~m/^(<[a-zA-Z0-9]+>)(.+)(<\/[a-zA-Z0-9]+>)$/) { print "s1='$1' s2='$2' s3='$3'\n" ; }
exit ;
結果は,次のように表示されるはずです。
s1='<H1>' s2='これは<EM>Perl</EM>スクリプトです。' s3='</H1>'
今度はうまくいきました。しかし,タグはタグ名だけではなく,属性名と属性値を含むこともあります。また,終了タグのないものもあります。ここでは,日本語のタグ名,日本語の属性値,全角の“<”と“>”は扱いません。正規表現式にそうした条件を考慮した,次のスクリプトで試してみましょう。
$text = '<A HREF="http://www.jagat.or.jp">日本印刷技術協会</A>' ;
if ($text =~m/^(<[\w\s=:\/\.\"]+>)(.*)(<\/[\w\s=:\/\.\"]+>)$/) { print "s1='$1' s2='$2' s3='$3' s4='$4'\n" ; }
exit ;
結果は,次のように表示されるはずです。
s1='<A HREF="http://www.jagat.or.jp">' s2='日本印刷技術協会' s3='</A>'
では,入れ子になったタグやそれが何組あるか分からないような場合は,どのように扱うべきでしょうか。次のスクリプトは一つの解法です。別の解法もあるでしょう。この解法では,いくつあるか分からないタグを一つ一つ検出していきます。
$text = '<H1>これは<EM>Perl</EM>スクリプトです。</H1>' ;
@as = () ;
while ($text =~m/^(.*)(<[\w\s=:\/\.\"]*>)(.*)$/) {
unshift(@as, $3) ;
unshift(@as, $2) ;
$text = $1 ;
}
foreach $s (@as) {
print "s=$s\n" ;
}
exit ;
結果は,次のように表示されるはずです。つまり,行の内容はタグとデータに分解され,それぞれがアレイ変数@asへ順に格納されます。明らかに,その終端は空の要素になります。
s='<H1>' s='これは' s='<EM>' s='Perl' s='</EM>' s='スクリプトです。' s='</H1>' s=''
では,2行にまたがったタグがある場合は,どのように扱うべきでしょうか。次のスクリプトは,作業用ファイルを使って行なう,一つの解法です。これもまた別の解法があるでしょう。
$sourcefile = <> ;
chop $sourcefile ;
$workfile = <> ;
chop $workfile ;
#
open IN, "<$sourcefile" ;
open OUT, ">$workfile" ;
$text = '' ;
while ($ntext = <IN>) {
chop $ntext ;
$text .= $ntext ;
if ($text =~m/^.*<[\w\s=:\/\.\"]*>(.*)$/) {
if ($1 eq '') {
print OUT "$text\\n\n" ;
$text = '' ;
next ;
}
}
}
if ($text ne '') {
print OUT "$text\n" ;
}
close IN ; close OUT ;
#
open IN, "<$workfile" ;
@as = () ;
$an = 0 ;
$text = '' ;
while ($text = <IN>) {
chop $text ;
@as = () ;
while ($text =~m/^(.*)(<[\w\s=:\/\.\"\\]*>)(.*)$/) {
unshift(@as, $3) ;
unshift(@as, $2) ;
$text = $1 ;
}
if ($text ne '') {
unshift(@as, $text) ;
}
print "----\n" ;
foreach $s (@as) {
print "s='$s'\n" ;
}
}
close IN ;
exit ;
ここでは,ファイル$sourcefileの内容は,次のようなものとします。ここで,
印は行末符号(CR又はCR+LF)を表わすものとします。
<H1 align=CENTER>これは<EM>テスト</em
>です。</
H1>
<p>
最初の段落です。
<p><a HREF
="http://www.jag
at.or.jp/">
2番目の段落です。</a>
</p>
![]()
結果は,次のように表示されるはずです。“---”は行の区切りを示しています。行にまたがった内容はタグとデータに分解され,行ごとにそれぞれがアレイ変数@asへ順に格納されます。
---- s='<H1 align=CENTER>' s='これは' s='<EM>' s='テスト' s='</em>' s='です。' s='</H1>' s='\n' ---- s='<p>' s='\n' ---- s='最初の段落です。' s='<p>' s='' s='<a HREF="http://www.jagat.or.jp/">' s='\n' ---- s='2番目の段落です。' s='</a>' s='\n' ---- s='</P>' s='\n'
ここで,幾つかの疑問が生じるでしょう。
先ず,なぜ作業用ファイル$workfileを使うのかという点です。実際に,文書実現値のすべてをメモリー上に格納することはできません。さらに,メモリー上に格納できない,極めて長い1行を入力しなければならないかもしれません。それについては別の対策が必要です。別の機会に説明しましょう。ともかく,そうした極めて長い1行がないとしても,行単位にタグとデータを分解することには十分に意義があると思います。
もう一つは,なぜ行末符号をわざわざ“\n”として含めるかという点です。こうしたものがない方が処理しやすいと思うかもしれませんが,HTMLのPREタグのように行末符号をデータ内容として扱う場合もあります。したがって,行末符号はタグによって無視するかどうかを決めることになるでしょう。
また,“<!--”と“-->”で挟まれた注釈宣言をどのように扱うのかを決めておく必要があります。ちなみに,最後に示したスクリプトでは注釈宣言を扱うことはできません。
結論として,Perlによる文書実現値の処理では,SGMLパーザーの入力処理と同じくらいのことが要求される,と覚悟をしたほうがいいでしょう。
また,Perlによる文書型定義の処理は,さらに困難なものです。それに挑戦する場合は,Earl Hood氏が開発したperlSGMLが参考になると思います。
(1998年9月記)