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 書くだけならね(できるとは言っていない
追記2
やりたいことが整理された結果、Google Drive 中心にいろいろやれば Mojolicious 使わなくて良さそう感が出てきた
Docker上でPerlを動かして、ホストにあるファイルを実行する
Perl インストールしないで Perl 実行するにはどうするんがいいのかなー?的に思って作った環境。
Perl 公式の Docker イメージ使ってみたけど、結構サイズが大きかった。
初回だけDockerイメージのダウンロードに時間がかかります。
$ 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::Spec
に Cwd
などのファイル・ディレクトリを扱う便利モジュールも数多くあります。
何かしようとして、うまくいかないとか、こういうことをがしたい!という方は Perl入学式の slack などで質問くださいませ。
長いおまけ: PerlでWindows上のファイル名を変更する
中には、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 接続して弥生会計を使いたい!
Mac に Parallels で Windows10 を入れ、その上で VPN 接続して弥生会計を使いたい!
そういう人も日本に3人くらいいるかもしれない、という感じで書いてみます。中身は薄いです。
弥生会計とは・・・?
シェア6割を誇ると言われる企業用の会計ソフトです。
こんな感じです。
複式簿記で入力していきます。
簿記では最終的な貸借対照表(BS)や損益計算書(PL)に至るまで、途中途中で精算表を作っていきます。
が、会計ソフトだとそういった帳票の転記もやってくれるので、とても楽です。
というか、会計ソフトがない頃の経理業務を想像すると地獄みしかないですね・・・
なぜ Parallels なのか
弥生会計が Mac 対応しておらず、Windows 用のクライアントソフトしかないからです。
一応、自己責任で Parallels 経由で利用できますよ、と公式ページにもあります。
Mac の他に別途 Windows ノートを用意するということも考えましたが、リモートワークの昨今、ノートPCを2つも持って歩きたくない・・・という怠惰な理由です。
あと「やってみたかった」という純粋な興味。
作業環境
- macOS Mojave
- Parallels Desktop 15
- Windows10
- 弥生会計 20
Mac への Parallels インストールと Windows10 のインストール、その Windows10 上での弥生会計インストール、これはうまくいきました。全くトラブルなし。
会社で利用している弥生会計は、大元のデータを Microsoft SQL Server に保存しています。
社内ネットワークに接続し、サーバへ接続してデータ編集、これも問題なくできました。
詰まったところ
が、VPN接続環境下ではサーバにアクセス出来ない現象が発生しました。
弥生会計の認証は通るものの、そのあとのサーバへの接続のところで失敗します。
VPN は macOS 側で接続した場合も Windows10 から接続した場合も接続は可能です。
また、同サーバにある共有フォルダ(SMB)にはアクセスが可能(サーバー名、IPアドレスとも)なことから、弥生会計の設定や何かに問題があるのでは?と推測しました。
ただ、素の Windows10 から利用していないので、もう一つの方法として Mac の SSD からパーティションを分けて BootCamp に Windows10 をインストールし、そこに弥生会計をインストール、VPNの設定を作って接続してみました。
Parallels 経由ではない、素の Windows10 環境です。・・・が、結果は変わらず。
ううんー???
となったところで、インフラ担当さんより、サーバを名前ではなくIPで指定してみたら?とのアドバイス。
共有フォルダ(SMB)だと名前で解決できたけど、そうか、弥生の名前解決の仕組みは別か・・・!
こんな感じ(サーバー名とかIPアドレスは適当に変更してます)
- 元の設定: KEIRI\YAYOI
- 変更した設定: 192.168.1.100\YAYOI
でアクセスしたところ、無事データの取得に成功しました。さすがインフラ担当、頼りになる!
ということで、色々と試行錯誤はしましたが、 Mac に Parallels 入れて Windows10 をインストールし、その Windows10 内の弥生会計からVPN経由で会社サーバーへの接続は可能だった!ということで記事を締めさせていただきますありがとうございました。
最近のお仕事 2020年04月
色々あって、3月の終わり頃から会社の経理業務を手伝っています。
経理業務の経験はないのですが、履歴書に簿記2級って書いてたのを覚えてくれていた人がいたようです。
既存サービスの保守とかアップデートをしつつ、経理の世界に足を突っ込んでいるような状態です。
それに伴い、触れる環境が随分変わりました。
こんな感じです。
前任者からの急ぎの引き継ぎで、Excelベースの便利シートなどがあるんですが、よくわからなかったりして「超古代の進んだ文明を理解できない原始人類」みたいな気分を日々味わってます。
とはいえ、割と楽しくお仕事はしており、これは周りの同僚さんのサポートあればこそです。ありがたいです。
弥生会計については触るのも初めてですが、体験版のついた参考書を買って練習したりしています。商業高校の生徒さんはこういうのやってるのか・・・負けてるわ・・・ってなりながら勉強しております。
そういえば、今年は Go でちょこっとお手伝いしたり、 PHP で画像合成ツール改修したりとお仕事 Perl 書いてないっすね。経理業務では隙あらば Perl で業務を楽にしていきたいです。
Mac の英語キーボード(US)と Windows10 のかな漢字変換のキーバインドを合わせる
下記のブログさまさまです・・・。ありがたいありがたい。
Perlでファイルの文字コードを指定して読み込み&書き込み
一昨日の金曜日、ムキー!ってなったことを、落ち着いて週末に解決するべく色々やった記録です。
まず、このGoogle Spread Sheet 開いてください。普通に開けると思います。
はい。タイトルで察していただけると思うんですが、そういうことです。
では、画面左上のタイトルの下にある [ファイル] メニューからダウンロードを選び、[カンマ区切りの値 csv] を選びます。
ダウンロードのダイアログが出るので、一度どっかに mojibake.csv
ってファイル名で保存してください。
保存したcsvファイルですが、VSCode や SublimeText で普通に開き、中身を確認することができます。Windows のメモ帳(notepad.exe)や macOS のテキストエディットでも文字化けは起こりません。
ではこれを Office365 の Excel で開いてみます。
見事に文字化けです!
Office365 は macOS でも利用できるのですが、同様に文字化けします。
憎いですね。許しがたいですね。
このような状況はよくあります。
これは「文字コード」の違いが原因です。文字コードについては色々大変なのでここでは説明しません。
説明しきる力量がないともいいます・・・
- cp932: Excel をはじめとしたMicrosoft 製品(日本語版)で利用されている文字コード
- utf-8: macOS や *unix で利用されている文字コード。Google Spread Sheetもこれ
という差異が文字化けを生んだわけです。
Excel(Office365) で utf8 のcsvファイルを文字化けさせずに読み込む
もちろん、Excel なので(信頼感)こういったファイルをちゃんと取り込む術があります。
まず、csvファイルを 読み込まず に Excel を開き、メニューから [データ] を選択、その中の [テキストファイル] を選びます。画像の左から2つ目のアイコンのところです。
ウィザードの画面が出ます。
[区切り記号付き] を選び、[元のファイル] のプルダウンメニューから [Unicode (UTF-8)] を選びます。
下のプレビューで見ると(字が細いですが)、正しく表示されていることが確認できます。
あとはなりなりに進んでいくと、無事に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行、書き込む際のファイルハンドルの文字コード指定 utf8
を cp932
に変えただけです。
コードはこちら。クリックすると展開されます
#!/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ですんなりファイルを読み込み、文字化けもせず。素晴らしい!
逆(cp932 -> utf8)もいけます
もちろん、Excel をはじめ Windows アプリが cp932 で出力した csv ファイルを読み取り、utf8 で書き出すことも可能です。
官公庁のcsvファイルなんかはだいたい Windows や Excel で作られているみたいなので、こちらの方が数は多いでしょうね。
例えば厚生労働省の人口動態データです。右上の [ダウンロード] ボタンからダウンロードしてみてください。
これを文字コードを指定せず、Perlで読み込んで出力すると文字化けします。
では、文字コードを指定して表示してみます。
#!/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;
無事、文字化けせずに表示ができました。
次回
1個のcsvファイルを処理するにはこれで良いと思うんですが、これが100個あったときにはファイル名を変えつつ実行しなくてはいけないのでは・・・?
心を込めて100回、ファイル名を変えるの?
・・・
ということで、次回はフォルダから中のファイルを拾う方法てのをやります。
おまけ
6年くらい前、Perl勉強してこ!ってときに使っていたのは Windows マシンだったのですが、Webから拾ってきた諸々が文字化けするのが解消できなかったんですね。
ですので、Macを買ったのでした。これぞ富豪的解決!・・・とはならず、今度は Windows から出力したファイルを mac で扱おうとして文字化けしてしまい、苦しみは尽きないのでした。
昨今、文字コードはアプリケーション側で吸収することも多く、文字コードの差異で苦しむのはプログラムを書くときくらいかも知れません。
Perlでcsvファイルを読み込んで 配列|ハッシュ にする
先週からの続きです
先週でテキストファイルの読み書きができるようになりました。
ということで、今回は代表的なテキストファイルのデータとして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
はそこで分割してしまいます。
Perlでcsvを扱うモジュールといえば Text::CSV_XS
が定番です。Text::CSV_XS
であれば、そのようなデータにもうまく対応してくれます。
(ただし、処理するファイルの前提はある)
また、スクリプトの上の方に use Text::CSV_XS;
とあると、「あー、csvを何かするスクリプトなのね」と理解の一助になります。
なお、標準モジュールではないのでインストールが必要です。
$ cpanm Text::CSV_XS
利用例としてはこんな感じです。
#!/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を読み込んでハッシュにする
こちらの使い方も多いと思います。
ハッシュにするにあたって考えるのは、key
と value
をどうするか?ということです。
今回はこんな感じのデータ構造を作ってみます。配列の中にハッシュリファレンスが入っています。
ハッシュなので順不同なのですが、見やすさ優先で 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を読み込んでハッシュにする(モジュールを使う)
ここまで長く書いてきてなんですが、ハッシュにするときもモジュールを使うのが楽でおすすめです。
こちらも標準モジュールではないのでインストールが必要です。
$ 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でテキストファイルの中身を表示する
色々書いたけど省略
- CSVファイルを処理する方法について書いておきたい
- そもそも、ファイルの入出力についてPerl入学式の現行カリキュラムでやってない
- しかもいい感じのCSVファイルのサンプル、Shift-JISフォーマットじゃん
- ・・・一歩ずつ、ファイル入出力からやっていこう
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; # ファイルハンドルを終了する
これだけだと、ファイルハンドルを設定して終了しただけです。
この、 open
と close
の間に処理を書きます。今回書かれているのは・・・
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行ずつ読み込んでいます。
こんな感じで、
という一連の処理がファイルの処理の基本となります。
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
するだけで利用できます。
#!/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();