livedoor の天気予報 API 用クラスつくった。

いまさらって感じもあるかもしれませんが、livedoor の「お天気 Web サービス」用の XML をパースして、JSON とか PHP の配列で取得するためのクラスを作った。

PHPのlivedoor Weather Hacks API用クラス

でも、いざ使おうと思ったら、「お天気 Web サービス」そのものが当初予定していた用途にマッチしないことに気がついて、あららと思いながらも、せっかくなので完成(?)させた。

ところで

こういうことって、気象庁あたりがやるべきではないんだろうか?と、おもって調べたら、ここでアメダスのデータをほぼリアルタイムに掲載していた。

で、クローラーつくって取り込んでしまえと思ったら、HTML内に重複したIDが指定されているらしくパーサーがエラー。。。

「自動巡回ソフトは原則としてお断り」みたいなことが書いてあったが、新手のクローラー対策だろうか?(笑)

MySQLで任意の緯度・経度から最も近いデータを取得するSQL

MySQLには位置情報を扱う機能があるが、コツが必要なようなのでメモ

テーブルを作成

テーブルを作成する際に注意するべきなのは以下の2点。

  • 緯度・経度を保存するフィールドのデータ型は、 “geometry” とする。
  • インデックスに SPATIAL を使用する。

自分の環境の、phpMyAdmin では SPATIAL インデックスが作成できなかったので、以下のようなSQLを発行した。

ALTER TABLE geom ADD SPATIAL INDEX(geo);

テーブルのスキーマは以下のような感じ。

CREATE TABLE `points` (
  `loc` varchar(20) NOT NULL,
  `point` varchar(50) NOT NULL,
  `geo` geometry NOT NULL,
  PRIMARY KEY  (`loc`),
  UNIQUE KEY `point` (`point`),
  SPATIAL KEY `geo` (`geo`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

実際のSQL

以下のようなSQLで、指定した緯度・経度に最も近い1件を取り出すことができる。

SELECT
    loc,
    GLength(
        GeomFromText(
            CONCAT(
                'LineString(135.8 33.48,',
                X(geo),
                ' ',
                Y(geo),
                ')'
            )
        )
    ) AS len
FROM points
WHERE
    MBRContains(
        GeomFromText(
            Concat(
                'LineString(',
                135.8 + 1,
                ' ',
                33.48 + 1,
                ',',
                135.8 - 1,
                ' ',
                33.48 - 1,
                ')'
            )
        ),
        geo
    )
ORDER BY len limit 0,1

ここで注意!

単純に最も近い1件を取り出すだけならWHERE句は必要ないが、MySQLでは空間インデックス(SPATIAL)をORDER BYで使用することはできなようだ。(たぶん。。。)

そのため、WHERE句で対象となるデータを減らした上で、ORDER BYしたらパフォーマンスが向上したので、やむなくWHERE句を挿入した。

WHERE句内で使用している “+ 1″ とか “- 1″ のあたりは指定した緯度経度を基準にした約100kmに相当する矩形をつくるという意味であるが、対象となるデータの件数や密度によって、増やしたり減らしたりする必要がある。

これに関しては、以下のサイトで詳しく検証されている。

MySQL TIPS 3 空間情報(geometry)を使って経度・緯度の検索を高速化する – イノベートな非日常

が、explain の結果にfilesort とあるので、ORDER BYではやはりインデックスが使用されていないことに注意する必要がある。

というわけで。。。

MySQLで位置情報を取り扱う際にORDER BYを使用するには注意が必要。

Google MAPを使用する等ユーザーインターフェースで工夫して逃げる方が得策かもしれない。

PHP5のDOM拡張モジュールでHTMLをパースする

PHP5で追加されたDOM拡張モジュールではHTMLのパースも可能である。
さらに、このDOM拡張モジュールは標準でインストールされている。

今回の記事では、このDOMDocumentクラスについてのおぼえがき。

ちなみに、DOMが苦手な人は”要素”とか”属性”を理解できずに、まとめてタグとしてしか理解出来ていないひとが多い気がする。

HTMLを読み込む

まずはじめに、DOMDocument オブジェクトを生成する。

$doc = new DOMDocument();

そのあとで、HTMLを読み込む。
以下のようにloadHTML()などを使用すると閉じタグのないHTMLでもパース可能になる。

$doc->loadHTML('<html></html>'); // HTMLソースを読み込む
$doc->loadHTMLFile('/path/to/index.html'); // HTMLファイルを読み込む

DOM拡張モジュールによるHTMLの読み込みには上記のように2種類の方法が用意されており、HTMLファイルを読み込む方法は、http:// からはじまるURLでも指定可能である。

2011/03/04 追記

HTMLを読み込む際に、上記の処理では文字化けするサイトがあることがわかった。
以下のような処理を行えば解消した。

$html = file_get_contents('/path/to/index.html');
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'auto');
$dom->loadHTML($html);

参考: 2008-05-30 – hamacoの日記

HTMLをパースするために実際の処理

HTMLパースの流れはJavaScript等のDOM操作と同じなので、わかるひとにはすぐにわかると思う。

HTML内の全てのA要素を取得する。

$elements = $doc->getElementsByTagName('a');

ちなみに、パラメータで指定する要素名(上記の例ではa)は、大文字小文字を区別するようなので注意。

取得した要素がいくつあるかを取得する。

以下のような感じ。

$elements->length

取り出した全ての $elements のhref 属性を出力する。

foreach ($element as $e) {
    print $e->getAttribute('href');
}

これも大文字小文字を区別するのかな?

取り出した全ての $elements のノード値を取得する。

<a>~</a> で囲まれた部分を取得するには。

foreach ($element as $e) {
    print $e->nodeValue;
}

取り出した $elements の n 番目の属性値を取得する。

取得した最初のa要素の href 属性を取得するには?

$elements->item(0)->getAttribute('href');

参考

Mail_CheckUserをGitHubに移行しました。

本サイトで配布しているメールアドレスをより厳密にチェックするためのクラス”Mail_CheckUser”をGitHubに移行しました。

miya0001′s Mail_CheckUser at master – GitHub

PEARに登録しかかっているので、早々に何とかしたいのですが、例外処理をきちんとしなさいとの難題を押し付けられて(笑)、どんづまり状態を打破すべく、GitHubで一緒に勉強しながら手伝ってくれるかたを募集中です。

PHP向けに軽くて簡単なテンプレートエンジン作った。

PHP向けに軽くて簡単なテンプレートエンジンを作った。

tinyTemplate

Smartyを使うのは面倒だけど、ビューとロジックはできればわけたい。みたいなときにご利用いただくと便利だと思います。

私は、メールフォームのメールテンプレートで使いたくて、開発しました。

考え方はいろいろあると思いますが、デフォルトではすべての出力をHTMLエスケープするのも特徴の一つです。

PHPのcall_user_func()での参照渡しに注意

どうもよくわからんのでメモ。

参照渡しとは?

関数に値を渡す通常の方法。

<?php

$i = 0;

test($i);

print $i; // 0を出力。$iの値そのものは変わらない

function test($int){
  $int++;
}

?>

参照渡しにする。(関数の定義の引数の部分に&をつける。)

<?php

$i = 0;

test($i);

print $i; // 1を出力。$iの値がかわった!

function test(&$int){
  $int++;
}

?>

通常の関数の引数は、渡された変数の値のみが渡されるが、参照渡しをすると変数そのものが渡されるので、このような挙動になる。

PHP5での仕様変更

ここまでは、特に問題はない。

しかし、PHP4では、以下のように呼び出し側でも参照渡しの指定ができたが、これはPHP5ではphp.iniを変えないと動作しない。

<?php

$i = 0;

test(&$i); // 呼び出す際に&をつける

print $i; // 1を出力。$iの値がかわった!

function test($int){
  $int++;
}

?>

これは実際、そうするべきだと思う。

なんで呼び出し側で参照渡しをできるようにしていたのか、よくわからんので、改善だと思う。

が!問題はここから!

call_user_funcを使用した際の参照渡し

以下のようにcall_user_funcを使用すると、関数側で参照渡しするよう指定していても参照渡しされないばかりか、警告が出る!

<?php

$i = 0;

call_user_func('test', $i);

print $i; // 0

function test(&$int){
  $int++;
}

?>

出力結果

Warning: Parameter 1 to test() expected to be a reference, value given in...

ためしに、call_user_func(‘test’, &$i); としたら予想通り以下のエラー。

Deprecated: Call-time pass-by-reference has been deprecated in

なんじゃそりゃ!?

解決策

いろいろ試した結果、以下のような方法なら大丈夫なようだが、全く納得がいかない。

call_user_func_array() を使用する

<?php

$i = 0;

call_user_func_array('test', array(&$i));

print $i; // 1を出力。$iの値がかわった!

function test(&$int){
  $int++;
}

?>

関数名を変数に代入してコールする

<?php

$i = 0;

$func = 'test';

$func($i);

print $i; // 1を出力。$iの値がかわった!

function test(&$int){
  $int++;
}

?>

たまたま、今のバージョンで警告が出ていないだけなのか?
もしそうなら、 call_user_funcしたいときは、どうやって参照渡しするんだ?

参考

とても小さなPHPのテンプレートエンジン “bTemplate”

WordPressのプラグイン開発でビューとロジックを分離するためにテンプレートエンジンを使いたくなった。

PHPのテンプレートエンジンでは、Smartyが有名だが、WordPressのプラグインに同梱するにはあまりにも高機能すぎるしファイル数も多い。

そこで、できれば1ファイルで構成されていて、シンプルで簡単なテンプレートエンジンはないものかと探してみたら、bTemplateというのがあった。

bTemplate

このbTemplateは、わずか289行のPHPで書かれたテンプレートエンジンで、思わず未完成のものではないかと心配してしまうぐらいシンプルなソースで記述されているが、ほぼ期待通りの動作を確認できた。

実装は、なんとなくPerlのHTML::Templateと似ている。

ちなみに、本家で配布しているファイルは、最終行に余分な改行が含まれており、そのままでは空白が出力されてしまうので使用する前に削除しておく必要がある。

主な機能

  • 変数の割り当て
  • ループ
  • 条件分岐(ifまたはelseのみ)

Smartyなどのような高機能なテンプレートエンジンとは違い、キャッシュ機能は実装されていないが、そのためにパーミッションの設定などに煩わされることがない。

やや残念なのが、条件分岐が変数のtrueまたはfalseによるifまたはelseしか実装されていない点であるが、これはロジック側で頑張れば良いことなので問題なさそう。

サンプル

単純なテンプレート

<html>
  <head>
    <title><tag:title /></title>
  </head>

  <body>
    <p>Hello, <tag:name /></p>
  </body>
</html>

上記テンプレート用のロジック(PHP)

<?php
$title = 'My Simple Template Example';
$name = 'L.E. Modesitt, Jr.';

include_once('bTemplate.php');
$tpl = new bTemplate();

$tpl->set('title', $title);
$tpl->set('name', $name);

echo $tpl->fetch('simple.tpl');
?>

シンプルなループ

<ul>
  <loop:names>
    <li><tag:names[] /></li>
  </loop:names>
</ul>

上記テンプレート用のロジック(PHP)

<?php
$names = array('Tom', 'Dick, 'Harry');

include_once('bTemplate.php');
$tpl = new bTemplate();

$tpl->set('names', $names);

echo $tpl->fetch('loop.tpl');
?>

SQLで得た結果をテーブルに出力

<table border="1">
  <tr>
    <td><b>id</b></td>
    <td><b>last_name</b></td>
    <td><b>first_name</b></td>
    <td><b>phone_number</b></td>
  </tr>
  <loop:users>
    <tr>
      <td><tag:users[].id /></td>
      <td><tag:users[].last_name /></td>
      <td><tag:users[].first_name /></td>
      <td><tag:users[].phone_number /></td>
    </tr>
  </loop:users>
</table>

PHP側

<?php
// Assuming you've already connected to the database
$result = mysql_query('SELECT * FROM users');

// This loop populates an array with the data from the query
while($row = mysql_fetch_assoc($result)) {
  $users[] = $row;
}

include_once('bTemplate.php');
$tpl = new bTemplate();

$title = 'Database Results';
$tpl->set('title', $title);

// Now we set the array we populated
$tpl->set('users', $users);

// And here we fetch the users.tpl file and set it to content
$tpl->set('content', $tpl->fetch('users.tpl'));

echo $tpl->fetch('master.tpl');
?>

ちなみに、テンプレートのHTMLをファイルからではなく、変数から取得したい場合は、以下のようにすればいい。

echo $tpl->parse($content);

WordPressのリンクをオンラインブックマークにする

前回までの記事で何度かご紹介したが、ようやくWotrdPressのリンクをオンラインブックマークふうに使用するためのプラグインができた。

WordPressのリンクをブックマークとして共有するプラグイン

これを使用すると、いちいち同期作業みたいなことをしなくても、ほぼ全てのブラウザでブックマークの共有が可能になる。

なんとiPhone版のSafariでも!

というわけで、ぜひお試しください。

WordPressでGoogleブックマークみたいなこと(2)

以前の記事で、WordPressのリンクをブックマークのように使用できないか?ということで、ごちゃごちゃ書いた。

で、あの後、致命的に設計不良があることに気がついた。

前回の記事ではXBELをWordPressのプラグインで出力して、それをブックマークレットからごちゃごちゃみたいなことを計画していたのだが、クロスドメイン制約にひっかかることを完全に忘れてた!

というわけで、結局JSONを使用することで解決して、ほぼ出来上がった。

とりあえず、デモンストレーション。

実際のブックマークレット

以下のリンクを、ブックマークバーにドラッグ&ドロップするか右クリックでお気に入りに登録してから使用する。

ブックマーク

とりあえず、現在はテスト中。

まもなく配ります。

ブックマークレットを登録する

今回制作したプラグインにはブックマークレットがついている。
これを使用するには、以下のようにリンクの部分をドラッグ&ドロップする。

ブックマークを使う

ブックマークを実際に使用するには、さきほど登録したブックマークレットをクリックする。

ブックマーク(WordPressのリンク)に追加する

ブックマークの下の方に “Add Bookmark Here…” というリンクがあるのでそれをクリックすると、WordPressのリンクの追加画面に移動する。

リンク先のURLやタイトルは自動的に入力されるので、必要に応じてカテゴリー等を登録すればブックマークレット側に反映される。

ちなみに、リンクの登録を行うためのインターフェースはログインユーザー以外には表示されない。

WordPressのプラグインで任意のURLのページを追加する

WordPressのプラグインを作る際に、任意のURLでページを追加したい時がある。

たとえば。

  • テーマに関係なくカスタムなスタイルシートを適用したい。
  • ショッピングカートなど独自の機能をもった動的なページを作りたい。

など。

あと、AJAXアプリケーションをWordPressに実装する場合も、この手を使用する場合が多い。

この方法のメリット

  • まったく独自の機能を持ったページでも、WordPressの初期設定や各種の関数にアクセスできる。
  • WordPressのパーマリンク設定やディレクトリ構造などに依存しない。
  • URLに/wp-content/pluginsとか/wp-content/themesとかが入らない。

WordPressには、update_option()とかget_option()などの便利な関数や、MySQLでプリペアードなSQLを記述するためのクラスが組み込まれており、これらの関数を利用出来ることはとてもメリットが大きい。

任意のURLのページを作成するには?

WordPressに任意のURLを作成するには、$wp_rewriteというグローバル変数に正規表現のURLのパターンなどを定義する必要がある。

あと、いくつかのアクションフックの定義も必要で、これらの処理は結構めんどくさい。

AddRewriteRulesクラス

とうわけで、AddRewriteRulesクラスというのを作成した。

これは上述した処理をシンプルに実装するためのクラスで、以下のような比較的簡単なソースで独自のURLでページを出力することができる。

以下のソースをプラグインとして適用すると、/mystyle.cssにアクセスした際にCSSが表示される。

new AddRewriteRules(
    'mystyle.css$',
    'mystyle',
    'callback_function'
);

function callback_function()
{
  header('Content-type: text/css; charset=UTF-8');
  echo 'body {background-color: #ff0000}';
  exit;
}

使用方法

AddRewriteRulesクラスには、以下のパラメータを定義する。

  1. Rewriteルールに定義するURLの正規表現
  2. WordPressに内部的に渡されるクエリー文字列
  3. そのURLでアクセスがあった際に実行されるコールバック関数

ソース

以下のソースを任意のファイル名で保存して、requireしてください。

<?php

class AddRewriteRules{

    private $rule     = null;
    private $query    = null;
    private $callback = null;

    function __construct($rule, $query, $callback){
        $this->rule     = $rule;
        $this->query    = $query;
        $this->callback = $callback;
        add_filter('query_vars', array(&$this, 'query_vars'));
        add_filter('rewrite_rules_array', array(&$this, 'rewrite_rules_array'));
        add_action('init', array(&$this, 'init'));
        add_action('wp', array(&$this, 'wp'));
    }

    public function init()
    {
        global $wp_rewrite;
        $rules = $wp_rewrite->wp_rewrite_rules();
        if (!isset($rules[$this->rule])) {
            $wp_rewrite->flush_rules();
        }
    }

    public function rewrite_rules_array($rules)
    {
        global $wp_rewrite;
        $new_rules[$this->rule] = $wp_rewrite->index . '?'.$this->query.'=1';
        $rules = array_merge($new_rules, $rules);
        return $rules;
    }

    public function query_vars($vars)
    {
        $vars[] = $this->query;
        return $vars;
    }

    public function wp()
    {
        if (get_query_var($this->query)) {
            call_user_func($this->callback);
        }
    }
}

?>