sironekotoroの日記

Perl で楽をしたい

Mojolicious::Lite でファイルをアップロード

いつもの Mojolicious::Lite です。

プロトタイピング

今回は、自分が欲しいなーと思っているもの、作りたいなーと漠然と思っているものを雑に作ってみます。

雑なので大穴がそこかしこに。

  • 同名のファイルは問答無用で上書き
  • アプリを再起動すると、アップロードしたファイルの履歴が飛ぶが、ファイルは残り続ける

ううーん、大穴。

ただ、「欲しいもの」をはっきりさせるとき、手間がかかりそうなのはどこか、何が足りないのか、別のモジュールに切り出せないのか、Perl だけじゃ無理で Javascript 使わないとダメなのはどこか?とか、そういう感覚を得たいんですね。

欲しいものがはっきりすると、実はすでに独立したアプリになってたりする・・・なんてこともあります。

作らずに済むのは良いですが、せっかくなので、それを模倣してみたりとか。楽しいですよね。

こんな感じのディレクトリ構成です。 public/ の中のファイル はテストで上げてみたもの。アップロードしたファイルはこの public フォルダの中に入ります。

$ tree
.
├── myapp.pl
└── public
    ├── temp_cp932.pl
    ├── 初級テキスト・問題集 解答・解説.pdf
    └── Mac_MaxCapacity.txt
#!/usr/bin/env perl
use Mojolicious::Lite;

# ファイルの保存先フォルダがなかったら作成する
# Mojolicious::Liteの公開フォルダはデフォルトでは
# スクリプトのあるフォルダの中にある public
my $save_folder = 'public';
unless ( -d $save_folder ) {
    mkdir $save_folder;
}

# アップロードしたファイルの情報を格納する配列リファレンス
my $upload_files = [];

get '/' => sub {
    my $c = shift;
    $c->stash( upload_files => $upload_files );

    $c->render( template => 'index' );
};

post '/' => sub {
    my $c = shift;

    # form内のファイルのところから情報を持ってくる
    my $file = $c->param('file_field');

    # ファイル移動先のパスを作る
    my $path = join( "/", $save_folder, $file->filename );

    # ファイルを移動する
    $file->move_to($path);

    my $localtime = localtime;

    # ファイル名などを保存する
    push @{$upload_files},
        {
        filename  => $file->filename,
        size      => $file->size,
        localtime => $localtime,
        };

    # 表示はgetにお任せ
    $c->redirect_to('/');
};

app->start;
__DATA__

@@ index.html.ep
% layout 'default';
% title 'Welcome';
<h1>Welcome to the Mojolicious real-time web framework!</h1>

<form action="/" method="POST" enctype="multipart/form-data">
    <ul>
        <li>ファイル:<input type="file" name="file_field" required></input>
    </ul>
    <input type="submit"></input>
</form>

<table border="1">
  <tr>
    <th>DL</th>
    <th>filename</th>
    <th>size</th>
    <th>localtime</th>
  </tr>
  <% for my $file ( @{$upload_files} ){ %>
    <tr>
      <td><a href="<%= $file->{filename} %>">ダウンロード</a> </td>
      <td><%= $file->{filename} %></td>
      <td><%= $file->{size} %></td>
      <td><%= $file->{localtime} %></td>
    </tr>
  <% } %>

</table>

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head><title><%= title %></title></head>
  <body><%= content %></body>
</html>

追記

まぁ、TODO 書くだけならね(できるとは言っていない

f:id:sironekotoro:20200524173910p:plain

追記2

やりたいことが整理された結果、Google Drive 中心にいろいろやれば Mojolicious 使わなくて良さそう感が出てきた

Docker上でPerlを動かして、ホストにあるファイルを実行する

Perl インストールしないで Perl 実行するにはどうするんがいいのかなー?的に思って作った環境。

Perl 公式の Docker イメージ使ってみたけど、結構サイズが大きかった。

初回だけDockerイメージのダウンロードに時間がかかります。

hub.docker.com

$ docker images perl
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
perl                5.30.2              a8b6deb2e511        12 days ago         857MB
perl                latest              a8b6deb2e511        12 days ago         857MB

中に入って確認するとdebianみたいっすね。

root@6210f71a6f0d:~# cat /etc/debian_version
10.3

ファイルの配置はこんな感じで。

$ tree
.
├── docker-compose.yml
└── hello.pl

0 directories, 2 files

hello.pl

#!/usr/bin/env perl
use strict;
use warnings;

print "Hello, World\n";

docker-compose.yml

version: '3'
services:
  perl:
    image: "perl:5.30.2"
    volumes:
     - ".:/usr/src/myapp"
    working_dir: "/usr/src/myapp"
    entrypoint:
     - "/usr/local/bin/perl"

こうやって実行。docker-compose.yml と、動かしたいスクリプトは同じところ置く。

$ docker-compose run perl hello.pl
Hello, World

Dockerfileでやってみたけど、いざコマンドラインから実行するときに引数が長くなってしまうのが嫌で(個人の感想です)、docker-compose.yml にまとめましたとさ。

ちなみに、と言うか、数ヶ月後の自分が勘違いしそうなので書いておくと、

$ docker-compose run perl hello.pl

ここの perl ってのは、Dockerコンテナ内の /usr/bin/local/perl ではなくて、docker-compose.yml 内にある services 名としての perl なので気をつける。

紛らわしい名前付はよくないですねー

Perlでディレクトリの中のファイルにアクセスする

早速やっていきます。

いつも通り、コードと対象のディレクトリは同じところに置きます。ディレクトリとファイルの位置はこんな感じ。

.
├── read_dir.pl         これから書くスクリプト
└── test_dir             ディレクトリ
    ├── test_file1.txt   ディレクトリの中のファイル その1
    └── test_file2.txt    ディレクトリの中のファイル その2
  • test_file1.txt の中身は hoge と1行だけ
  • test_file2.txt の中身は fuga と1行だけ

Perlディレクトリの中のファイルにアクセスする方法として、ディレクトリハンドルを使う方法と、ファイルグロブを使う方法を書いていきます。

その1 ディレクトリハンドル

ファイルの中身を読んだり書き込んだりには ファイルハンドル を用いました。ディレクトリを扱うには ディレクトリハンドル を使います。

#!/usr/bin/env perl
use strict;
use warnings;

my $directory_name = 'test_dir';    # ディレクトリ名

# ディレクトリハンドルの略称で $DH とした。
# ディレクトリ開けなかったらエラーを出して死ぬ(die)
opendir my $DH, $directory_name or die; 

for my $content ( readdir $DH ) {
    print $content . "\n";
}
closedir $DH;

コードの解説していきます。

opendir my $DH, $directory_path or die; 
# 中略
closedir $DH;

ファイルを開くときは open を用いましたが、ディレクトリのときには opendir を使います。ディレクトリの処理が終わった後にはclosedir で閉じます。

ディレクトリハンドル $DH が無事作成されると、この $DH の中にディレクトリの中の情報が入ります。

ついでに説明ですが、末尾に or die をつけていることで、ディレクトリが開けなかったらスクリプトが終了するようにしています。

これはよくある書き方、定型、お約束みたいなものですね。

for my $content ( readdir $DH ) {

次に、 readdir ですが、これは $DH の中身を1つ取り出す関数です。これが for 文の中にあるので、 $DH の中身を一通り吐き出して終了、というわけです。

実行結果は以下の通りです。

.
..
test_file2.txt
test_file1.txt

ファイル以外にも表示されているものがあります。 ... です。

これはPerlが動いている環境( macOS や msys2 , Linux )において特別な意味を持つものです。

ターミナルで

$ ls .

$ ls .. 

と入力してみると分かりやすいと思います。

ディレクトリのファイル一覧をとるときにはこの ... を除外する書き方をすることも多いです。

... の時は for のループを飛ばす、という処理です。こんな感じ。

for my $content ( readdir $DH ) {
    if ($content eq '.' or $content eq '..' ){
        next
    }
    print $content . "\n";
}

もう少しスマートに、後置if文と正規表現を使うとこんな感じ。正規表現覚えてますか〜?

for my $content ( readdir $DH ) {
    next if $content =~ /^\.{1,2}$/;
    print $content . "\n";
}
closedir $DH;

これで ... の場合には next で次のループに行くので表示される事がなくなります。

その2 ファイルグロブ

ディレクトリにアクセスするもう一つの方法が、 ファイルグロブ を使う方法です。

#!/usr/bin/env perl
use strict;
use warnings;

my $files_path = 'test_dir/*';  # ファイルのパスをワイルドカードで指定している

my @contents       = glob($files_path);  # glob ファイルグロブを使っている 

for my $content (@contents) {
    print $content . "\n";
}

実行結果

test_dir/test_file1.txt
test_dir/test_file2.txt

さっきのディレクトリハンドルを用いる方法とちょっと書き方や結果が異なります。

ただ、ディレクトリハンドルよりも直感的に書けたりするのでこちらが使われている場合も多い・・・気もします(個人の感想です)。

glob ですが、これは引数のパスに該当するファイル名のリストを取得します。このとき、ワイルドカードや拡張子の指定をついでに行う事ができます。

ディレクトリハンドルはディレクトリ名を指定しましたが、glob の場合はそのディレクトリの中身を指すパス になります。

ワイルドカードを使って、特定の拡張子のファイルのみを指定する事が可能です。

例えばこんな感じ。

my $files_path = 'test_dir/*.csv';

もちろん、ディレクトリハンドルでも同様のことは可能ですが、条件が単純な時はファイルロブを使った方が楽でしょう。

また、実行結果でもわかるようにパスの組み立ても可能です。これも条件が単純なときには便利です。

ディレクトリの中のファイルの中身を表示してみる

ディレクトリの中のファイルの中身を見たい時のスクリプトを例に、ディレクトリハンドル を用いたものと、ファイルグロブを使ったもの、と2つ書いてみました。

ディレクトリハンドルの中にあるのはファイル名のみなので、ファイルの中身にアクセスするときには ディレクトリ名/ファイル名 というようにパスを作ってあげる必要があります。

ディレクトリハンドル編

#!/usr/bin/env perl
use strict;
use warnings;

my $directory_name = 'test_dir';

opendir my $DH, $directory_name or die;

for my $content ( readdir $DH ) {
    next if $content =~ /^\.{1,2}$/;

    # ファイルのパスを作成する
    my $content_path = $directory_name . '/' . $content;

    # ファイルのパスからファイルハンドルを作成して中身を表示する
    open my $FH, '<', $content_path;
    for my $line (<$FH>) {
        print $line , "\n";
    }
    close $FH;

}
closedir $DH;

実行結果

fuga
hoge

ファイルグロブ編

ディレクトリハンドルより短くかけます。

#!/usr/bin/env perl
use strict;
use warnings;

my $directory_path = 'test_dir/*';

my @contents = glob($directory_path);

for my $content (@contents) {

    open my $FH, '<', $content;
    for my $line (<$FH>) {
        print $line . "\n";
    }
    close $FH;

}

実行結果

fuga
hoge

ということで、これで Perl でファイルやディレクトリを扱う最低限のスクリプトの紹介はおしまいです。

Perl には File::Find とか Path::Tiny とか File::SpecCwd などのファイル・ディレクトリを扱う便利モジュールも数多くあります。

何かしようとして、うまくいかないとか、こういうことをがしたい!という方は Perl入学式の slack などで質問くださいませ。

docs.google.com

長いおまけ: PerlWindows上のファイル名を変更する

中には、Windows で msys2 などの環境を使わずに Perl を利用する人もいるかもしれません。

6年くらい前のうちみたいに・・・

そのとき、Perl から Windows 上の日本語ファイル名を扱えずに困り果てる・・・という事がありました。

試しに、ファイル名に特定の文字列をつけるスクリプトを解説付きで書いてみました。

Windows10 + ActivePerl 5.28 で動かしてます。

変数の中の文字列が cp932 なのか Perlで扱う utf8 なのかを変数名の先頭ににつけてみました。

・・・が、残念なことに分かりやすさに直結しなかったなぁという。

苦労したんだけどー

ディレクトリ構造です。

├── rename_file_win.pl         これから書くスクリプト
└── 日本語ディレクトリ             ディレクトリ
    ├── 日本語ファイル1.txt   ディレクトリの中のファイル その1
    └── 日本語ファイル2.txt    ディレクトリの中のファイル その2

Windows 上で日本語のファイル名を扱うときの注意点は、文字コードです。

これが基本です。

use strict;
use warnings;
use Encode qw/decode encode/;
use utf8;

my $utf8_dir_name = '日本語ディレクトリ'; # これはutf8のソースコード上に書かれた文字

# ディレクトリ名が書かれているのは utf8 上のソースコード
# この utf8 で記されたディレクトリ名を Windows(cp932) が解釈できるよう変換する

# スクリプトから見て「内->外」なのでエンコードする
my $cp932_dir_name = encode('cp932', $utf8_dir_name); 

opendir my $DH, $cp932_dir_name or die;    # Windows上のディレクトリを開く

    for my $cp932_content (readdir $DH){
        # カレントディレクトリと上位ディレクトリは飛ばす
        next if $cp932_content =~ /^\.{1,2}$/;

        # $cp932_content は readdir が持ってきた Windows(cp932) のディレクトリ内のファイル名。
        # これを Perl が解釈できるように変換する
        # スクリプトから見て「外->内」なのでデコードする
        my $utf8_content = decode('cp932', $cp932_content);

        # ファイル名の先頭に文字列を追加してみる
        # 追加する文字列。utf8 上のソースコードに書かれているのでこの文字列は utf8
        my $utf8_add_string = '(変更済み)';

        # 新しいファイル名の変数を用意
        my $utf8_new_filename = $utf8_add_string . $utf8_content;

        # ファイル名を変更するために、元のファイル名と新しいファイル名それぞれに親ディレクトリをつけたパスを用意する
        # File::Spec を用いることで、OSごとのディレクトリとファイルの区切り文字をよしなに変換してくれる
        # ここで変換するのはあくまで区切り文字のみ。文字コードの変換ではない。
        use File::Spec;
        my $utf8_old_file_path = File::Spec->catfile($utf8_dir_name, $utf8_content);
        my $utf8_new_file_path = File::Spec->catfile($utf8_dir_name, $utf8_new_filename);

        # Windows上で rename するので Windows(cp932) に変換する。
        # スクリプトから見て「内->外」なのでエンコードする
        my $cp932_old_file_path = encode('cp932', $utf8_old_file_path);
        my $cp932_new_file_path = encode('cp932', $utf8_new_file_path);

        # rename 関数を使ってファイル名を変更する
        rename $cp932_old_file_path, $cp932_new_file_path or die;

        # 変換状況を表示する
        print "$cp932_old_file_path => $cp932_new_file_path\n";

    }

closedir $DH;

Mac に Parallels で Windows10 を入れ、その上で VPN 接続して弥生会計を使いたい!

MacParallels で Windows10 を入れ、その上で VPN 接続して弥生会計を使いたい!

そういう人も日本に3人くらいいるかもしれない、という感じで書いてみます。中身は薄いです。

弥生会計とは・・・?

シェア6割を誇ると言われる企業用の会計ソフトです。

こんな感じです。

f:id:sironekotoro:20200429133456p:plain

複式簿記で入力していきます。

簿記では最終的な貸借対照表(BS)や損益計算書(PL)に至るまで、途中途中で精算表を作っていきます。

が、会計ソフトだとそういった帳票の転記もやってくれるので、とても楽です。

というか、会計ソフトがない頃の経理業務を想像すると地獄みしかないですね・・・

なぜ Parallels なのか

弥生会計Mac 対応しておらず、Windows 用のクライアントソフトしかないからです。

一応、自己責任で Parallels 経由で利用できますよ、と公式ページにもあります。

support.yayoi-kk.co.jp

Mac の他に別途 Windows ノートを用意するということも考えましたが、リモートワークの昨今、ノートPCを2つも持って歩きたくない・・・という怠惰な理由です。

あと「やってみたかった」という純粋な興味。

作業環境

Mac への Parallels インストールと Windows10 のインストール、その Windows10 上での弥生会計インストール、これはうまくいきました。全くトラブルなし。

会社で利用している弥生会計は、大元のデータを Microsoft SQL Server に保存しています。

社内ネットワークに接続し、サーバへ接続してデータ編集、これも問題なくできました。

詰まったところ

が、VPN接続環境下ではサーバにアクセス出来ない現象が発生しました。

弥生会計の認証は通るものの、そのあとのサーバへの接続のところで失敗します。

VPNmacOS 側で接続した場合も Windows10 から接続した場合も接続は可能です。

また、同サーバにある共有フォルダ(SMB)にはアクセスが可能(サーバー名、IPアドレスとも)なことから、弥生会計の設定や何かに問題があるのでは?と推測しました。

ただ、素の Windows10 から利用していないので、もう一つの方法として MacSSD からパーティションを分けて BootCamp に Windows10 をインストールし、そこに弥生会計をインストール、VPNの設定を作って接続してみました。

Parallels 経由ではない、素の Windows10 環境です。・・・が、結果は変わらず。

ううんー???

となったところで、インフラ担当さんより、サーバを名前ではなくIPで指定してみたら?とのアドバイス

共有フォルダ(SMB)だと名前で解決できたけど、そうか、弥生の名前解決の仕組みは別か・・・!

こんな感じ(サーバー名とかIPアドレスは適当に変更してます)

  • 元の設定: KEIRI\YAYOI
  • 変更した設定: 192.168.1.100\YAYOI

でアクセスしたところ、無事データの取得に成功しました。さすがインフラ担当、頼りになる!

ということで、色々と試行錯誤はしましたが、 MacParallels 入れて Windows10 をインストールし、その Windows10 内の弥生会計からVPN経由で会社サーバーへの接続は可能だった!ということで記事を締めさせていただきますありがとうございました。

最近のお仕事 2020年04月

色々あって、3月の終わり頃から会社の経理業務を手伝っています。

経理業務の経験はないのですが、履歴書に簿記2級って書いてたのを覚えてくれていた人がいたようです。

既存サービスの保守とかアップデートをしつつ、経理の世界に足を突っ込んでいるような状態です。

それに伴い、触れる環境が随分変わりました。

こんな感じです。

前任者からの急ぎの引き継ぎで、Excelベースの便利シートなどがあるんですが、よくわからなかったりして「超古代の進んだ文明を理解できない原始人類」みたいな気分を日々味わってます。

とはいえ、割と楽しくお仕事はしており、これは周りの同僚さんのサポートあればこそです。ありがたいです。

弥生会計については触るのも初めてですが、体験版のついた参考書を買って練習したりしています。商業高校の生徒さんはこういうのやってるのか・・・負けてるわ・・・ってなりながら勉強しております。

www.jikkyo.co.jp

そういえば、今年は Go でちょこっとお手伝いしたり、 PHP で画像合成ツール改修したりとお仕事 Perl 書いてないっすね。経理業務では隙あらば Perl で業務を楽にしていきたいです。

Mac英語キーボード(US)と Windows10 のかな漢字変換キーバインドを合わせる

下記のブログさまさまです・・・。ありがたいありがたい。

www.shujima.work

Perlでファイルの文字コードを指定して読み込み&書き込み

一昨日の金曜日、ムキー!ってなったことを、落ち着いて週末に解決するべく色々やった記録です。

まず、このGoogle Spread Sheet 開いてください。普通に開けると思います。

docs.google.com

はい。タイトルで察していただけると思うんですが、そういうことです。

では、画面左上のタイトルの下にある [ファイル] メニューからダウンロードを選び、[カンマ区切りの値 csv] を選びます。

ダウンロードのダイアログが出るので、一度どっかに mojibake.csv ってファイル名で保存してください。

保存したcsvファイルですが、VSCode や SublimeText で普通に開き、中身を確認することができます。Windows のメモ帳(notepad.exe)や macOS のテキストエディットでも文字化けは起こりません。

ではこれを Office365 の Excel で開いてみます。

見事に文字化けです!

f:id:sironekotoro:20200425150657p:plain

Office365 は macOS でも利用できるのですが、同様に文字化けします。

憎いですね。許しがたいですね。

このような状況はよくあります。

これは「文字コード」の違いが原因です。文字コードについては色々大変なのでここでは説明しません。

説明しきる力量がないともいいます・・・

という差異が文字化けを生んだわけです。

Excel(Office365) で utf8 のcsvファイルを文字化けさせずに読み込む

もちろん、Excel なので(信頼感)こういったファイルをちゃんと取り込む術があります。

まず、csvファイルを 読み込まずExcel を開き、メニューから [データ] を選択、その中の [テキストファイル] を選びます。画像の左から2つ目のアイコンのところです。

f:id:sironekotoro:20200426135231p:plain

ウィザードの画面が出ます。

f:id:sironekotoro:20200426135326p:plain

[区切り記号付き] を選び、[元のファイル] のプルダウンメニューから [Unicode (UTF-8)] を選びます。

f:id:sironekotoro:20200426135825p:plain

下のプレビューで見ると(字が細いですが)、正しく表示されていることが確認できます。

あとはなりなりに進んでいくと、無事にExcelのシートに取り込むことが可能です。

Perlでやる場合

・・・別にPerlでやらなくても良いのでは?導入が長いのでは?

わかります。うちもこのエントリをボツにしようかと思いましたもん。

でも、仮にこういったファイルが100個あったとして、一個一個心を込めてExcelのウィザードを起動するの・・・?

という心の声が聞こえてきたんで書きます。

想定環境は macOS 上かPerl入学式で作った msys2 上の環境です。mojibake.csv と同じ場所にスクリプトを作ります。

それと、今回扱うのはcsvファイルですが内容を変更しないので、csv関係のモジュールは使いません。

文字コード指定して読み込んで、文字コード指定して書き出すのみ、です。

単にファイルを指定して読むだけ

前回、前々回でやった範囲です。復習ですね。

#!/usr/bin/env perl
use strict;
use warnings;

my $input_file = 'mojibake.csv';

open my $FH, '<', $input_file;
for my $line (<$FH>) {
    chomp $line;
    print $line . "\n";
}
close $FH;

文字コードを指定して読み込んで表示する

上記のコードとの違い、わかりますでしょうか。

#!/usr/bin/env perl
use strict;
use warnings;

use Encode;    # 文字コードを扱う便利モジュール

my $input_file = 'mojibake.csv';

open my $FH, '<:encoding(utf8)', $input_file;   # utf8の文字コードのファイルを読み込む
for my $line (<$FH>) {
    chomp $line;
    print encode('utf8', $line) . "\n"; # 文字コードを指定して出力
}
close $FH;

Perl文字コードが異なるファイルや入出力に対応するときは Encode モジュールを使うのが鉄板です。標準モジュールなのでインストールは不要です。

use Encode;

ファイルを読み込むときの第二引数 < に、文字コード指定の :encoding(utf8) がついています。

open my $FH, '<:encoding(utf8)', $input_file;   # utf8の文字コードのファイルを読み込む

ファイルの文字コードがわかっている場合には、このように文字コードを指定して読み込みます。

そして、処理した結果を出力する時(今回は print でターミナルに出力)には、同じように文字コードを指定しています。

print encode('utf8', $line) . "\n";`  

mac と msys2 のターミナルの文字コードは utf8 なので、encode の utf8 で指定して print しています。

文字コードを指定してファイルに書き込む(utf8)

読み込んで書き込む、ということで、ファイルハンドルも2つ用意します。$ReadFH$WriteFH です。

書き込みの際のファイルハンドルは第2引数の不等号の向きが > とファイル名(またはファイル名が入っている変数)側を向いています。そして、文字コードの指定 :encoding(ctf8) もついています。

open my $WriteFH, '>:encoding(utf8)', $output_file;    # utf8の文字コードでファイルに書き込む

まずはutf8の文字コードで書き出してみます。

#!/usr/bin/env perl
use strict;
use warnings;

use Encode;    # 文字コードを扱う便利モジュール

# 読んだファイルの内容をためておく配列
my @lines;


# ファイル読み込み処理
my $input_file = 'mojibake.csv';

open my $ReadFH, '<:encoding(utf8)', $input_file;   # utf8の文字コードのファイルを読み込む
for my $line (<$ReadFH>) {
    chomp $line;
    push @lines, $line;
}
close $ReadFH;


# ファイル書き込み処理
my $output_file = 'utf8.csv';

open my $WriteFH, '>:encoding(utf8)', $output_file;    # utf8の文字コードでファイルに書き込む
for my $line (@lines) {
    print $WriteFH $line . "\n";
}
close $WriteFH;

文字コードを指定してファイルに書き込む(cp932)

次に、いよいよExcelが素で解釈してくれる文字コードcp932 で書き込みます。

と言っても、上記のコードの1行、書き込む際のファイルハンドルの文字コード指定 utf8cp932 に変えただけです。

コードはこちら。クリックすると展開されます

#!/usr/bin/env perl
use strict;
use warnings;

use Encode;    # 文字コードを扱う便利モジュール

# 読んだファイルの内容をためておく配列
my @lines;


# ファイル読み込み処理
my $input_file = 'mojibake.csv';

open my $ReadFH, '<:encoding(utf8)', $input_file;   # utf8の文字コードのファイルを読み込む
for my $line (<$ReadFH>) {
    chomp $line;
    push @lines, $line;
}
close $ReadFH;


# ファイル書き込み処理
my $output_file = 'cp932.csv';

open my $WriteFH, '>:encoding(cp932)', $output_file;   # cp932の文字コードでファイルに書き込む
for my $line (@lines) {
    print $WriteFH $line . "\n";
}
close $WriteFH;

これでやっと、Excelでそのまま開いて文字化けしないファイルができました。

Excelですんなりファイルを読み込み、文字化けもせず。素晴らしい!

f:id:sironekotoro:20200426150340p:plain

逆(cp932 -> utf8)もいけます

もちろん、Excel をはじめ Windows アプリが cp932 で出力した csv ファイルを読み取り、utf8 で書き出すことも可能です。

官公庁のcsvファイルなんかはだいたい WindowsExcel で作られているみたいなので、こちらの方が数は多いでしょうね。

例えば厚生労働省の人口動態データです。右上の [ダウンロード] ボタンからダウンロードしてみてください。

www.data.go.jp

これを文字コードを指定せず、Perlで読み込んで出力すると文字化けします。

f:id:sironekotoro:20200426153527p:plain

では、文字コードを指定して表示してみます。

#!/usr/bin/env perl
use strict;
use warnings;

use Encode;

my $input_file = 'mi010000.csv';

open my $FH,  '<:encoding(cp932)', $input_file;
for my $line (<$FH>){
    chomp $line;
    print encode('utf8',$line);
}
close $FH;

無事、文字化けせずに表示ができました。

f:id:sironekotoro:20200426155944p:plain

次回

1個のcsvファイルを処理するにはこれで良いと思うんですが、これが100個あったときにはファイル名を変えつつ実行しなくてはいけないのでは・・・?

心を込めて100回、ファイル名を変えるの?

・・・

ということで、次回はフォルダから中のファイルを拾う方法てのをやります。

おまけ

6年くらい前、Perl勉強してこ!ってときに使っていたのは Windows マシンだったのですが、Webから拾ってきた諸々が文字化けするのが解消できなかったんですね。

ですので、Macを買ったのでした。これぞ富豪的解決!・・・とはならず、今度は Windows から出力したファイルを mac で扱おうとして文字化けしてしまい、苦しみは尽きないのでした。

昨今、文字コードはアプリケーション側で吸収することも多く、文字コードの差異で苦しむのはプログラムを書くときくらいかも知れません。

Perlでcsvファイルを読み込んで 配列|ハッシュ にする

先週からの続きです

先週でテキストファイルの読み書きができるようになりました。

sironekotoro.hateblo.jp

ということで、今回は代表的なテキストファイルのデータとしてCSVの処理方法をやっていきます。

CSVファイル

Comma Separated Values の頭文字を取った名前の通り、カンマ区切のデータのことです。

1行にカンマ区切りで複数のデータのがあり、それが複数行ある・・・というです。

この1行のことをレコードと呼び、レコードの中のカンマで区切られたところをフィールドと呼びます。

Perl入学式のリファレンス回の練習問題からデータを持ってcsv風に書くとこんな感じです。

name,country,perl,python,ruby,php,binary
Alice,England,60,80,80,50,30
Bob,America,40,10,50,30,50

では、これを records.csv というテキストファイルにして、色々と処理をしていきます。

csvファイルを単に読むだけ

先週の復習です。csvファイルと今回作成するPerlスクリプトは同じ場所に置いて実行してください。

#!/usr/bin/env perl
use strict;
use warnings;

my $filename = 'records.csv';
open my $FH, '<', $filename;
for my $line (<$FH>){
    chomp $line;
    print $line . "\n";
}
close $FH;

csvを読み込んで配列にする

データが詰まっている1行はカンマ区切りなので、split で分割して配列に入れるだけです。

#!/usr/bin/env perl
use strict;
use warnings;

my $filename = 'records.csv';
open my $FH, '<', $filename;

for my $line (<$FH>) {
    chomp $line;
    my @field = split /,/, $line;   # カンマ区切りで配列に格納
    print "@field" . "\n";          # 配列をprint
}

close $FH;

ファイルの読み込みと、読み込んだデータの処理を分けて書くときとには配列のリファレンスを使うことになると思います。

こんな感じかなー。無名配列でもっと短くかけますが、そこは各自で・・・

#!/usr/bin/env perl
use strict;
use warnings;

my @records;    # 配列リファレンスを格納する配列

my $filename = 'records.csv';
open my $FH, '<', $filename;

for my $line (<$FH>) {
    chomp $line;
    my @field = split /,/, $line;    # カンマ区切りで配列に格納

    my $record = \@field;            # 配列リファレンスにする
    push @records, $record;   # 配列リファレンスを格納用の配列に入れる
}

close $FH;

# 結果表示
for my $record (@records) {
    my @record = @{$record};    # デリファレンスして配列に戻す
    print "@record" . "\n";     # デリファレンスしたものを表示
}

csvを読み込んで配列にする(モジュールを使う)

ここまで書いてきてなんですが、自分でパース(データを分けること)することはお勧めしません。

例えば、フィールドのデータ内に , があると、split はそこで分割してしまいます。

Perlcsvを扱うモジュールといえば Text::CSV_XS が定番です。Text::CSV_XS であれば、そのようなデータにもうまく対応してくれます。

(ただし、処理するファイルの前提はある)

また、スクリプトの上の方に use Text::CSV_XS; とあると、「あー、csvを何かするスクリプトなのね」と理解の一助になります。

なお、標準モジュールではないのでインストールが必要です。

$ cpanm Text::CSV_XS

perldoc.jp

利用例としてはこんな感じです。

#!/usr/bin/env perl
use strict;
use warnings;

use TEXT::CSV_XS;

my $csv = Text::CSV_XS->new();    # csvを扱う便利オブジェクト

my $filename = 'records.csv';
open my $FH, '<', $filename;
for my $line (<$FH>) {
    chomp $line;
    my $status = $csv->parse($line); # CSV文字列をパースしてフィールド群に切り分ける
                                     # $status は成否判定が入っている(今回は使わない)
    my @columns = $csv->fields();    # パースされたフィールド群を配列に入れる
    print "@columns" . "\n";
}
close $FH;

csvを読み込んでハッシュにする

こちらの使い方も多いと思います。

ハッシュにするにあたって考えるのは、keyvalue をどうするか?ということです。

今回はこんな感じのデータ構造を作ってみます。配列の中にハッシュリファレンスが入っています。

ハッシュなので順不同なのですが、見やすさ優先で key でソートしてます。

$VAR1 = [
          {
            'binary' => '30',
            'country' => 'England'
            'name' => 'Alice',
            'perl' => '60',
            'python' => '50',
            'ruby' => '80',
          },
          {
            'binary' => '50'
            'country' => 'America',
            'name' => 'Bob',
            'perl' => '40',
            'python' => '30',
            'ruby' => '50',
          }
        ];
#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;

my @records;    # ハッシュリファレンスを格納する配列

my $filename = 'records.csv';
open my $FH, '<', $filename;

for my $line (<$FH>) {
    chomp $line;
    next
        if $line
        =~ /^name/;  # nameから始まる項目名の行だったら飛ばす

    my @field = split /,/, $line;    # カンマ区切りで配列に格納

    # ハッシュのデータ構造にする
    my %record = (
        name    => $field[0],
        country => $field[1],
        perl    => $field[2],
        python  => $field[3],
        ruby    => $field[4],
        python  => $field[5],
        binary  => $field[6],
    );

    my $record = \%record;    # ハッシュリファレンスにする
    push @records,
        $record;   # ハッシュリファレンスを格納用の配列に入れる
}

close $FH;

print Dumper \@records;

csvを読み込んでハッシュにする(モジュールを使う)

ここまで長く書いてきてなんですが、ハッシュにするときもモジュールを使うのが楽でおすすめです。

metacpan.org

こちらも標準モジュールではないのでインストールが必要です。

$ cpanm Text::CSV::Simple

3行で収まっちゃった・・・

#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;

use Text::CSV::Simple;

my $filename = 'records.csv';

my $parser = Text::CSV::Simple->new;
$parser->field_map(qw/name country perl python ruby php binary/);
my @data = $parser->read_file($filename);

print Dumper \@data;

というわけで

駆け足ながらPerlでのcsvファイル処理について書いてみました。

誰向けの記事かというと、6年くらい前の自分向けの記事です。

Perlでテキストファイルの中身を表示する

色々書いたけど省略

  1. CSVファイルを処理する方法について書いておきたい
  2. そもそも、ファイルの入出力についてPerl入学式の現行カリキュラムでやってない
  3. しかもいい感じのCSVファイルのサンプル、Shift-JISフォーマットじゃん
  4. ・・・一歩ずつ、ファイル入出力からやっていこう

Perlでテキストファイルの中身を表示する

では早速ファイルを用意します・・・と思って適当なの探したんだけど、良い感じのデータがない・・・ので、プログラムファイル自身を表示する、というふうに方向転換します。

read_file.pl という名前のスクリプトを作り、実行してみます。

Perlでファイルを読み込む(手作業編)

#!/usr/bin/env perl
use strict;
use warnings;

my $filename = 'read_file.pl';    # ファイル名を指定する

open my $FH, '<', $filename;       # ファイルハンドルを宣言する

for my $line (<$FH>) {  # 行入力演算子で1行ずつ読み込む
    chomp $line;
    print $line . "\n";
}

close $FH;    # ファイルを閉じる

実行すると、スクリプト自身を表示します。

Perl入学式の現行カリキュラムではファイルの取り込みや書き込みは学習しませんので、ここで少し解説です。

Perlではファイルハンドルというものを使ってファイルの読み書きを行います。

ファイルハンドルは「ファイルを扱うもの」くらいに思っておいてください。直訳じゃん・・・

コードの中では $FH という変数がファイルハンドルです。

open という関数を使ってファイルハンドルを宣言してファイルを open し、使い終わったらファイルハンドルを close します。

open my $FH, '<', $filename;    # ファイルハンドルにファイルを読み込ませる

# 何らかの処理を書く

close $FH;                          # ファイルハンドルを終了する

これだけだと、ファイルハンドルを設定して終了しただけです。

この、 openclose の間に処理を書きます。今回書かれているのは・・・

for my $line (<$FH>){
    chomp $line;
    print $line . "\n";
}

おなじみのfor文です。 <$FH> 以外のところはPerl入学式の第2回でやったところです。こんな感じですね。

for my $line ( 0..9 ){
    chomp $line;
    print $line . "\n"; # 0 から 9 までを改行して表示する
}

では、この <$FH> はどういう意味でしょう・・・となる前にもう一つ、Perl入学式でやった標準入力を思い出してみます。

#!/usr/bin/env perl
use strict;
use warnings;

my $line = <STDIN>;
chomp $line;
print $line . "\n";

似ていますね、 <$FH><STDIN>

この <> は行入力演算子というもので、その名の通り、行単位で1行ずつ読み込んでくれるものです。

<STDIN> の場合には標準入力を enter キーが押されるごとに、<$FH>の場合にはファイルを1行ずつ読み込んでいます。

perldoc.jp

こんな感じで、

  1. oepn でファイルハンドルを宣言し、ファイルを開く

  2. 開いたファイルから1行読み取り、何か処理をする(今回は表示する)

  3. close でファイルハンドルを閉じる

という一連の処理がファイルの処理の基本となります。

Perlでファイルを書き込む(手作業編)

こちらでもファイルハンドルを使っていきます。

今回はFizzBuzzをファイルに出力してみます。

#!/usr/bin/env perl
use strict;
use warnings;

my $filename = 'fizzbuzz.txt';
open my $FH, '>', $filename; # ファイルを書き込むときは不等号の向きが ファイル名側になる

for my $n ( 1 .. 100 ) {
    if ( $n % 3 == 0 && $n % 5 == 0 ) {
        print $FH "FizzBuzz" . "\n";    # 一見普通のprint文・・・?
    }
    elsif ( $n % 3 == 0 ) {
        print $FH "Fizz" . "\n";
    }
    elsif ( $n % 5 == 0 ) {
        print $FH "Buzz" . "\n";
    }
    else {
        print $FH $n . "\n";
    }
}

close $FH;

実行しても特に表示は出ませんが、fizzbuzz.txt というファイルがスクリプトと同じ場所に作成されています。

では解説です。

fizzbuzzの処理を open, close で囲っています。ここはファイルを読み込んだ時と同じです。

ただし、open したときの不等号の向きに注意してください。

open my $FH, '>', $filename; # ファイルを書き込むときは不等号の向きが ファイル名側になる

不等号がファイル名側を指しているのがファイルへの書き込みです。

そしてもう一つ。

print $FH "FizzBuzz" . "\n"; # 一見普通のprint文・・・?

print のところにファイルハンドル $FH が入っています。

これにより、各数字のfizzbuzzの結果がファイルハンドル経由でファイルに書き込まれています。

カンマは無しで、print と表示したい文字列や変数の間にスペース区切りでファイルハンドルを記述します。

ファイルの読み込みと書き込みを同じスクリプトで行う

ファイルハンドルを2つ用います。

今回は先に作成した fizzbuzz.txt を読み込んで、逆順のファイルを作成する、というスクリプトを書いてみました。

#!/usr/bin/env perl
use strict;
use warnings;

my $read_filename = 'fizzbuzz.txt';
my $write_filename = 'reverse_fizzbuzz.txt';

my @array = ();

# ファイルの読み込み
open my $READ, '<', $read_filename;
for my $line (<$READ>) {
    chomp $line;
    push @array, $line;
}
close $READ;


# ファイルの書き込み
open my $WRITE, '>', $write_filename;
for my $line (reverse @array){
    print $WRITE $line . "\n";
}
close $WRITE;

ファイルに追記したい(手作業編)

ファイハンドルを宣言するときの不等号を >> とします。直感的!

ファイル入出力をモジュールで

IO::Fileがメジャーだと思います。Perlの組み込みモジュールであり、use IO::File するだけで利用できます。

fizzbuzz.txt を読み込むスクリプトです。

#!/usr/bin/env perl
use strict;
use warnings;

use IO::File;

my $FH = IO::File->new('fizzbuzz.txt', '<');
for my $line ( <$FH> ){
    print $line;
}
$FH->close();