読者です 読者をやめる 読者になる 読者になる

masatoz’s blog

プログラミングのメモ、日常の記録

高速ソート手法のシュワルツ変換はわりと簡単に実装できることがわかった

「すぐわかる Perlオブジェクト指向」からシュワルツ変換を取り上げます。

シュワルツ変換は、ソートを高速化するための方法です。

ソート対象の文字列の長さを最初に調べてインデックスをつくり、ハッシュの値にします。そのハッシュのキーにはソートする文字列そのものを入れます。

それでインデックスを比較した結果に応じてハッシュのキーを取り出すことでソートすることができます。 こうすることで、たぶん、文字列の長さをしらべる処理回数を減らすことができるため速度が上がるのだと思います。

数MB程度以上のテキストをソートする際に威力を発揮するようです。

いきなり完成コード

mapを使うと短く書けます。また玄人っぽい雰囲気がでます。

慣れないと読みづらいのが難点かも。

    my @sorted = map { 
        $_->[0], "\n" }  # 文字列を取り出す
        sort { $b->[1] <=> $a->[1] }  # 文字列の長さを比較
        map  [$_, length($_)], @lines; # 文字列とその長さのリストをつくる
    }

違う書き方

mapをforに置き換えてみます。

my @sorted = ();
for (@lines) {
    push(@sorted, [$_, length($_)]);
}
@sorted = sort { $b->[1] <=> $a->[1] } @sorted;

my @sorted2 = ();
for (@sorted) {
    push(@sorted2, $_->[0])
}

print map { $_,"\n" } @sorted2;

こんなんだっけ…ものすごくごちゃごちゃになってしまいました。

やっぱりmapを使った方がよいかもしれない!

Tie::Fileでテキストファイルをもっとかんたんに配列のように扱う

テキストファイルを全部読み込んで(または読込ながら)、文字列に処理を加える機会は多いと思います。

そんなとき、普通にテキストファイルを読みこむこともできるのですが、Tie::Fileモジュールを使うともっと簡単でした。

これもまたPerlクックブックで発見しました(ボリューム1)。

ちなみにTie::FileモジュールはPerlバージョン5.8以降で標準モジュールとして組み込まれています。

my @lines = ();
my $file_name = 'f1.txt';

tie @lines, "Tie::File", $file_name or die "Can't tie to $file_name: $!\n";

# 逆順にしてみる
@lines = reverse @lines;

# 特定の文字列を含む行をろ過
my @filtered = grep { /perl/i } @lines;
print "Found: @filtered\n";

# 行数(配列の要素数)
print "Line count: $#lines";

1行でテキストファイルを変数に入れられるですごいラク。

つまづきポイント

use strictしている場合、ファイルを読み込むときにTie::Fileをダブルクオートで囲む必要がありました。

Perlクックブックのサンプルコードでは、裸で使用されていたので、そのとおりやってみたら、Bareword "Tie::File" not allowed while "strict subs"というエラーをもらいました。

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();
    }
}