Permalink
Find file
e9dfac3 Aug 20, 2016
@xtetsuji @papix
709 lines (547 sloc) 25 KB

Perl入学式

第4回 サブルーチン/正規表現編


諸注意

  • 会場について
    • 飲食・喫煙・トイレetc
  • 写真撮影について
    • 写真撮影NGな方はお手数ですが申し出てください

紹介

  • 講師・サポーター紹介

本日の内容

  • リファレンスの復習
  • サブルーチン
  • サブルーチンとリファレンス
  • 正規表現
  • 正規表現と置換
  • 正規表現とメタ文字
  • 正規表現のオプション

皆さんで自己紹介


リファレンスの復習

  • 前回の復習問題の「2. score.pl」の「2-1. 点数の合計」, 「2-2. 言語ごとの平均」の問題を解きながら, リファレンスを復習してみましょう.
  • こちらにサンプルデータがあります.

サブルーチン


サブルーチンとは?

  • プログラムの中で, 意味や内容がまとまっている作業をひとかたまりにしたものを「サブルーチン」と呼びます.
  • Perlにおける「サブルーチン」は, 「関数」とほぼ同義です.

組み込み関数

  • Perlには, これまで使ってきたprintjoinなど, Perlが提供する関数(組み込み関数)が用意されています
  • サブルーチンを使うことで, printjoinのように, 「特定の処理をするコード」をひとかたまりにして, 簡単に呼ぶことが出来るようになります.

サブルーチンの定義

  • それでは, 早速サブルーチンを定義していきましょう.
  • 今回は, 末尾に自動的に改行(\n)を付与しながら文字列を表示するsayというサブルーチンを定義してみます.

サブルーチンの定義

sub say {
    my $str = shift @_;
    print "$str\n";     
}
say("hello, world!"); # => hello, world![改行]
  • それでは, 詳しく見て行きましょう.

サブルーチンの定義

sub say { ... }
  • Perlでサブルーチンを定義する為には, sub サブルーチン名 { ... }と書きます.
  • 末尾の}の後に, ;を書く必要はありません.
  • サブルーチン名として使える文字は大文字・小文字の英数字と, アンダースコア(_)です.
    • 但し, サブルーチン名の先頭は英文字か_でなければなりません.

サブルーチンの定義

sub output_data { ... }
  • 複数の単語でサブルーチン名を構築する時は, このように単語どうしを_で繋げる(スネークケース)場合が多いです.

クイズ

sub hoge!    { ... }
sub _hoge    { ... }
sub 1hoge    { ... }
sub hoge_123 { ... }
  • この中で, サブルーチン名として正しいものはどれでしょうか ?

正解は?

sub hoge!    { ... } # => '!'はサブルーチン名に使えない
sub _hoge    { ... }
sub 1hoge    { ... } # => 先頭は英字or'_'
sub hoge_123 { ... }
  • 正解は_hogehoge_123です.

サブルーチンの呼び出し

say('hoge');
  • 定義したサブルーチンは, 定義したサブルーチン名の後ろに()を付けることで呼び出せます.
  • サブルーチンに値(引数)を渡したい場合, ()の中に書きます.
  • ()を使わずに, 先頭に&を付けて&sayで呼びだすこともできますが, 古い書き方なので使わないようにしましょう.

サブルーチンの引数

sub say {
    my $str = shift @_; # @_ => ('hoge')
    ...
}
say('hoge');
  • サブルーチンに与えられた引数は, @_という配列(無名配列)に格納されます.
  • 2行目では, shiftを使って, この@_の先頭の要素を取得しています.
  • このサブルーチンをsay('hoge');のように呼んだ場合, @_の中身は('hoge')になるので, $strにはhogeという文字列が入ります.

サブルーチンの引数

sub say {
    my $str = shift;
    ...
}
  • サブルーチンに与えられた引数が格納されている@_は, 省略することができます.
  • その為, 2行目のmy $str = shift;は, my $str = shift @_;と同じ意味になります.

サブルーチン「add」

sub add {
    my ($left, $right) = @_;
    return $left + $right;
}
my $result = add(2, 5);
  • 次に, 2つの引数を受け取り, その和を返すサブルーチンaddを考えてみることにします.
  • addサブルーチンの定義と呼び出しは, このように書くことができます.

サブルーチンの引数

sub add {
    my ($left, $right) = @_;
    ...
}
my $result = add(2, 5);
  • サブルーチンに複数の引数が与えられた場合(この場合は25), サブルーチン側ではこのようにして受け取ることができます.
    • サブルーチンに複数の引数を与える時は, (配列のように),で区切って渡します.

サブルーチンの引数

sub add {
    my $left  = shift;
    my $right = shift;
    ...
}
  • 先程の引数の受け取り方は, 上記のコードと同じ意味になります.

サブルーチンの返り値

sub add {
    my ($left, $right) = @_;
    return $left + $right;
}
my $result = add(2, 5);
  • サブルーチンは, returnを使うことで, 任意のデータを呼び出し元へ返すことができます.
    • この場合, $left + $rightの計算結果が, 呼び出し元へ返され, $resultに格納されます.

複数個のreturn

sub same {
    my ($i, $j) = @_;
    if ($i == $j) {
        print "true\n"; # $i == $jなら表示
        return 1;
    } else {
        print "false\n"; # $i != $jなら表示
        return 0;
    }
    print "!?\n"; # 絶対に表示されない!
    return undef;
}
  • returnはサブルーチンの中に複数個書くことができますが, returnに到達した場合, それ以降の処理は一切行われず, すぐさま値を返してサブルーチンの実行を終了します.

複数の返り値

sub calc {
    my ($left, $right) = @_;
    return ($left + $right, $left - $right);
}
my ($add, $min) = calc(5, 4);
  • サブルーチンは, このようにリストを返すことで, 複数個の値を返すこともできます.

returnがない場合の返り値

sub add {
    my ($left, $right) = @_;
    $left + $right;
}
  • サブルーチンの中にreturnがない場合, サブルーチンの返り値は, 最後に評価された処理の結果(この場合, $left + $rightの計算結果)を返します.

練習問題

  • 次のような機能を持つコードを, simple_calc.plという名前で作成しよう.
    • 2つの引数の和を計算するaddと同様に, 2つの引数の差を計算するmin, 積を計算するmul, 商を計算するdivというサブルーチンを作ってみよう.
    • これらのサブルーチンが正しく実装できているか(与えた2つの引数に対して, 適切な値を返すか)を確認するコードを書いてみよう.

サブルーチンと
リファレンス


配列を引数に

my @hoge = qw/ hoge fuga /;
my @foo  = qw/ foo bar baz /;
sub output {
    my (@array1, @array2) = @_;
    print '@array1 = ' . join(',', @array1) . "\n";  
    print '@array2 = ' . join(',', @array2) . "\n";  
}
output(@hoge, @foo);
  • サブルーチンの引数として, 2つの配列を与えてみましょう.
    • この時, 実行結果はどうなると思いますか? 考えてみましょう.

配列を引数に

@array1 = hoge,fuga,foo,bar,baz
@array2 =
  • 実行結果はこのようになります!

配列を引数に

@array1 = hoge,fuga
@array2 = foo,bar,baz
  • このようになる, と予測した方は多いのではないでしょうか?

なぜ!?

my @array1 = qw/ hoge fuga /;
my @array2 = qw/ foo bar baz /;
my @array  = (@array1, @array2);
# @array = ('hoge', 'fuga', 'foo', 'bar', 'baz');
  • これは, 複数の配列をリストで評価すると, 展開されて元の配列の区別がなくなってしまう為です.
  • 先程の場合, @_の中身は`('hoge', 'fuga', 'foo', 'bar', 'baz')になります.
    • Perlは元の配列の@hoge@fooの境目が@_のどこにあるかわからないので, 全て@array1に突っ込もうとします.

そこで!

  • この問題を解決する為に, リファレンスを利用することができます!

リファレンス渡し

my @hoge = qw/ hoge fuga /;
my @foo  = qw/ foo bar baz /;
sub output {
    my ($array1, $array2) = @_;
    print '@$array1 = ' . join(',', @$array1) . "\n";  
    print '@$array2 = ' . join(',', @$array2) . "\n";  
}
output(\@hoge, \@foo);
  • 8行目で配列のリファレンスを渡しているので, 4行目では2つのスカラ変数で引数を受け取っています.

リファレンス渡し

@$array1 = hoge,fuga
@$array2 = foo,bar,baz
  • 実行してみると, 期待通りの結果を取得することができました.

リファレンス渡し

  • 配列と同様に, ハッシュもリファレンスで渡すことができます.
  • 更に, リファレンスで渡す場合, 配列をそのまま渡した時のように, データのコピーが発生しません.
  • 引数として与えた配列・ハッシュの構造をそのままサブルーチンに渡すことができ, データのコピーが発生しない「リファレンス渡し」ですが, 1つ注意点があります.

中身の書き換え

my %hash = ( alice => 'fool' );
print $hash{alice}; # => fool
fix(\%hash);
print $hash{alice}; # => clever
sub fix {
    my $hash = shift;
    $hash->{alice} = 'clever';
}
  • サブルーチンに渡される配列・ハッシュのリファレンスは, サブルーチンの外側にある配列・ハッシュの実体(この場合, %hash)を指しているので, サブルーチンの中でデータ構造を書き換えると, 外側の実体にも影響が出てしまいます.

練習問題

  • 配列とハッシュをそれぞれ1つずつ定義してから, 第1引数に配列のリファレンス, 第2引数にハッシュのリファレンスを受け取り, その中身を出力する(for文などを利用して...), output_array_and_hashというサブルーチンを書いてみよう.
  • このコードは, output_array_and_hash.plという名前で保存するようにしましょう.

正規表現


正規表現

  • ここでは, データ処理の強い味方「正規表現」を取り上げます.
    • 正規表現を使うことで, 文字列を自由自在に検出したり, 置き換えたりすることができます.
  • 正規表現は非常に複雑なので(正規表現だけで分厚い1冊の技術書が書けるほどです), Perl入学式で全てを紹介することはできませんが, コードを書く上でよく使う「基本的な部分」を中心に, 紹介していきます.

パターンマッチ

my $str = 'papix loves perl!';
if ($str =~ /perl/) {
    print "'$str'は'perl'を含みます.";
}
  • $str =~ /perl/は, $strの中に「perl」という文字列が含まれるなら真, そうでないなら(含まれないなら)偽, になります.
  • この, /に囲まれた, 文字列のパターンを表現するものが「正規表現」です.

パターンマッチ

my $str = 'papix loves perl!';
if ($str eq 'perl') {
    print "'$str'は'perl'です.";
}
if ($str =~ /perl/) {
    print "'$str'は'perl'を含みます.";
}
  • eqは完全一致か否かしか判定できません. しかし正規表現とパターンマッチを活用することで, 「xxxという文字列を含む」や, その逆の「xxxという文字列を含まない」といった複雑な判定を行うことができます.

パターンマッチ

my $str = 'papix loves perl!';
if ($str !~ /ruby/) {
    print "'$str'は'ruby'を含みません.";
}
  • $str !~ /ruby/と書くことで, $strの中に「ruby」という文字列を含まないなら真, そうでないなら(含むなら)偽, になります.

任意の1文字

my $ans = 'y';
if($ans =~ /[yY]/) {
    print "文字列にはyないしYが含まれています.\n";
}
  • []で文字をくくると, []の中の任意の1文字にマッチします.
  • よって/[yY]/は, yないしYにマッチします.

任意の1文字(否定)

my $ans = 'n';
if($ans =~ /[^yY]/) {
    print "文字列にはyないしY以外の文字が含まれています.\n";
}
  • []で文字をくくり, その先頭に^を置くと, []の中にない任意の1文字にマッチします.
  • よって/[^yY]/は, yないしY以外の文字にマッチします.
  • ^は, 必ず[の後に置いて, [^の形で用います.

任意の1文字(連続)

my $ans = 'b';
if($ans =~ /[a-c]/) {
    print "文字列にはa, b, cのいずれかが含まれています.\n";
}
  • []の中で, 文字の間に-を挟むことによって, 文字列の範囲を表現できます.
  • この場合, [a-c][abc]と同じ意味になります. [1-5]のように, 数値に対しても利用できます.

ワイルドカード

my $ans = 'get';
if($ans =~ /g.t/) {
    print "マッチ!\n";
}
  • .は, 改行文字(\n)を除く, 任意の1文字にマッチします.
  • よって/g.t/は, getgotなど, g+任意の1文字+tにマッチします.
    • .がマッチするのは1文字だけなので, goatなどはマッチしません.
    • また, gtにもマッチしません.

量指定子'?'

my $ans = 'gt';
if($ans =~ /g.?t/) {
    print "マッチ!\n";
}
  • ?は, その直前の要素が0個または1個の場合にマッチします.
    • 例えばab?は, aまたはabにマッチします.
  • よって/g.?t/は, g+任意の1文字+tに加え, gtにもマッチします.

量指定子'+'

my $ans = 'get';
if($ans =~ /g.+t/) {
    print "マッチ!\n";
}
  • +は, その直前の要素が1個以上の場合マッチします.
    • 例えばab+cは, abcabbbbcなどにマッチしますが, acにはマッチしません.
  • よって, /g.+t/は, g+任意の1文字以上+tにマッチします.

量指定子'*'

my $ans = 'great';
if($ans =~ /g.*t/) {
    print "マッチ!\n";
}
  • *は, その直前の要素が0個以上の場合マッチします.
    • 例えばab*cは, acabc, abbbbbcなどにマッチします.
  • よって/g.*t/は, gで始まりtで終わる全てのフレーズとマッチします(greatなど).

柔軟な量指定子

my $str = 'Gyaaaaaaaaa!';
print "マッチ!\n" if $str =~ /a{5,}/;
# マッチする
my $str2 = 'Gyaa!';
print "マッチ!\n" if $str2 =~ /a{5,}/;
# マッチしない
  • {m,n} ... その直前の要素がm回以上, n回以下繰り返す場合マッチ
  • {m,} ... その直前の要素がm回以上繰り返す場合マッチ
  • {m} ... その直前の要素がm回繰り返す場合マッチ

練習問題

  • 引数として文字列を受け取り, その文字列にperlないしPerlが含まれるなら「Perl Monger!」と表示するサブルーチンperl_checkerを書いてみましょう.
  • コードは, perl_checker.plという名前で保存するようにしましょう.

マッチした文字列の取得

my $str = '私は perl が好きです.';
if($str =~ /私は (.+) が好き/) {
    print "彼は, $1 が好きです.\n";
    # => 彼は, perl が好きです
}
  • 正規表現のパターンを()を囲むと, そのパターンに一致する文字列を取得することができます.
  • 例えばこの場合, $1にはperlが入り, 彼は, perl が好きです.と表示されるはずです.

マッチした文字列の取得

my $str = '私は perl と 旅行 が好きです.';
if($str =~ /私は (.+) と (.+) が好き/) {
    print "彼は, $1 と $2 が好きです.\n";
    # => '彼は, perl と 旅行 が好きです.'と表示.
}
  • 複数の()が存在する場合, 先頭から$1, $2... で取得することができます.

マッチングの原則

my $str = 'Hello hoge! Hello fuga!';
if($str =~ /Hello (.+)!/) {
    print "Nice to meet you, $1!\n";
}
  • hogeを抜き出してNice to meet you, hoge!としたいので, このようなコードを書きました.
  • しかしながら, 実際にはNice to meet you, hoge! Hello fuga!と表示されます.

マッチングの原則

my $str = 'Hello hoge! Hello fuga!';
if($str =~ /Hello (.+?)!/) {
    print "Nice to meet you, $1!\n";
}
  • これは, 正規表現が「なるべく長くマッチする(最長マッチ)」ようになっている為です.
  • このように, 量指定子のあとに?を付けて, 最短マッチにすれば, Nice to meet you, hoge!と出力されるはずです.

練習問題

my $words_ref = [
    'alice loves meat!',
    'bob loves sushi!',
];
  • このような配列のリファレンスを受け取り, リファレンスに格納された文字列について, 「loves」の後に記述されている好きな食べ物の単語を正規表現で取得し, 「alice -> meat」, 「bob -> sushi」のように表示するサブルーチン, love_foodを書いてみよう.
  • このコードは, love_food.plという名前で保存するようにしましょう.

正規表現と置換


置換

my $str = 'abc def ghi';
$str =~ s/abc/ABC/;
# $str = 'ABC def ghi';
  • s/PATTERN/REPLACE/で, PATTERNREPLACEに置換します.
    • PATTERNを記述する為に, 正規表現を利用することができます.
  • $strに含まれる全てのPATTERNを置換したい場合, s/PATTERN/REPLACE/gと表記します.
    • 最後にオプションとしてgを付けることで, 繰り返し評価・置換します.

変数の使用

my $str = 'perl ruby python';
my $pattern = 'perl';
if($str =~ /$pattern/) {
    print "'$str'には'$pattern'が含まれます.\n";
}
  • このように, 正規表現として変数を利用することもできます.

練習問題

my $str = 'I love ruby';
  • この$strに格納された文字列を, 置換を利用して, 「I love perl」に書き換えるようなコードを書いてみましょう.
    • コードは, regexp_replace.plという名前で保存しましょう.

正規表現とメタ文字


メタ文字

  • メタ文字を使うと, 「数字とマッチ」や「アルファベットとマッチ」などといった正規表現を, より簡単に表現することができます.
  • ここでは, よく使うメタ文字を紹介します.

メタ文字(1)

  • \w ... アルファベット, 数字, アンダーバーの1文字

    • [a-zA-Z0-9_]と同じ意味です.
  • \W ... アルファベット, 数字, アンダーバー以外の1文字

    • [^a-zA-Z0-9_]と同じ意味です.
  • \d ... 数字の1文字

    • [0-9]と同じ意味です.
  • \D ... 数字以外の1文字

    • [^0-9]と同じ意味です.

メタ文字(1)

  • \s ... 空白文字にマッチ

    • [ \n\r\f\t]と同じ意味です.
  • \S ... 空白文字以外にマッチ

    • [^ \n\r\f\t]と同じ意味です.

メタ文字(1) 使い方

my $str1 = '2012年7月22日';
if($str1 =~ /(\d+)年(\d+)月(\d+)日/) {
    print "$1/$2/$3";
    # "2012/7/22"と表示される.
}
my $str2 = "この    文章  は\n 読みにく\nい    で  \t    す\n";
$str2 =~ s/\s+//g;
# $str2 = "この文章は読みにくいです";
  • \sを使えば, 余分な空白や改行を抜き取ることができます.

メタ文字(2)

  • | ... 選択一致(OR検索)
    • 例えば, abc|def|ghiは, abc, def, ghiのいずれかにマッチします.
  • (x) ... グループ化
    • 正規表現をグループ化します.
    • 先に説明したように, ()の中のパターンにマッチした文字列は記憶され, $1$2のように後で参照することができます(後方参照).
  • (?:x) ... 後方参照しないグループ化
    • 正規表現をグループ化しますが, ()の中のパターンにマッチした文字列は記憶されません.

メタ文字(2) 使い方

my $str = 'perl is good!';
if($str =~ /(?:perl|ruby|python) is (good|bad)!/) {
    print "評価は $1 です!\n";
    # "評価は good です!"と表示される.
}
  • perl, ruby, python|でつなぎ, ()で囲うことで, 選択一致をグループ化しています.
  • 更に, (?:とすることで, 後方参照しないようにしています.
    • その為, $1は(good|bad)のパターンにマッチした文字列となります.

正規表現のメタ文字(3)

my $str = 'john is dead.';
if ($str =~ /dead\./) {
    print "match!\n";
}
  • \ ... メタ文字を無効化する
    • 正規表現の中で特殊な意味を持つ文字(例えば/.など)を無効化します.
  • この場合. $str =~ /dead./は, john is dead!などでもマッチしてしまう(.は任意の1文字とマッチ, なので).
  • \.のようにすれば .そのものとのマッチができます.

アンカー

  • アンカーは, 行頭や行末など, 文字列の特定の位置とマッチします.
    • ^ ... 行頭
    • $ ... 行末

アンカー 使い方

my $str = 'john is great';
# 行頭に'john'がある場合のみマッチ
if ($str =~ /^john/) {
    print "match!\n";
}

区切り記号の変更(1)

my $str = '/usr/local/bin/perl';
if ($str =~ m|bin/perl|) {
    print "match!\n";
}
  • 正規表現は/で区切りますが, /だと不都合な場合も多いです(例えば, URLを表記する場合など. 全ての/をエスケープする必要がある).
  • そこで, m//のように, 先頭にmを付けると, 任意の記号のペアを区切り記号として利用することができます.
  • 今まで見てきた通り, 区切り記号が / の場合のみ m を省略できます.
  • この場合, |を区切り記号にしています. よって, /をエスケープする必要はありません.

区切り記号の変更(2)

my $str = '/usr/local/bin/perl';
$str =~ s|/usr/local/bin/|/usr/bin/|;
  • 置換の場合, このようにできます.

正規表現のオプション


繰り返してマッチ(g)

my $str = 'Hello, hoge! Hello, fuga!';
my @name = ($str =~ /Hello, (\w+?)!/g);
# @name = ('hoge', 'fuga'); となる.
  • gは, 正規表現のマッチングを繰り返し行います.
  • また, 正規表現に()が含まれる場合, マッチした文字列のうち()の中に含まれる文字列をリストとして返します.

繰り返してマッチ(g)

my $str = 'Hello, hoge! Hello, fuga!';
my $str =~ s/Hello/Good morning/g;
  • 置換の部分で説明したように, s///gとすると, 置換の処理を繰り返し行なってくれます.

大文字/小文字を区別しない(i)

my $str = 'John and Beth';
if ($str =~ /john/i) {
    print "match!\n";
}
  • iは, 正規表現中のアルファベットの大文字・小文字を区別せずにマッチングを行います.
  • よって, /john/iは, johnはもちろん, JohnJOHN, jOhNなどにもマッチします.

練習問題 (1/3)

while (chomp(my $input = <STDIN>)) {
    ...
}
  • 上記のコードは, 標準入力から入力された文字列を, ひたすら$inputに代入するコードである.
  • このコードの...の部分を, 次の条件を満たすように書き換えてみよう.
  • この問題のコードは, while_input.plという名前で保存するようにしよう.

練習問題 (2/3)

  • 文字列が0の場合, ループを抜ける(lastを使って...).
  • 文字列がperlないしPerlを含む場合, 「Find Perl!」と表示する.
  • 文字列に大文字小文字問わず, pythonの文字列が含まれる場合, 「Find Python!」と表示する.
  • 文字列にperlないしrubyないしpythonが含まれる場合, 「Love Programming!」と表示する.

練習問題 (3/3)

  • 文字列の先頭にpapixがある場合, 「Find papix!」と表示する.
  • 文字列にHelloが含まれる場合, その後に続く単語xxxxを使って「Hello! xxxx!」と表示する.
    • 例えば, 文字列に「Hello papix」が含まれる場合, 「Hello! papix!」と表示すればOKです.

質問タイム


お疲れさまでした