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:encode | 0.032 | 0.091 |
hunchentoot:url-encode | 4.120 | 11.356 |
java.net.URLEncoder | 0.126 | 0.402 |
【URLデコード】 | 『心』のデコード時間(秒) | Apacheログのデコード時間(秒) |
url:decode | 0.039 | 0.246*1 |
hunchentoot:url-decode | 0.103 | 0.274 |
java.net.URLDecoder | 0.062 | 0.301 |
*1:ちなみに、この内の0.18秒くらいは、sb-ext:octets-to-string関数の処理に費やされている