Home JavaScript Greasemonkey PHP

Simple Shorten URL2010-01-29


自前で短縮 URL を提供したい場合の、超シンプルな実装です。複雑な機能はいっさいない代わりに、3 つのファイルをアップするだけで DB の設定すら不要で簡単に動きます。
PHP や .htaccess、サーバへのファイルアップなどについては、あらかじめ知識がある事を前提としています。

必須要件

  • PHP 5
  • .htaccess 有効
  • mod_rewrite 有効

向いている

  • 自分専用の短縮 URL を簡単に使いたい
  • DB とか用意したくない / 出来ない
  • リンクの解析機能とか要らない

向いていない

  • 不特定多数に公開する
  • 同一 URL の短縮結果が常に同じであってほしい
  • リンクの解析機能が要る

インストール

下記の 3 つのファイルを用意します。このうち、index.php の冒頭には設定が書いてあるので、その部分の編集は必須です。必要な部分を編集したら、サーバにアップします。
データファイルを保存するので、アップするディレクトリには書き込み権限を付与しておいて下さい。
[まとめてダウンロード]

≪.htaccess≫
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /expand.php?key=$1 [L]
</IfModule>
短縮 URL の任意のパスを受け取り、expand.php に受け渡すための mod_rewrite 向け設定が書いてあります。ドメイン直下にスクリプトが置けない場合、RewriteRule を書き換えて対応する必要があります。
例えば、http://otchy.net/s/XXX なら以下のようにします。
RewriteRule ^s/(.*)$ /s/expand.php?key=$1 [L]

≪expand.php≫
<?php
$key = $_GET['key'];
$file = 'dat/' . implode('/', str_split($key)) . '/dat';
if (is_file($file)) {
    $url = file_get_contents($file);
    header('HTTP/1.1 301 Moved Permanently');
    header('Location: ' . $url);
} else {
    header('HTTP/1.1 404 File Not Found');
}
?>
シンプルにリダイレクトするだけのスクリプトです。

≪index.php≫
<?php
define('OWN', 'http://otchy.net/');
define('TARGET', 'https://www.otchy.net/');
define('LEN', 3);
define('CHARS', '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
define('EXP', strlen(CHARS));
function shorten() {
    $url = $_GET['url'];
    $pos = strpos($url, TARGET);
    if ($pos !== 0) {
        global $message;
        $message = 'Input URL for "' . TARGET . '".';
        return FALSE;
    }
    $dir = nextDir();
    $file = $dir . '/dat';
    $fp = fopen($file, 'w');
    fputs($fp, $url);
    fclose($fp);
    $chars = explode('/', $dir);
    array_shift($chars);
    array_shift($chars);
    return OWN . implode($chars);
}
function nextDir() {
    $dir = 'dat';
    $id = 0;
    for ($i=LEN-1; $i>=0; $i--) {
        $num = currentNum($dir);
        $id += $num * pow(EXP, $i);
        $dir .= '/' . substr(CHARS, $num, 1);
    }
    $id++;
    $dir = '';
    for ($i=0; $i<LEN; $i++) {
        $num = $id % EXP;
        $dir = '/' . substr(CHARS, $num, 1) . $dir;
        $id = intval($id / EXP);
    }
    $dir = 'dat' . $dir;
    $dirs = explode('/', $dir);
    $dir = '.';
    foreach ($dirs as $d) {
        $dir .= '/' . $d;
        if (!is_dir($dir)) {
            mkdir($dir);
        }
    }
    return $dir;
}
function currentNum($dir) {
    $files = scandir($dir);
    $char = $files[count($files)-1];
    if ($char == '..') return 0;
    return strpos(CHARS, $char);
}
$url = FALSE;
if ($_GET['submit'] == 'shorten' || strlen($_GET['callback']) > 0) {
    $url = shorten();
    $callback = $_GET['callback'];
    if (strlen($callback) > 0) {
        $json = '';
        if ($url === FALSE) {
            $json = '{errmsg:' . "'" . $message . "'" . '}';
        } else {
            $json = '{url:"' . $url . '"}';
        }
        echo $callback . '(' . $json . ');';
        exit;
    }
}
?>
<html>
<head>
</head>
<body>
<div style="color:red;"><?php echo $message ?></div>
<form method="GET">
URL<input name="url" value="<?php echo htmlspecialchars($_GET['url']) ?>" /><input type="submit" name="submit" value="shorten" />
</form>
<?php if ($url !== FALSE) { ?>
<div style="color:green;">
Success shorten: <?php echo htmlspecialchars($_GET['url']) ?> to <a href="<?php echo $url ?>" target="_blank"><?php echo $url ?></a>
</div>
<?php } ?>
<div>
Bookmarklet:<a href="javascript:(function(w,d,s){if(!w.ssurl){w.ssurl=function(o){if(o.errmsg){alert(o.errmsg);}else{prompt('Shorten!',o.url);}}}s=d.createElement('script');s.src='<?php echo OWN ?>?url='+location.href+'&callback=ssurl';d.getElementsByTagName('head')[0].appendChild(s);})(window,document);">Shorten!</a>
</div>
</body>
</html>
<?php
冒頭の定数を必要に応じて書き換える必要があります。
OWN:短縮 URL を提供する URL です。
TARGET:短縮対象となる URL です。これ以下の URL のみ短縮対象として受け入れます。
LEN:ハッシュの長さです。62 進数で管理されるので、普通の利用だったら 3 桁で十分です。3 桁で、最大 238,328 (=62×62×62) 件の URL を短縮出来ます。それで不足する場合は数値を大きくして下さい。
CHARS:ハッシュに使用する文字です。通常は変更の必要がありませんが、サーバが Windows の場合は大文字と小文字が区別されないため、a〜z を削除する必要があります。この時は 36 進数でハッシュが管理されるので、同時に LEN の値も大きくした方が良いです。

フォームのデザインを変更したい場合は、<html>以降を修正して下さい。

動作確認

3 つのファイルがアップ出来たら、OWN で設定した URL を開くと URL 短縮用のフォームが表示されるはずなので、そこで動作確認が出来ます。
一度短縮 URL が作成されると、インストールディレクトリ以下に dat ディレクトリが生成されます。この dat ディレクトリをバックアップする事でデータのバックアップが出来ると同時に、不要な場合は削除する事で、最初の状態に戻ります。

また、簡易 API として、GET のパラメータに callback を渡すと、JSONP として動作するようになっています。
otchy.net の例だと、http://otchy.net/?url=https://www.otchy.net/&callback=funcName のような感じです。
エラー時は {errmsg:'error message'} が callback に渡され、短縮成功時は {url: 'shorten url'} が渡されます。

これを利用したブックマークレットもセットで作ってあるので、ブックマークレットをセットアップしたら、短縮したいページを開いた状態でブックマークレットを起動すると、短縮された URL を取得する事が出来ます。

デメリット

ハッシュをランダム化するなどの処理は行っていないため、前後の短縮 URL が簡単に類推出来ます。また、同一 URL を短縮しても常に新しいハッシュを生成するため、アクセスログで重複を取り除いたりするのが厄介です。
これらのデメリットが容認出来ない場合は、利用をお勧めしません。

技術的なトピック

ソースを読めば一目瞭然ですが、DB を使わない代わりに、ハッシュの文字を 1 文字ずつに分解したディレクトリ階層に情報を保存しています。
実際の動作については、ファイルシステムの実装依存ですが、このようにディレクトリを走査するとファイルシステムが一種の DB インデックスのように働く事が期待され、対象の URL の数がかなり大きくなっても高速に動作するはずです。

ライセンス

ライセンスは MIT License (日本版 wikipedia の項) で公開します。