Perl で、ブラウザから送られてきたデータを CGI.pm を使わずに処理する方法
最も原始的な例は以下のようになるだろう。今さらだけど、知っておいて損はないかもしれない。
HTTP プロトコルの場合、クライアントからサーバにデータを送る方法は POST メソッドか GET メソッドと仕様で定義されているから、POST の場合と GET の場合の2つのパターンを考えてデータを受け取ってあげればよい。
最も原始的な例は以下のようになるだろう。今さらだけど、知っておいて損はないかもしれない。
HTTP プロトコルの場合、クライアントからサーバにデータを送る方法は POST メソッドか GET メソッドと仕様で定義されているから、POST の場合と GET の場合の2つのパターンを考えてデータを受け取ってあげればよい。
#!/usr/bin/perl
use GD;
I would like to propose the unique requirements for mobile phones. The following items are significant considerations under mobile site development, especially in i-mode phones.
ウェブページへのアクセスログを記録する方法は、いろいろな方法があると思う。もっとも簡単なのは Apache などのウェブサーバが自動的に記録するログをチェックすることで、これには何の手間もいらない。しかし、レンタルサーバなどでログが自由に扱えない場合はどうしたらいいのだろう?
そのひとつが、画像としてリンクされている Perl などの CGI を呼び出す方法だ。利用者に知られずこっそりとアクセスログを取るには 1x1 ピクセルの画像をそのウェブページの背景と共に表示してしまえば、そのウェブページのソースをチェックされない限り、ほとんどの場合は気づかれずにすむだろう。
そんなわけで、以下のように JavaScript と対応する Perl スクリプトで、アクセスログを記録する仕組みを作ってみた。ログの形式は Apache に準拠する(たぶん)。
PHP で、以下の仕様を満たすカウンタを作成してみる (このサイトで使っています)。
連続カウント防止機能は、
if(($ip_check == 1 && $ip != $_SERVER['REMOTE_ADDR']) ...
の部分でチェックしている。$ip には、「直前にアクセスしてきた IP アドレス」が入っている。同じ IP アドレスからアクセスし続ける限りはカウントされない。通常はこれでわりと正確なカウンタとなり得ると思うが、ファイアウォールの内側にいる同一マシンなどで、ファイアウォールのアドレスがアクセスのたびに変わる場合はこの限りではない。また、ケータイからのアクセスなどの場合も毎回アクセス元の IP アドレスがある一定の範囲内で変わるので連続カウント防止にはならないことに注意。
カウンタの数字をカンマ区切りにしているのは以下の 1行である。
$total = number_format($total); // カンマ区切りにする
以下は PHP の全ソース。
<?php
// count.php
// (c) 2006 twinkle.cc
// https://perltips.twinkle.cc/
// updated by yas 2006/01/03
// updated by yas 2003/05/07
// updated by yas 2003/03/05
// オリジナル: https://works.xrea.jp/
// ※このスクリプトと同じディレクトリに「count.dat」という名前のファイルを作成し、置いておくこと。
if(!$file) { $file='count.dat'; }
// 設定 ここから ────────────────────────────
$HOME = 'count.datがあるディレクトリのパス'; // ディレクトリ
$data = "$HOME/$file"; // カウント記録ファイル
$ip_check = 1; // 連続カウント防止(yes=1 no=0)
// 設定 ここまで ────────────────────────────
$count = file($data); // ファイルを配列に
// データ内の文字列を分解してそれぞれの変数に代入
list($total, $ip)=explode('<>', $count[0]);
// カウントアップ処理
if(($ip_check == 1 && $ip != $_SERVER['REMOTE_ADDR'])
|| $ip_check == 0) {
<pre>$total++;
$new_data = implode('<>', array($total, $_SERVER['REMOTE_ADDR']));
$fp = fopen($data, 'w'); // 書きモードでオープン
flock($fp, LOCK_EX); // ファイルロック
fputs($fp, $new_data); // 書き込み
flock($fp, LOCK_UN); // ロック解除
fclose($fp); // ファイルを閉じる
}
$total = number_format($total); // カンマ区切りにする
print $total; // カウンタを表示
?>
このソースを自身の PHP のカウンタを表示したいページを同じディレクトリに置き、同じディレクトリに count.dat という名前のファイルを置く (私のところは「chmod 644 count.dat」で動いているがお使いの環境によってはパーミッションに注意)。
最後に、カウンタを表示したい場所に以下のように書けばよい。
<?php
include('count.php');
?>
gmap_excel.html のファイルの内容
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="styles-site.css" type="text/css" />
</head>
<body>
<form enctype="multipart/form-data" action="gmap_excel.cgi" method="post">
<input type="hidden" name="MAX_FILE_SIZE" value="2000000" />
<table width="600">
<tr><td>Names file:</td><td><input type="file" name="file" />
<input type="submit" value="Upload" /></td></tr>
</table>
</form>
</body>
gmap_excel.cgi のファイルの内容
#!/usr/bin/perl
use lib "path_to_your_perl_lib/lib/perl5/site_perl";
use strict;
use Geo::Google;
use CGI;
our $XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; # XML ヘッダ
our $HOME_DIR = 'your_home_dir';
our $LOCK_DIR = 'path_to_lock_dir/lock'; # do chmod 777 path_to_lock_dir/lock
our $LOCK_FILE = 'lock';
our $XML_FILE = 'path_to_your_output_xml/gmap_excel.xml'; # 出力用 XML ファイル名
our $col_index = 0; # Excel シートの現在の列(カラム)インデックス
our $row_index = 0; # Excel シートの現在の行インデックス
our @data = (); # 出力用データ
our @current_data = (); # 現在のデータ
# オブジェクト初期化
our $CGI = new CGI;
our $GEO = new Geo::Google;
# HTML 出力
print qq|Content-type: text/html\n\n|;
print qq||;
print qq||;
print qq||;
init();
print qq|
|;
# 個々のデータのハンドラ
sub dataHandler {
my ($expat, $text) = @_;
# $row_index = 0 と 1 は、Excel で データのヘッダのため必要なし
if($text !~ /^\s+$/ && $row_index > 1) { # 空白行でなければ
$current_data[$col_index] = $text;
}
}
# 要素の開始
sub startElement {
my ($expat, $tag, %attr) = @_;
if($tag eq 'Row') { # <Row>で始まるとき
$col_index = 0; # カラムを初期化
$row_index++; # 行を進める
@current_data = (); # 現在の行データを初期化
}
if($tag eq 'Data') { # <Data>で始まるとき
$col_index++; # 列を進める
}
}
# 要素の終了
sub endElement {
my ($expat, $tag) = @_;
my ($xml, $html);
my ($lng, $lat, $name, $link, $category1, $category2);
my ($street, $city, $state, $phone, $comment1, $comment2);
# </Row> のとき
if($tag eq 'Row') {
push(@data, { # データに現在の行データを代入
'category1' => $current_data[1],
'category2' => $current_data[2],
'name' => $current_data[3],
'address' => $current_data[4],
'phone' => $current_data[5],
'link' => $current_data[6],
'comment1' => $current_data[7],
'comment2' => $current_data[8]
});
}
# </Workbook> のとき
if($tag eq 'Workbook') {
$row_index = -1; # 行数を初期化 ($row_index = 0 はヘッダのため -1 からスタート)
foreach my $row (@data) {
$row_index++;
if($row->{address}) {
$lng = ''; $lat = '';
my ($degree) = getDegrees($row->{address});
if($degree) {
$lng = $degree->longitude;
$lat = $degree->latitude;
}
}
$name = $row->{name};
$link = $row->{link}
=~ /http:\/\/[!#-9A-~]+\.+[a-z0-9]/ ? $row->{link} : '';
$category1 = $row->{category1};
$category2 = $row->{category2};
($street, $city, $state) = split(',', $row->{address});
$street = $street;
$city = $city;
$state = $state;
$phone = $row->{phone};
$comment1 = $row->{comment1} eq '-' ? '' : $row->{comment1};
$comment2 = $row->{comment2};
# lng と lat が検索できたら (データが入っているということだから)
if($lng && $lat) {
$xml .= "\t<marker\n"
. "\t\tlng = \"$lng\" lat = \"$lat\"\n"
. "\t\tname = \"" . $name . "\"\n"
. "\t\tlink = \"" . $link . "\"\n"
. "\t\tcategory1 = \"" . $category1 . "\"\n"
. "\t\tcategory2 = \"" . $category2 . "\"\n"
. "\t\taddress1 = \"$street\"\n"
. "\t\taddress2 = \"$city, $state\"\n"
. "\t\tphone = \"$phone\"\n"
. "\t\tcomment1 = \"$comment1\"\n"
. "\t\tcomment2 = \"$comment2\"\n"
. "\t/>\n\n";
} elsif($row_index) {
$html .= '<tr><td>'
. "$row_index / $name / $street, $city, $state"
. '</td></tr>';
}
}
$xml = $XML_HEADER
. "<markers>\n"
. $xml
. "</markers>\n";
if($html) { # エラーがあれば
$html = '<table>'
. '<tr><td>***** 以下、緯度経度が検索不能でした。</td></tr>'
. $html
. '</table>';
} else { # エラーがなければ
print '<br />done.';
}
# ファイルのオープン
&file_lock; # ファイルロック
open(XML, ">$HOME_DIR$XML_FILE")
or die('Cannot open an XML file: ' . "$HOME_DIR$XML_FILE");
print XML $xml; # 書き込み
close(XML); # ファイルを閉じる
&file_unlock; # ロック解除
print $html;
}
}
sub getDegrees { # 住所から緯度経度を検索
return $GEO->location( address => shift); # shift = address / リファレンスを返す
}
sub init() {
# XMLパーサー作成 ('UTF-8')
my $xml_parser = new XML::Parser( ProtocolEncoding => 'UTF-8',
Handlers=>{ Start=>\&startElement,
End =>\&endElement,
Char =>\&dataHandler });
# ファイルのオープンはいらない。input タグで指定した name 属性がそのままファイルハンドルになる
my $xml = $CGI->param('file');
# XMLパース処理
$xml_parser->parse($xml) or die "XML error: $xml_parser";
# ファイルクローズ
close($xml);
}
sub file_lock {
my $wait = 5;
while (!symlink('.',"$HOME_DIR$LOCK_DIR/.$LOCK_FILE")) {
if (--$wait <= 0) {
# &error( '現在他の方が使用中です。',
# 'しばらくしてから再度ご利用ください。');
}
sleep (1);
}
}
sub file_unlock {
unlink ("$HOME_DIR$LOCK_DIR/.$LOCK_FILE");
}
1;
gmap_excel.xml 出力例
<?xml version="1.0" encoding="UTF-8"?>
<marker
lng = "-122.125690" lat = "37.413570"
name = "Tofu House"
link = ""
category1 = "Restaurant"
category2 = "Korean"
address1 = "4127 El Camino Real"
address2 = " Palo Alto, CA 94306"
phone = "650-424-8805"
comment1 = ""
comment2 = "Santa Clara の Tofu House と同じ店。"
/>
...
Perl でケータイ向けのサイトを書いているとき、シフトJIS でスクリプトを保存しないければならない場合がある (文字列テーブルなど)。このとき、
Unrecognized character \x81 at ./your.cgi line XX.
といったエラーに悩まされているなら、
my $group = 'グループ';
などのように、「―」を「ー」で表すようにするとよい (Mac の場合は、「ー」が「―」になるようで、この現象が起こる)。または、文字化けする文字の後ろに「\」を付ける。
以下は、Shift JIS で文字化けする文字の一覧。文字化けを起こしやすいよく使う文字は強調しておいた。
文字 - Shift-JIS - Unicode での記述方法
<strong>―</strong> 81 5C ―<br />
<strong>ソ</strong> 83 5C ソ<br />
Ы 84 5C Ы<br />
Ⅸ 87 5C Ⅸ<br />
噂 89 5C 噂<br />
浬 8A 5C 浬<br />
欺 8B 5C 欺<br />
圭 8C 5C 圭<br />
<strong>構</strong> 8D 5C 構<br />
蚕 8E 5C 蚕<br />
<strong>十</strong> 8F 5C 十<br />
<strong>申</strong> 90 5C 申<br />
曾 91 5C 曾<br />
箪 92 5C 箪<br />
<strong>貼</strong> 93 5C 貼<br />
<strong>能</strong> 94 5C 能<br />
<strong>表</strong> 95 5C 表<br />
暴 96 5C 暴<br />
<strong>予</strong> 97 5C 予<br />
禄 98 5C 禄<br />
兔 99 5C 兔<br />
喀 9A 5C 喀<br />
媾 9B 5C 媾<br />
彌 9C 5C 彌<br />
拿 9D 5C 拿<br />
杤 9E 5C 杤<br />
歃 9F 5C 歃<br />
濬 E0 5C 濬<br />
畚 E1 5C 畚<br />
秉 E2 5C 秉<br />
綵 E3 5C 綵<br />
臀 E4 5C 臀<br />
藹 E5 5C 藹<br />
觸 E6 5C 觸<br />
軆 E7 5C 軆<br />
鐔 E8 5C 鐔<br />
饅 E9 5C 饅<br />
鷭 EA 5C 鷭<br />
偆 ED 5C 偆<br />
砡 EE 5C 砡<br />
Procmail を利用すればサーバに送られてきたメールが標準入力から Perl スクリプトに渡すことができる。ここでは Procmail の詳細は割愛するが、以下は標準入力に入っているメールを処理する関数。
使用するクラス
use MIME::Parser;
use MIME::Base64;
use Mail::Address;
use MIME::Parser;
use MIME::Base64;
use Mail::Address;
use Unicode::Japanese;
my ($mail) = &parse;
print "From: " . $mail->{sender} . "\n";
print "To: " . $mail->{recipient} . "\n";
print "Subject: " . $mail->{subject} . "\n";
sub parse {
my ($mail);
# Parser Setting
my $parser = new MIME::Parser;
$parser->output_to_core(1); # Keeps body analyzed internally
$parser->tmp_recycling (1); # Recycles the temporary stuff
my $entity = $parser->parse(\*STDIN) or die "Parse Error in __FILE__\n";
# Headers
$mail->{sender } = $entity->head->get('from' );
$mail->{subject } = $entity->head->get('subject');
$mail->{recipient} = $entity->head->get('to' );
$mail->{recipient} =~ s/\n//g;
# From
@addrs = Mail::Address->parse($mail->{sender});
foreach $addr (@addrs) {
$mail->{sender} = $addr->address if $addr->address; # USE CAUTION, for the multiple address
}
# Subject
my $subject = $mail->{subject};
$lws = '(?:(?:\x0D\x0A|\x0D|\x0A)?[ \t])+';
$ew_regex = '=\?ISO-2022-JP\?B\?([A-Za-z0-9+/]+=*)\?=';
$subject =~ s/\n//g;
$subject =~ s/($ew_regex)$lws(?=$ew_regex)/$1/gio;
$subject =~ s/$lws/ /go;
$subject =~ s/$ew_regex/decode_base64($1)/egio;
$subject = Unicode::Japanese->new($subject, 'auto')->sjis_imode;
$mail->{subject} = $subject;
# Get MIME
($mail->{body}, $mail->{filename}, $mail->{mimetype}, $mail->{object}) = &getEntities($entity);
$mail->{body } = Unicode::Japanese->new($mail->{body}, 'auto')->sjis_imode;
$mail->{body } =~ s/[ \n\r]+$//g; # Delete the white spaces in the end of the line
$mail->{subject} =~ s/[ \n\r]+$//g; # Delete the white spaces in the end of the line
return $mail;
}
sub getEntities {
my $entity = shift;
my $body, $filename, $mimetype, $object;
# BODY
my @parts = $entity->parts;
if (@parts) { # multipart...
my $i;
foreach $i (0 .. $#parts) { # dump each part...
&getEntities($parts[$i]);
}
} else { # single part...
# Get MIME type, and display accordingly...
my ($type, $subtype) = split('/', $entity->head->mime_type);
my $bodyhandle = $entity->bodyhandle;
if ($type =~ /^(text|message)$/) { # text
$body .= $bodyhandle->as_string;
} else { # binary
$filename= $entity->head->recommended_filename;
$mimetype = $entity->head->mime_type;
$object = $bodyhandle->as_string;
binmode($object);
# if we want to store it as BASE64, you can continue the processs here
}
}
return ($body, $filename, $mimetype, $object);
}
1;
Perl の場合は、CGI として動作させる場合は、必ず
print qq|Content-type: text/html\n\n|;
という行が必要な以外は、PHP とほとんど同じである。 アップロードには、いつも使っているおなじみの CGI モジュールを使う。
use CGI;
そうすると、以下のようにアクセスできる。
our $CGI = new CGI;
...
my $xml = $CGI->param('file');
$xml はファイルポインタが入っている。通常は open(IN, "..."); などするときの IN にあたるものである。以下は、コード。アップロードする側の HTML ファイルは PHP でアップロードファイルを処理するで紹介したのと同じ。
#!/usr/bin/perl
use strict;
use CGI;
our $CGI = new CGI;
print qq|Content-type: text/html\n\n|;
init();
sub init() {
# XMLパーサー作成 ('UTF-8')
my $xml_parser = new XML::Parser( ProtocolEncoding => 'UTF-8',
Handlers=>{ Start=>\&startElement,
End =>\&endElement,
Char =>\&dataHandler });
# ファイルのオープンはいらない。input タグで指定した name 属性がそのままファイルハンドルになる
my $xml = $CGI->param('file');
# XMLパース処理
$xml_parser->parse($xml) or die "XML error: $xml_parser";
# ファイルクローズ
close($xml);
}
1;
以下は、HTML 文書中にあるリンクを任意の文字列に置換するスクリプトであるが、他のタグでもいろいろと応用が利くと思われる。
#!/usr/bin/perl
use LWP::UserAgent;
use HTTP::Status;
use HTTP::Response;
our $URL = 'https://perltips.twinkle.cc/'; # アクセスする URL
my $proxy = new LWP::UserAgent;
$proxy->agent('your own created browser name here');
$proxy->timeout(60);
my $response = $proxy->request(HTTP::Request->new('GET' => $URL));
my $content = $response->content;
my %tags = (
'img' => 'src',
'a' => 'href',
'link' => 'href',
'td' => 'background',
'form' => 'action'
);
my $data = $content;
skip_others: while($data =~ s/<([^>]*)>// && $i++ < 10000) { # 無限ループに陥るのを防ぐ
<pre>my $in_brackets = $1;
foreach $key (keys %tags) {
if($in_brackets =~ /^\s*$key\s+/i) {
if($in_brackets =~ /\s+$tags{$key}\s*=\s*["']([^"']*)["']/i
|| $in_brackets =~ /\s+$tags{$key}\s*=\s*([^\s]+)/i ) {
my $link = $1;
$link =~ s/[\n\r]//g;
$link =~ s/\./\\./g;
$link =~ s/\?/\\?/g;
$content =~
s/\s*=\s*["']*$link["']*/=<<<ここに置換文字列を書く>>>/
if $link !~ m/mailto:/i
&& $link !~ m/javascript/i;
next skip_others;
}
}
}
}
1;