masatoの日記

やっていきます

Perlの組み込みモジュールText::Diffで差分をとってみる

シンプルなDiffツールのひとつとして

職場ではWindowsを使っていて、普段DiffをとるときはGUIなAraxis Mergeを使っています。1文字単位で差分を色分けして表示してくれるので、わかりやすく便利です。 とはいえ有料ソフトなので、だれでもが使えるわけではないのです。

かんたんに差分を確認したいだけであれば、色分け表示とかなくても大丈夫でしょう。

そんなとき、Perlクックブック Vol.1をぱらぱら見ていたら、ずばりText::Diffというモジュールが見つかりました。

このモジュールの使い方と出力のサンプルをのせてみます。

コード

file1

吾輩わがはいは猫である。名前はまだ無い。
 どこで生れたかとんと見当けんとうがつかぬ。
何でも薄暗いじめじめした所でミャオミャオないていた事だけは記憶している。

file2

吾輩わがはいは猫である。名前はまだ無い。
 どこで生れたかとんと見当けんとうがつかぬ。
何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。

鳴き声を変えてみました。

スクリプト

use Text::Diff;

#比較するファイル2つを指定
my $file1 = 'f1.txt';
my $file2 = 'f2.txt';

#引数にファイルを渡すだけ
my $diff = diff($file1, $file2);
print $diff;

簡単です。

結果は↓

--- f1.txt      Thu May  4 15:57:19 2017
+++ f2.txt      Thu May  4 15:57:26 2017
@@ -1,3 +1,3 @@
 吾輩わがはいは猫である。名前はまだ無い。
  どこで生れたかとんと見当けんとうがつかぬ。
-何でも薄暗いじめじめした所でミャオミャオないていた事だけは記憶している。
\ No newline at end of file
+何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
\ No newline at end of file

なんかよくわからない\ No newline at end of fileというメッセージが出てきてしまっていますが。

感想

1行ずつ比較されるようです。画面上で行の途中で折り返しされるほど1行の文字数が多い場合、どこが差分なのか探すのが大変な感じがします。

そのため、Text::Diffで差分をみるまえに、センテンス毎に改行を入れてやった方がみやすくなりそうです。つまり日本語であれば読点、英文であればピリオドで改行を入れてやる、と。

差分のみを出力する方法があれば、もっと便利だなと思います。

環境

Perl v5.18.2

Perl サブルーチンのレファレンスをつくる

テーマ

サブルーチンをレファレンス化して実行してみる。

「わかりやすい Perlオブジェクト指向」第5章より。

サンプルコード

my $codeRef = \&daikeiNoMenseki;
my $menseki = $codeRef->(200,40,500);
print "Area measurement: $menseki\n";

sub daikeiNoMenseki {
    my ($joutei, $katei, $takasa) = @_;

    if ($joutei < 0 || $katei < 0 || $takasa < 0 ) {
        warn "argument must be greater than 0!\n";
        return -1;
    }

    my $menseki = ($joutei + $katei) * $takasa / 2;
    return $menseki;
}

ポイント

  1. 配列レファレンスやハッシュレファレンスと同様に\を付けることでレファレンスがつくれる。

&daikeiNoMenseki\&daikeiNoMensekiとする。

2.サブルーチンレファレンスでも矢印記法でデリファレンスできる。

$codeRef->(200,40,500)

VBA Dir関数でフォルダ内のファイルを取得する

よくやる処理だけど、全然おぼえられないので写経する。

Office TANAKA - Excel VBAファイルの操作[ファイルの一覧を取得する]

Sub GetFiles()
    Dim buf As String
    Dim c As Long: c = 0
    Dim fs() As String

    'hoge以下にあるファイル一覧を取得したい
    Const Path As String = "D:\buf\"
    buf = Dir(Path & "*")
    Do While buf <> ""
        ReDim Preserve fs(c) As String
        fs(c) = buf
        c = c + 1
        buf = Dir()
    Loop

    Dim i
    For i = LBound(fs) To UBound(fs)
        Debug.Print fs(i)
    Next
End Sub

Perlで2次元ハッシュをつくる

ハッシュの中にハッシュを入れて2次元データ構造をつくってみる。

今回はハッシュのハッシュレファレンスにしてみた。

「すぐわかる Perlオブジェクト指向」(123ページ)

サンプルコード

use strict;
use warnings;
use DDP;

my $hash_ref_with_hash;
my $category;

while  (<DATA>) {
    chomp;
    if ( /^\[(.*)\]/ ) {
        $category = $1;
    } elsif (/^(.*)=(.*)$/) {
        $hash_ref_with_hash->{$category}{$1} = $2;
    }
}

p $hash_ref_with_hash;

__DATA__
[File]
perl_root=C:\Perl
tmp=C:\tmp
file1=file1.txt
file2=file2.txt
file3=file3.txt
[Internet]
web=http://www.gihyo.co.jp/
ftp=ftp://www.gihyo.co.jp/

出力

p $hash_refの出力。

\ {
    File       {
        file1       "file1.txt",
        file2       "file2.txt",
        file3       "file3.txt",
        perl_root   "C:\Perl",
        tmp         "C:\tmp"
    },
    Internet   {
        ftp   "ftp://www.gihyo.co.jp/",
        web   "http://www.gihyo.co.jp/"
    }
}

つまずきポイント

1.ハッシュレフにハッシュを入れる方法は、$hash_ref->{key} = $valueとする。 $hash_ref->{ $key => $value}とすることはできない。

2.キャプチャ箇所の取得方法 if ($line = /^\[(.*)\]/) { $captcha = $1 }とすると簡潔になる。

それ以前はいったん置換してから、その結果を新しい変数に代入するという冗長なことをやっていた。

こんな風に。

if ($line =~ /^\[/) {
    $line = s/^\[(.*)\]/$1/;
    $captcha = $1;
}

展開する方法

keys %{$hash_ref_with_hash->{$category}}の部分がポイント。

見た目が仰々しい。

## プレーンハッシュのハッシュレファレンスを展開する
for $category (keys %$hash_ref_with_hash) {
    for my $key (keys %{$hash_ref_with_hash->{$category}}) {
        print "[$category]\n";
        print "    $key    $hash_ref_with_hash->{$category}{$key}\n";
    }
}

Googleフォームから自動返信メールを送る

サンプルコード

コードだけアップ。

メール本文のテンプレートをスプレッドシートから取得している。

スクリプトにハードコーディングすると変更しづらいのでこのほうがよいかと思う。

function sendMailFromForm(e) {
  
    // 件名、本文
    var subject = "受付完了"; 

    //スプレッドシートから返信メール本文を取得する
    var body = getBody();
    Logger.log(body);
    
    // 列名の指定
    var MAIL_COL_NAME = 'メールアドレス';

    // メール送信者
    var admin = "sample@hoge.com";
    var cc = "";        // Cc:
    var bcc = admin;    // Bcc:
    var reply = admin; // Reply-To:
    var to = "";       // To: 

    // 送信先オプション
    var options = {};
    if ( cc ) options.cc = cc;
    if ( bcc ) options.bcc = bcc; //bccで確認メールを自分宛に送ることが可能
    if ( reply ) options.replyTo = reply; //返信先メールアドレス

    try{
        // スプレッドシートの操作
        var sh = SpreadsheetApp.getActiveSheet(); //スプレッドシートをshに入れる
        var new_row = sh.getLastRow(); //シートの最終行取得する
        var col = sh.getLastColumn(); //右端の列を取得する
        var r = sh.getDataRange();

        // メール件名・本文作成と送信先メールアドレス取得
        for (var j = 1; j <= col; j++ ) {
            var col_name = r.getCell(1, j).getValue();        // 列名を取得。列見出しは1行目にあるのでgetCell(行数、列数)の行数=1で固定
            var cell_value = r.getCell(new_row, j).getValue(); // 一番下の行のセル値、すなわち最新のフォームデータの値を取得
            
            if ( col_name === MAIL_COL_NAME ) {
              to = cell_value;
            }
        }

        // メール送信
        MailApp.sendEmail(to, subject, body, options);
    } 
    catch(e) {
        MailApp.sendEmail(admin, "Error:自動返信メール送信エラー(Googleフォーム): ", e.message);
    } 
}

//「返信」シートA1セルの中身を取得する関数
function getBody() {
    var bk = SpreadsheetApp.getActiveSpreadsheet();
    var sh = bk.getSheetByName("body");
    if(sh != null) {
        return sh.getRange("A1").getValue();
    }
}  

正規表現で名前付きキャプチャを使う

正規表現パターンがある程度ややこしくなると、後方参照でキャプチャの順番を書くのが大変になります。

そこで番号ではなくて名前でキャプチャを指定できるという名前付きキャプチャを試してみました。

Perlでは(?<name>)とするとキャプチャに名前が付きます。
名前付きキャプチャで補足した結果は%+というハッシュに記憶されるようです。

サンプル。

my $str = '0123 The message: "The weather today is great" is displayed.';

my $p = '\A(?<no>\d+).*?"(?<quoted>.*?)".*\Z';

$str =~ /$p/;

# 名前付きキャプチャは$+{name}で参照できる
my $index = $+{no}; # 0123
my $quoted = $+{quoted}; # The weather today is great

このときの%+をダンプしてみるとこんなふうになってました。

use Data::printer;
p %+;

普通のハッシュです。

Tie::Hash::NamedCaptureというものがあるようです。

{
    no       "0123",
    quoted   "The weather today is great"
} (tied to Tie::Hash::NamedCapture)

感想

キャプチャした箇所をハッシュにまとめられるので、管理しやすそうだと思いました。

grepを使ってリストにマッチする要素が含まれるかを判定する

ループの代わりにgrepすると簡潔に書ける。

人の名前がはいったリストがあるとして、その中にFredさんがいるかどうかを確かめたい。
forループならこう書ける。

for my $person ( @people ) {
    next unless $person =~ /Fred/;
    $flag = 1;
    last;
}

grepを使うと1行だ。

my $ret = grep { /Fred/ } @people;

結果のプリントを含めたサンプルコード。

use strict;
use warnings;
use 5.0100001;

my @people = qw/ Jeff Masato Koji /;

## forループを使った場合
my $flag;
for my $person ( @people ) {
    next unless $person =~ /Fred/;
    $flag = 1;
    last;
}

if ($flag) {
    say $flag;
} else {
    say "Fred doesn't seem to be here."
}

## grep演算子を使った場合
my $ret = grep { /Fred/ } @people;

if ($ret) {
    say "Hi, Fred.";
} else {
    say "Fred isn't here.";
}