URLエンコード/デコード

URLのエンコード/デコード処理は、時々必要になって、その度に(適当に)自作しているものの一つなので、今回少し真面目に作成してパッケージにまとめてみた。
仕様は『URLEncoder (Java Platform SE 6)』を参考にさせてもらった。

(defpackage :url
  (:use :common-lisp)
  (:export :encode 
           :decode))
(in-package :url)

;;;;;;;;;;;;;;;;;;;
;;;; 宣言及び型定義
(declaim (optimize (speed 3) (debug 0) (safety 0) (compilation-speed 0)))
(declaim (inline safe-code-p digit-hexchar write-hex hexchar-digit))

(deftype quartet () '(unsigned-byte 4))
(deftype octet   () '(unsigned-byte 8))
(deftype simple-octets () '(simple-array octet))


;;;;;;;;;;;;;;;;;
;;;; エンコード系
;; 安全(= URLエンコードが不要)な文字かどうかの判定
;; 参照: http://java.sun.com/javase/ja/6/docs/ja/api/java/net/URLEncoder.html
(defun safe-code-p (code)
  (declare (octet code))
  (or (<= (char-code #\a) code (char-code #\z))
      (<= (char-code #\A) code (char-code #\Z))
      (<= (char-code #\0) code (char-code #\9))
      (case code 
        (#.(map 'list #'char-code ".-*_") t))))

;; 速度を除いて、(digit-char digit 16)と等価
(defun digit-hexchar (digit)
  (declare (quartet digit))
  (schar "0123456789ABCDEF" digit))

;; 速度を除いて、(format out "%~2,'0X" code)と等価
(defun write-hex (code out)
  (declare (octet code))
  (write-char #\% out)
  (write-char (digit-hexchar (ldb (byte 4 4) code)) out)
  (write-char (digit-hexchar (ldb (byte 4 0) code)) out))

(defun encode (url &optional (#1=external-format :utf8))
  (declare (string url)
           (sb-ext:muffle-conditions sb-ext:compiler-note))
  (with-output-to-string (out)
    (loop FOR code ACROSS (the simple-octets (sb-ext:string-to-octets url :external-format #1#)) DO
      (cond ((safe-code-p code)           (write-char (code-char code) out))     
            ((= code (char-code #\Space)) (write-char #\+              out))     
            (t                            (write-hex  code             out))))))


;;;;;;;;;;;;;;;
;;;; デコード系
;; 16進数の文字を数値に変換する
(declaim (ftype (function (standard-char) quartet) hexchar-digit))
(defun hexchar-digit (ch)
  (cond ((char<= #\0 ch #\9)        (- (char-code ch) (char-code #\0)))
        ((char<= #\a ch #\f)  (+ 10 (- (char-code ch) (char-code #\a))))
        ((char<= #\A ch #\F)  (+ 10 (- (char-code ch) (char-code #\A))))
        (t  (error "Non-hex character(~S) encountered" ch))))
         
(defun decode (url &optional (#1=external-format :utf8) &aux (len (length url)))
  (declare (string url)
           (sb-ext:muffle-conditions sb-ext:compiler-note))
  (let ((ary (make-array len :element-type 'octet :fill-pointer 0))) ; 大きめの配列をフィルポインタ付きで作成
    (loop FOR i fixnum FROM 0 BELOW len DO
      (case (char url i)
        (#\%       (vector-push (+ (ash #2=(hexchar-digit (char url (incf i))) 4) #2#) ary))
        (#\+       (vector-push (char-code #\Space)                                    ary))
        (otherwise (vector-push (char-code (char url i))                               ary))))
    (sb-ext:octets-to-string ary :external-format #1#)))

ベンチマーク

使用例を兼ねて、ベンチマーク結果を載せておく。
比較対象は、hunchentootのurl-encode/url-decode関数とjavaのURLEncoder.encode/URLDecoder.decodeメソッド

;;;; common lisp用のベンチマークコード
;;; データ準備
;; ファイル読込み関数
(defun read-file (path)
  (sb-ext:octets-to-string
    (with-open-file (in path :element-type '(unsigned-byte 8))
      (let ((as (make-array (file-length in) :element-type '(unsigned-byte 8))))
	        (read-sequence as in)
	as))))

;; エンコード/デコード対象の文字列用意
(defvar *kokoro* (read-file "/path/to/kokoro"))             ; 夏目漱石の『心』:        542KB
(defvar *log*    (read-file "/path/to/apache.log"))         ; apacheのログファイル:    2.4MB
(defvar *enc-kokoro* (read-file "/path/to/encoded-kokoro")) ; 『心』エンコード版:      1.6MB
(defvar *enc-log*    (read-file "/path/to/encoded-log"))    ; apacheログ エンコード版: 3.2MB


;;; 計時方法
;; url
(time (progn (url:encode *kokoro* :utf8) 'done))
(time (progn (url:decode *enc-kokoro* :utf8) 'done))

;; hunchentoot
(time (progn (hunchentoot:url-encode *kokoro* :utf8) 'done))
(time (progn (hunchentoot:url-decode *enc-kokoro* :utf8) 'done))
// Java用のベンチマークコード

/*****************************/
/** ファイル名: Decode.java **/
import java.net.URLDecoder;
import java.io.*;
import java.util.*;

class Decode {
    public static void main(String[] args) throws Exception {
        final String filename = args[0];
        StringBuilder sb = new StringBuilder();

        BufferedReader br = new BufferedReader(new FileReader(new File(filename)));
        String s;
        while((s = br.readLine()) != null) 
                sb.append(s+"\n");
        
        final String text = sb.toString();
        final long beg_t = System.currentTimeMillis();
        URLDecoder.decode(text,"UTF-8");
        final long end_t = System.currentTimeMillis();
        System.out.println((double)(end_t-beg_t)/1000.0 + " sec");
    }
}

/*****************************/
/** ファイル名: Encode.java **/
import java.net.URLEncoder;
import java.io.*;
import java.util.*;

class Encode {
    public static void main(String[] args) throws Exception {
        final String filename = args[0];
        StringBuilder sb = new StringBuilder();

        BufferedReader br = new BufferedReader(new FileReader(new File(filename)));
        String s;
        while((s = br.readLine()) != null) 
                sb.append(s+"\n");
        
        final String text = sb.toString();
        final long beg_t = System.currentTimeMillis();
        URLEncoder.encode(text,"UTF-8");
        final long end_t = System.currentTimeMillis();
        System.out.println((double)(end_t-beg_t)/1000.0 + " sec");
    }
}

/**********************/
/** コンパイルと実行 **/
// URLエンコード
$ javac Encode.java
$ java -cp . Encode /path/to/kokoro

// URLデコード
$ javac Decode.java
$ java -cp . Decode /path/to/encoded-kokoro

// javaのバージョン
$ java -version
java version "1.6.0_17"
Java(TM) SE Runtime Environment (build 1.6.0_17-b04)
Java HotSpot(TM) Server VM (build 14.3-b01, mixed mode)


ベンチマーク結果:

【URLエンコード『心』のエンコード時間(秒)Apacheログのエンコード時間(秒)
url:encode0.0320.091
hunchentoot:url-encode4.12011.356
java.net.URLEncoder0.1260.402

【URLデコード】『心』のデコード時間(秒)Apacheログのデコード時間(秒)
url:decode0.0390.246*1
hunchentoot:url-decode0.1030.274
java.net.URLDecoder0.0620.301
そこそこ速い。

*1:ちなみに、この内の0.18秒くらいは、sb-ext:octets-to-string関数の処理に費やされている