本稿では、コンピュータを使った作業一般に (というか、主にテキストデータを「やっつける」のに) おいて役立つであろう Perl の使用法を紹介します。

テキストデータを処理するのにあたって、Perl 以外にも様々な選択肢があります。 シェルスクリプトや sed, awk を組み合わせて使うこともできますし、今から学ぶのであれば Ruby あたりの方がよかったりするかもしれません。 私が Perl を使う理由としては、

  1. 大抵のコンピュータにインストールされている
  2. 正規表現での文字列操作などを短いプログラムで処理することができる
  3. sed から移行しやすかった (s/// の記法がそのまま使える)

といったことが挙げられます。

例えば sed を使ってもよいかもしれませんが、インストールされているバージョンによってはコマンドのオプションが全く異なっていて予想外の動作をすることもあります。
大抵のコンピュータにインストールされていますが、Windows には標準でインストールされていません。これについては cygwin などで対処するとよいでしよう。

perldoc

Perl の使用法の前に、まずは Perl に関する情報をどこから得ればよいかについてです。本稿では、Perl の言語やライブラリに関する包括的な情報は取り扱いません。 代わりに、perldoc を使用できます。perldoc は、Perl に関するドキュメントを読むのに使用するプログラムです。Perl に関する大抵の情報は、perldoc によって得られます。

perldoc perl

で、perldoc で読むことのできるコアドキュメントを一覧できます。perldoc そのものについては perldoc perldoc でマニュアルを参照できます。 日本語で perldoc を読みたければ、perldoc.jp にアクセスするとよいでしょう。コアドキュメントの一覧は本体のページにあります。

Perl ワンライナー

テキストデータをやっつけるのに役立つ Perl の使用法として、プログラムをファイルに保存して実行するのではなく、コマンドラインで直接指定して実行させる、ワンライナーでの使用法をまずは紹介します。

-e

perldoc perlrun に詳しいですが、Perl は、-e スイッチによってプログラムを直接指定して実行させられます。ワンライナー版の Hello, world! は次のように書けます。

$ perl -e 'print "Hello, world!\n"'
Hello, world!

-E スイッチを使うと、Perl 5.10 以降で追加されたオプション機能 (switch-case 的な構文の given-when 構文や say 関数など) を有効にすることができます。

$ perl -E 'say "Hello, world!"'
Hello, world!

「オプション機能」については perldoc feature に詳しいです。

-n, -p

-n スイッチを使うと、標準入力、または指定されたファイルの内容を処理できます。sed の -n スイッチと同様なものだと考えればよいでしょう。

$ cat data.txt 
Foo
Bar

$ perl -ne 'print "Hello, $_"' data.txt
Hello, Foo
Hello, Bar

この場合、$_ には入力行の内容が入っています。Perl の特殊変数については、perldoc perlvar に詳しいです。

-p スイッチは -n スイッチと似ていますが、入力行を自動的に出力します。これも sed の -p スイッチと同様なものです。

$ perl -pe 's/^/Hello, /' data.txt
Hello, Foo
Hello, Bar

なお、これは perldoc perlrun に記載されていますが、-n スイッチを使った場合、指定したプログラムの周囲に次のようなループが存在するような動きをします。

  LINE:
    while (<>) {
        ...             # your program goes here
    }

-p の場合は次のようなループとなります。

  LINE:
    while (<>) {
        ...             # your program goes here
    } continue {
        print or die "-p destination: $!\n";
    }

<> は標準入力、またはコマンドラインに指定されたファイルを読み込むために使用できます。while (<>) によって、入力行が $_ にセットされた状態でループ内の処理が実行されます。

-l スイッチを指定すると、入力行から改行コードが除去され、print する際に再び付与されます。入力行の一部分を処理して出力する場合などに便利です。

$ ls -Fla *.txt | perl -lne '/ (\d+:\d+) .*\.txt$/ and print $1'
16:20
22:29
03:47

-a

-a スイッチによって awk のような動きをさせられます。

$ cat data2.csv
Foo,f,1
Bar,b,2
Baz,b,3

$ perl -F',' -lae 'print "$F[1] $F[2]"' data2.csv
f 1
b 2
b 3

-F で指定した区切り文字によって split された $_@F に設定されます。

文字コードについて

Perl における文字コードの扱いについては歴史的な経緯が色々とあるらしく、インターネットを検索すると (Perl のバージョンに応じた) 色々なやり方が紹介されています。それらを参考に色々と試しているとどんどん深みにハマっていき、妙なソースコードが出来上がるようなこともしばしばです。。。

しかし、Perl 5.8 以降 (できれば 5.10.1 以降) を使用し (おそらく大抵のコンピュータにはこれ以降のバージョンがインストールされていると思われます)、以下のやり方で処理すれば特に問題は起きないと思われます。おそらくは。。。

-C, -Mutf8

UTF-8 のテキストファイルを、日本語を含むプログラムで処理してみます。

$ cat encoding/utf8.txt
UTF-8 のテキストファイルです。
Perl では「:utf8」というエンコーディング指定で処理するとよいでしょう。

$ perl -ne 's/テキストファイル/てきすとふぁいる/ and print' encoding/utf8.txt
UTF-8 のてきすとふぁいるです。

手元の Mac で動かしてみたところ、あっさり動作しました。。

私が普段 Perl を使用している環境は Windows 7 上の cygwin だったりしますが、こちらでは、次のように指定しなければ期待した通りに動作しません (LANG の設定など特に問題ないはずなのですが)。

$ perl -CSDA -Mutf8 -ne 's/テキストファイル/てきすとふぁいる/ and print' encoding/utf8.txt
UTF-8 のてきすとふぁいるです。

-C に続く文字 (フラグ) は次のような意味となります。

フラグ 意味
S STDIN, STDOUT, STDERR に対して UTF-8 で入出力する
D 入出力ストリームのデフォルトエンコーディングを UTF-8 にする
A @ARGV の要素 (コマンドライン引数) を UTF-8 でデコードする

つまり、-CSDA で、全ての入出力が UTF-8 で処理されるようになります。

-Mmodule は、プログラムの実行前に use module; を実行します。つまり、-M に続けて指定したモジュール・プラグマをインポートします。

-Mutf8utf8 プラグマを有効にし、プログラムが UTF-8 で書かれていることを Perl に示します (詳細は perldoc utf8 参照)。

UTF-8 以外のマルチバイト文字処理

世の中 UTF-8 のテキストデータばかりならよいのですが、残念ながらそれ以外のテキストデータも避けては通れません。。ここでは、Shift_JIS 的な (Windows-31J, MS932, CP932 の) テキストデータを処理する方法を紹介します (CP932 (Microsoft Code Page 932) については Wikipedia の解説 をどうぞ)。

CP932 のテキストデータを読み取って、UTF-8 で出力してみます。

$ cat encoding/cp932.txt
Shift_JIS �I�ȕ����R�[�h�̃e�L�X�g�t�@�C���ł��B
Perl �ł́u:encoding(cp932)�v�Ƃ����G���R�[�f�B���O�w��ŏ�������Ƃ悢�ł��傤�B

$ perl -Mopen=':std,IN,:encoding(cp932),OUT,:utf8' -pe '' encoding/cp932.txt
Shift_JIS 的な文字コードのテキストファイルです。
Perl では「:encoding(cp932)」というエンコーディング指定で処理するとよいでしょう。

-Mopen... は、デフォルトの PerlIO レイヤを設定するためのプラグマ指定です。上記の例では、入力を cp932 というエンコーディングに、出力を utf8 に設定し、:std で標準入出力にも指定のエンコーディングを適用しています (詳細は perldoc open 参照)。

cat では文字化けしてしまいますが、-pe に空文字列を指定した Perl のワンライナーでは文字化け無しで出力できています。このワンライナーは、nkf のようなプログラムがインストールされていない環境でその代わりに使用できます。

grep

次のように、簡易的な grep を文字コード指定で実現できます。

$ perl -Mopen=':std,IN,:encoding(cp932),OUT,:utf8' -pe '' encoding/cp932.txt
Shift_JIS 的な文字コードのテキストファイルです。
Perl では「:encoding(cp932)」というエンコーディング指定で処理するとよいでしょう。

$ perl -Mopen=':std,IN,:encoding(cp932),OUT,:utf8' -pe '' encoding/cp932-2.txt
open my $in, '<:utf8', $file or die "$!: $file";

のように open するか、または、既に open しているファイルハンドルがあるなら、

binmode $in, ':utf8'

とすればよいでしょう。

$ perl -Mutf8 -Mopen=':std,IN,:encoding(cp932),OUT,:utf8' -ne '/ファイル/ and print "$ARGV:$.:$_"; eof and close ARGV' encoding/cp932.txt encoding/cp932-2.txt
encoding/cp932.txt:1:Shift_JIS 的な文字コードのテキストファイルです。
encoding/cp932-2.txt:3:のように open するか、または、既に open しているファイルハンドルがあるなら、

$ARGV は、<> から読み込んでいる際のファイル名を、$. は行番号を表します。

<> から読み込む場合、ファイルハンドルがクローズされずに次のファイルを読み込むため、明示的にクローズしない限りファイル毎に行番号がリセットされません。

eof は、<> がファイルの終端に達している場合に真を返します。また、<><ARGV> のシンタックスシュガーです (さらに言えば <ARGV>readline ARGV のシンタックスシュガーです)。ので、上記の例ではファイルの終端に達した際に一旦ファイルハンドルをクローズして行番号をリセットしています (ちなみに eof() とすると「最後のファイルの終端」の場合にだけ真が返されます)。詳細については、perldoc -f eof, perldoc perlvar を参照して下さい。

また、次のように、マッチした部分をハイライトして出力してもよいでしょう。

$ perl -Mutf8 -Mopen=':std,IN,:encoding(cp932),OUT,:utf8' -ne 's/ファイル/\e[31m$&\e[0m/ and print "$ARGV:$.:$_"; eof and close ARGV' encoding/cp932.txt encoding/cp932-2.txt
encoding/cp932.txt:1:Shift_JIS 的な文字コードのテキストファイルです。
encoding/cp932-2.txt:3:のように open するか、または、既に open しているファイルハンドルがあるなら、

ただし、一旦まともな文字コードで標準出力した後であれば、単に grep などでハイライトした方が簡単かもしれません。

$ perl -Mutf8 -Mopen=':std,IN,:encoding(cp932),OUT,:utf8' -ne 'print "$ARGV:$.:$_"; eof and close ARGV' encoding/cp932.txt encoding/cp932-2.txt | egrep --color=auto 'ファイル'
encoding/cp932.txt:1:Shift_JIS 的な文字コードのテキストファイルです。
encoding/cp932-2.txt:3:のように open するか、または、既に open しているテキストハンドルがあるなら、

perldoc の内容を出力するワンライナー

perldoc を使っていると、この内容も grep, perl その他で処理できればと思うときがあります。次のワンライナーは perldoc の内容をテキストファイルに出力します。

mkdir pages
perl -MFile::Find=find -MFile::Spec::Functions -le 'find { wanted => sub { m|/pods/(.*)\.pod$| and !/perl\d*delta/ and print "$1" }, no_chdir => 1 }, @INC' | xargs -i perldoc -t -dpages/{}.txt {}

mkdir modules
perl -MFile::Find=find -MFile::Spec::Functions -le 'find { wanted => sub { m|^(.*)\.pm$| or next; $_ = $1; for my $i (@INC) { s|$i|| } s|/|::|g; print }, no_chdir => 1 }, @INC' | xargs -i perldoc -t -dmodules/{}.txt {}

Perl スクリプト

Perl の構文など

Perlの構文については「基礎文法最速マスター」シリーズの発端となった Perl基礎文法最速マスターによくまとまっていますのでこれを読んでみるのがよいでしょう。

また、perldoc perlsyn (Perl の文法), perldoc perlop (Perl の演算子と優先順位) あたりを参考にして下さい (長いですが)。

リファレンスについて

「Perl基礎文法最速マスター」には「リファレンス」についての説明があまりありませんでした。関数に配列・ハッシュ (連想配列) を引き渡して処理させる場合に理解しておいた方がよいポイントですので簡単に説明して本稿を締めたいと思います。ちなみに「リファレンス」についての説明も perldoc によくまとめられています (Perlのリファレンスとネストしたデータ構造)。長いですが。

ちなみに、perldoc.jp からのリンクで、2時間半で学ぶPerl というとても現実的な?テキストがありましたので紹介しておきます。ちなみにこちらには文字列置換についての説明も含まれています。

#!/usr/local/opt/coreutils/bin/genv perl

use strict;
use warnings;
use utf8;
use v5.14;

sub main {
    my $s1 = 'foo';
    my @a1 = ('bar', 'baz');    # qw(bar baz) とも書ける
    sub1($s1, @a1);
    sub2($s1, \@a1);    # 配列をリファレンスで引き渡す
    sub2($s1, ['bar', 'baz']);  # [] は配列のリファレンスを表す
    sub2($s1, [qw(bar baz)]);   # 同じ結果となる

    # ハッシュについても概ね同様
    my %h1 = ('f' => 'Foo', 'b' => 'Bar');
    sub3($s1, \%h1);
    sub3($s1, { 'f' => 'Foo', 'b' => 'Bar'});   # {} はハッシュのリファレンスを返す
}

sub sub1 {
    my ($s1, $s2) = @_;
    say "s1: $s1, s2: $s2";
}

sub sub2 {
    my ($s1, $ar1) = @_;    # 配列のリファレンスとして受け取る
    print "s1: $s1, ar1: [ ";
    for (my $i = 0; $i < scalar(@$ar1); ++$i) { # 「@$」で、配列としてデリファレンスできる。また、配列はスカラコンテキストで評価すると要素数となる。
        print "$i: $ar1->[$i], ";   # $ref->[index] で、配列リファレンスの要素にアクセスできる
    }
    say ']';

    # 配列リファレンスへのアクセス例を示すため回りくどく書いたが、次のようにも書ける
#    my $i = 0;
#    for my $s (@$ar1) {
#        print $i++ . ": $s ";
#    }
}

sub sub3 {
    my ($s1, $hr1) = @_;    # ハッシュのリファレンスとして受け取る
    print "s1: $s1, hr1: { ";
    for my $k (sort keys %$hr1) {   # 「%$」で、ハッシュとしてでリファレンスできる
        print "$k: $hr1->{$k}, ";   # $ref->{index} で、ハッシュリファレンスの要素にアクセスできる
    }
    say '}';
}

main;