AJAX

Perl で、Excel のデータ (XML) をアップロードして処理する方法

  1. まず、Excel で 以下のように category1、category2、name、address、phone、link、comment1、comment2 というカラム構成のワークシートを作成し、適当にデータを入力、XML 形式で保存しておく。
    注: 住所は米国の住所でないと正しく処理されないことに注意
  2. 次に、以下にあるように gmap_excel.html と gmap_excel.cgi の 2つのファイルを用意する (.gmap_excel.cgi 中のパスの設定などは適宜変更のこと)。
  3. gmap_excel.html から上の XML ファイルをサーバにアップロードする。
  4. サーバ側では Excel の XML データを処理して、gmap_excel.xml というファイルが出力される。
  5. このサーバ側で出力された gmap_excel.xml ファイルは Google Maps API を使った AJAX クライアントから利用できる。

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 で、住所から緯度経度を取得する方法

Geo::Google モジュールを利用する (米国)。住所を渡せばコーディング的にはほとんど 1行で緯度経度を返してくれる。Google Maps にアクセスしにいって緯度経度を取ってきていてるので、地図データベースは Google Maps のものである。以前紹介した PHP のときのように SOAP も何もいらない (Geo::Google は、内部で LWP::Simple を利用している)。PHP の Geocoder 版は、Geocoder のデータベースに SOAP 経由でアクセスする。少し使ってみた結果、Geocoder のデータベースは少しズレが生じるようである (米国の場合)


use utf8;
use Encode;
use Geo::Google;
use XML::Parser;
#!/usr/bin/perl use utf8; use Encode; use Geo::Google; use XML::Parser; my $lng, $lat; my $address1 = 'your address part 1 here'; my $address2 = 'your address part 2 here'; my ($degree) = getDegrees("$address1 $address2"); if($degree) {
$lng = $degree->longitude;
$lat = $degree->latitude;
} else {
$lng = ''; $lat = '';
} sub getDegrees { # 住所から緯度経度を検索
return $GEO->location( address => Encode::encode_utf8(shift));
                                                    # shift = address / リファレンスを返す
}
続きを読む

ブログ (Movable Type) と Google Map を組み合わせる

今回取り組む内容の仕様としては、以下の通りである。

  • ブログを利用する。
  • ブログに自分の好きな地域(どこかは予め決めておく)の地図を表示する。
  • 地図にはマーカーが表示される。
  • マーカーの情報は AJAX (XMLHttpRequest) の非同期通信により、サーバーから XML ファイルを読み込んだものである。
  • マーカーをクリックすると吹き出しにその地点の情報が出る。
  • 吹き出し中の「続きを読む」リンクをクリックすると、ブログエントリにジャンプする。
  • マーカーは自分独自のアイコンを使う。

まずは、実例をみてイメージをつかんでもらった方がいいだろう。

このサイトで動作している JavaScript は以下のものである。

//<![CDATA[ var BASE_URL = "http://2kids.net"; var GMAP_FILE = "/yas/gmap.xml"; var ICON_DEFAULT = "/yas/images/icon_restaurant.png"; var ICON_SHADOW = "/yas/images/icon_restaurant_shadow.png"; var map; var baseicon; var xmlHttp; function init() { <pre>map = new GMap(document.getElementById("map")); // 地図オブジェクトを生成 baseicon = new GIcon(); // アイコンオブジェクトを作成 xmlHttp = GXmlHttp.create(); // XmlHttpRequest オブジェクトを作成 // 地図を設定 map.addControl(new GLargeMapControl()); // 拡大縮小のボタン map.addControl(new GMapTypeControl()); // 地図と衛星の切り替え map.centerAndZoom(new GPoint(-122.0745, 37.4170), 7); // ベイエリアを表示 // アイコンを設定 baseicon.image = BASE_URL + ICON_DEFAULT; baseicon.shadow = BASE_URL + ICON_SHADOW; baseicon.iconSize = new GSize (16, 16); baseicon.shadowSize = new GSize (34, 20); baseicon.iconAnchor = new GPoint( 8, 8); // <- (icon_width / 2, icon_height) baseicon.infoWindowAnchor = new GPoint( 9, 2); baseicon.infoShadowAnchor = new GPoint(18, 25); // マーカーを設定: XML データを読み込んで表示 xmlHttp.open("GET", BASE_URL + GMAP_FILE, true); xmlHttp.onreadystatechange = showMarkers; xmlHttp.send(null); } function showMarkers() {
if (xmlHttp.readyState == 4) {

    var xmlDoc = xmlHttp.responseXML;
    var marker = xmlDoc.documentElement.getElementsByTagName("marker");
    for (var i = 0; i < marker.length; i++) {
        addMarker(marker[i]);
    } // End of for
}
} function addMarker(xml) {
var point  = new GPoint(xml.getAttribute("lng"),
                        xml.getAttribute("lat"));
baseicon.image = BASE_URL + xml.getAttribute("icon");
var icon   = new GIcon(baseicon);
var marker = new GMarker(point, icon);
map.addOverlay(marker);
var html   = '<div id="InfoWindow">'
           + '<img src="' + baseicon.image + '" alt="icon"> '
           + '<a href="' + xml.getAttribute("site") + '">'
           + xml.getAttribute("name")
           + '</a><br />'
           + xml.getAttribute("street") + '<br />'
           + xml.getAttribute("city")   + '<br />'
           + xml.getAttribute("phone")  + '<br />'
           + '<a href="' + BASE_URL + xml.getAttribute("link") + '">'
           + 'レビューを読む'
           + '</a><br />'
           + '</div>';
GEvent.addListener(marker, "click", function() {
//  marker.showMapBlowup(3);  // 吹き出しに地図が表示される模様。
//  map.centerAndZoom(p, 3);
    marker.openInfoWindowHtml(html);
});  // click のイベントリスナ登録
} //]]>

このスクリプトは以下のようなフォーマットの XML データを利用する。

<marker
    lng="-121.9485855102539" lat="37.318707484418496"
    icon="/yas/images/icon_mexican.png"
    name="Chevys Winchester"
    site="http://www.chevys.com/"
    link="/yas/archives/cat60/#a000761"
    street="550 S. Winchester"
    city=" San Jose, CA 95128"
    phone="(408) 241-0158"
/>

さて、それでは仕組みについてひとつひとつ説明していこう。 今回の動作確認済み環境としては、以下の通りである (2005/10/28 現在)。

<ul><li>Movable Type 3.2 Release 2 (日本語版)</li><li>Firefox 1.0.7 または IE 6.0SP2 または Safari 2.0.1</li></ul>

というわけでここでは Movable Type 3.2 Release 2 (日本語版) と Firefox 1.0.7 を例に取って話を進める。以下は必要な手順である。

  1. アイコンの作成 (Google Map に表示するマーカーのこと)
  2. JavaScript 作成 (Google Map 用)
  3. map.xml の作成 (XMLHttpRequest を使ってマーカーの情報を読み込む)
  4. ブログテンプレートの修正
  5. スタイルシートの設定
  6. ブログエントリの追加

それではまずはアイコンについ説明しよう。

  • Google Map API では、マーカーと呼ばれるポイントを指し示すアイコンが必要となる。Google が用意したデフォルトのマーカーアイコンはオレンジ色の水玉を逆さにしたような感じのものだが、これは自由にカスタマイズできる。
  • アイコンの実体は画像ファイルである。画像のフォーマットは、JPEG、GIF、PNG が使えるが、PNG がいいだろう。
  • なぜなら、アイコンには影を付けられるのだが、この影は、αチャネルを透過にした PNG を使うことになっているからだ (まぁ、どの画像フォーマットを選ぶかは実際は好みの問題だ)。
  • ちなみにアイコンの影は、オリジナルのアイコンに対して45度の傾きを付けるのが、Google からのガイドラインのようである。実際影をつけるのは結構難しかった。私の場合はオリジナル画像をグレー処理し、その後何度かソフト処理をし、ぼかしをかけた…という感じで、この作業に多くの時間を取ってしまった^^;

以下、JavaScript からのアイコン部分生成に関する抜粋だ。要は、GIcon オブジェクトに用意されているいろいろなプロパティを設定してあげれば、自分で作ったアイコンが表示されるのである。

var BASE_URL = "http://2kids.net"; var baseicon; function init() {
baseicon = new GIcon();  // アイコンオブジェクトを作成

// アイコンを設定
baseicon.image            = BASE_URL + "/yas/images/icon_restaurant.png";
baseicon.shadow           = BASE_URL + "/yas/images/icon_restaurant_shadow.png";
baseicon.iconSize         = new GSize (16, 16);
baseicon.shadowSize       = new GSize (34, 20);
baseicon.iconAnchor       = new GPoint( 8,  8);  // <- (icon_width / 2, icon_height)
baseicon.infoWindowAnchor = new GPoint( 9,  2);
baseicon.infoShadowAnchor = new GPoint(18, 25);
baseicon.image = BASE_URL + xml.getAttribute("icon");
} function addMarker(xml) {
var point  = new GPoint(xml.getAttribute("lng"),
                        xml.getAttribute("lat"));
baseicon.image = BASE_URL + xml.getAttribute("icon");
var icon   = new GIcon(baseicon);
var marker = new GMarker(point, icon);
map.addOverlay(marker);
} //]]>

次に肝心の JavaScript であるが、Movable Type のテンプレート作成機能を利用する。Movable Type で、環境設定 / テンプレート-モジュールと選択していき、「モジュールを新規作成」をクリックして以下のように作成(設定)し、保存する。

  • テンプレート名: gmap.js (←ここは実は何でもいい)
  • このテンプレートにリンクするファイル: gmap.js (別のテンプレートから使用するファイル名)
  • モジュールの内容: 冒頭の JavaScript

JavaScript を保存したら、


map.xml

というファイル名で、XML ファイルを保存する。以下に XML ファイルの内容 (属性) を説明しよう (属性は独自に設定したものです)。

<marker
    lng="-121.9485855102539" lat="37.318707484418496" <font color="blue">← ここは、</font><b><font color="orange">経度</font><font color="blue">・</font><font color="red">緯度</font></b><font color="blue">の順で入る</font>
    icon="/yas/images/icon_mexican.png" <font color="blue">← アイコン画像へのルートからのパス + ファイル名 (http:// は、JavaScript 中の最初に記述済み)</font>
    name="Chevys Winchester" <font color="blue">← 吹き出し中の一番上に表示される、マーカーの名前</font>
    site="http://www.chevys.com/" <font color="blue">← マーカーの名前をクリックすると表示されるウェブページ</font>
    link="/yas/archives/cat60/#a000761" <font color="blue">← Movable Type のブログのエントリへのリンク※</font>
    street="550 S. Winchester" <font color="blue">← マーカーの住所 (サンプルはアメリカなので番地が先)</font>
    city=" San Jose, CA 95128" <font color="blue">← マーカーの住所 (サンプルはアメリカなので市町村がが後)</font>
    phone="(408) 241-0158" <font color="blue">← 電話番号</font>
/>

※…「Movable Type のブログのエントリへのリンク」とあるのは、個別ブログについている ID である。個々のブログエントリのソースを見ればわかると思う。
これでスクリプト & XML データの準備が整うので、テンプレートを修正する。環境設定 / テンプレートで、

  • メインページ
  • アーカイブ → カテゴリーアーカイブ
  • アーカイブ → 日付アーカイブ
  • アーカイブ → 個別エントリーアーカイブ

のすべてのテンプレートにおいて、

… <script src="http://maps.google.com/maps?file=api&v=1&key=ここにあなたが取得した Google Map API のキーが入る" type="text/javascript"</script> <script src="/ルートからブログへのパス/gmap.js" type="text/javascript"></script> <body onLoad="init()"> …

のように修正する。onLoad="..." で最初に呼ばれる関数を指定しないと、IE ではうまく動作しなかった。
また、吹き出し中に表示されるリンクをクリックすることによって、個別のブログエントリにジャンプできるように、テンプレートを少し修正する。ブログエントリのタイトルを表示する箇所を見つけ、以下のように... タグをはさむ。

">

"><$MTEntryTitle$>

また、個別エントリから地図に戻れるように、以下の部分の最後に「地図に戻る」リンクを追加する。「#a000835」とある部分は、個別ブログエントリで、Google Map を表示したいブログエントリの ID である。

投稿者 <$MTEntryAuthor$> : "><$MTEntryDate format="%Y/%m/%d"$> | #comments">コメント (<$MTEntryCommentCount$>) | #trackbacks">トラックバック | 地図に戻る

続いてスタイルシートも設定してしまおう。Movable Type のテンプレート→スタイルシートで、スタイルシートの最後に以下の内容を追加する。

#map {
font-family: "Trebuchet MS", Trebuchet, verdana, arial, sans-serif;
text-align: center;
} #InfoWindow {
font-family: "Trebuchet MS", Trebuchet, verdana, arial, sans-serif;
}

こんな具合である。#map と #infoWindow を自由にいじってみて欲しい (#InfoWindow は実は JavaScript 中にそのように設定できるように記述してある)。
最後はブログエントリの追加である。ブログの本文に、以下の1行を追加すれば Google Map が表示される。width と height は地図の大きさなので自由に設定して欲しい。

<div id="map" style="width: 500px; height: 400px;">
続きを読む

AJAX でサーバにあるファイルの内容を表示したい。

  1. Ajax (エイジャックス)とは、Asynchronous JavaScript + XML の略で、JavaScript からブラウザのバックグラウンドで HTTP リクエストの通信をして、非同期に通信が行えるようにする技術のこと。Gmail や Google Maps が良い例。
  2. この技術のキモは XMLHttpReqeuest という JavaScript から呼べるオブジェクトである。IE や Firefox には実は XMLHttpRequest というオブジェクトが実装されており、これを使うことによって表示されているページの裏で、*ユーザーの操作とは非同期に* HTTP 通信を行うことができる。
  3. HTTP 通信自体はその性質上、決して非同期になることはない。ブラウザ(クライアント)からリクエストしたらサーバからレスポンスが返ってくるだけである。ただし、そのレスポンスはいつ返ってくるかわからないから、通常ブラウザは結果が返ってくるまで待つ必要があるが、XMLHttpRequest.onreadystatechange = foo などのように、コールバック関数(メソッド)を指定しておけばサーバからのレスポンスが返ってきた時に呼び出される関数を指定できる。
  4. セキュリティ上の理由により JavaScript (XMLHttpRequest) からアクセスできる URL は HTML のダウンロード元と同じドメインに限定される(Java のアプレットで言うサンドボックスモデルと同じ意味)。
  5. 多くのサイトで XMLHttpRequest を使った POST の方法は解説されているため、ここではシンプルに HTTP プロトコルの GET メソッドの使い方を提示してみた。
  6. Note: Mac OS X Safari (Safari/412.5) では、SSL / TLS の https:// で XMLHttpRequest.open できないことが判明 (2005/09/06 追記)
<script type="text/javascript"> <!-- // ^^^^^ 単に <script> と書いても良い。 var xmlhttp; // 特に IE に対応するために、XMLHttpRequest オブジェクトをブラウザによって作成 function createXmlHttp() {
try {
    return new ActiveXObject ("Msxml2.XMLHTTP");
} catch(e) {
    try {
        return new ActiveXObject ("Microsoft.XMLHTTP");
    } catch(e1) {
        try {
            return new XMLHttpRequest();
        } catch(e2) {
            return null;
        }
    }
}
} // メイン部分。HTML タグの onload のところで関連付けられている。 // よって、ブラウザが HTML を読み込むと同時にこの関数(メソッド)が実行される。 function getContent() {
if ((xmlhttp = createXmlHttp()) == null) {
    window.alert("XMLHTTP Initialization Failed.");
} else {
    xmlhttp.abort();    // 連続して HTTP 通信する場合は必ず呼び出す。
}

// 1. handleHttpEvent() のように指定すると動かないので注意。
xmlhttp.onreadystatechange = handleHttpEvent;

// 2. 第3パラメータの true / false は、true = 同期, false = 非同期
// なお、セキュリティ上の理由により open で指定できる URL は
// HTML のダウンロード元と同じドメインに限定される。
// (Java のアプレットで言うサンドボックスモデルと同じ意味)
xmlhttp.open('GET', 'https://perltips.twinkle.cc/ajax_result.txt', true);

// 3. ここはオプショナル。
xmlhttp.setRequestHeader("Content-Type", "text/html");

// 4. GET のとき: send(null) / POST のとき: send(a variable)
xmlhttp.send(null);
} // コールバック関数。 // 上の xmlhttp.onreadystatechange = handleHttpEvent; で関連付けられている。 function handleHttpEvent() {
// readyState の番号の意味は以下の通り。
// 0 = uninitialized
// 1 = loading
// 2 = loaded
// 3 = interactive
// 4 = complete
if ( xmlhttp.readyState == 1) {
    // "data" とは、<body> の中の、<span id="data"> の部分
    // よって、この部分に "Loading..." が表示される(表示は一瞬だけとなる)
    document.getElementById("data").innerHTML = "Loading...";
}
else if ( xmlhttp.readyState == 2) {
    // "data" とは、<body> の中の、<span id="data"> の部分
    // よって、この部分に "Loaded." が表示される(表示は一瞬だけとなる)
    document.getElementById("data").innerHTML = "Loaded";
}
else if (xmlhttp.readyState == 4
     &&  xmlhttp.status == 200) {    // 200 は、HTTP プロトコルにおける正常終了
    document.getElementById("data").innerHTML = xmlhttp.responseText;
}
} // --> </script> <body onload="getContent()">

サーバ側のファイル (ajax_result.txt) は以下の通り。

This is the server-side HTML.

参考になるサイト・リンク

続きを読む
AJAX を購読