いろいろな言語での urlencode

Web アプリケーションを運用していると, GET パラメータから値を取得しなければならないことが多々あると思います.

URL エンコードの方法は, RFC 2396 - Uniform Resource Identifiers (URI): Generic Syntax(旧 RFC1738) で定められており, 基本的には, この定義に基づいてエンコードしなくてはなりません.

PHP には, この RFC2396 に基いて URL エンコード/デコードを行う rawurlencode/rawurldecode 関数があります.

PHP: rawurlencode - Manual / PHP: rawurldecode - Manual

-_. を除くすべての非アルファベット文字をパーセント 記号 (%)に続いて 2 つの 16 進数がある表現形式に 置き換えた文字列を返します。これは、文字定数が特殊な URL デリミタと して解釈されたり、URL デリミタが(いくつかの電子メールシステムのような) 転送メディアにより文字変換されて失われてしまったりすることが ないように、RFC 1738 で定められたエンコーディング方法です。

HTML 4.01 Specification では, 「application/x-www-form-urlencoded MIME 形式でエンコードするように」とあります.

HTML フォームでデータの受け渡しをする場合は, こちらの方法を用いなくてはなりません.

RFC2396 では, 「スペース(空白文字)は,『%20』にエンコードしなさい」とあるのですが, HTML(RFC1866)では, 「スペースは『+』に置き換えなさい」とあります.

プログラム言語における, urlencode/urldecode は, この解釈がまちまちで, PHP の場合, HTMLにて規定された application/x-www-form-urlencoded MIME 形式に準拠した urlencode/urldecode 関数があります.

PHP: urlencode - Manual / PHP: urldecode - Manual

-_. を除くすべての非英数文字が % 記号 (%)に続く二桁の数字で置き換えられ、 空白は + 記号(+)にエンコードされます。 同様の方法で、WWW のフォームからポストされたデータはエンコードされ、 application/x-www-form-urlencoded メディア型も同様です。歴史的な理由により、この関数は » RFC 1738 エンコード( rawurlencode() を参照してください) とは異なり、 空白を + 記号にエンコードします。

しかし, 残念なことに, ブラウザによって "%XX" の形式に変換する対象の文字はまちまちです.

Java の URLEncoder#encode(String, String) のソースコードには, 下記のようなコメントがあります.

        /* The list of characters that are not encoded has been
         * determined as follows:
         *
         * RFC 2396 states:
         * -----
         * Data characters that are allowed in a URI but do not have a
         * reserved purpose are called unreserved.  These include upper
         * and lower case letters, decimal digits, and a limited set of
         * punctuation marks and symbols.
         *
         * unreserved  = alphanum | mark
         *
         * mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
         *
         * Unreserved characters can be escaped without changing the
         * semantics of the URI, but this should not be done unless the
         * URI is being used in a context that does not allow the
         * unescaped character to appear.
         * -----
         *
         * It appears that both Netscape and Internet Explorer escape
         * all special characters from this list with the exception
         * of "-", "_", ".", "*". While it is not clear why they are
         * escaping the other characters, perhaps it is safest to
         * assume that there might be contexts in which the others
         * are unsafe if not escaped. Therefore, we will use the same
         * list. It is also noteworthy that this is consistent with
         * O'Reilly's "HTML: The Definitive Guide" (page 164).
         *
         * As a last note, Intenet Explorer does not encode the "@"
         * character which is clearly not unreserved according to the
         * RFC. We are being consistent with the RFC in this matter,
         * as is Netscape.
         *
         */

これによると, Netscape が "%XX" の形式に変換対象としない文字は,

"-", "_", ".", "*"

であり, 一方 Internet Explorer の場合は, 上記に加えて RFC で予約文字と規定している「@(アットマーク」も変換対象としません.

"-", "_", ".", "*", "@"

安全にデータを受け渡すためには, ブラウザの仕様に準拠した方が安全... ということで,
O'Reilly の "HTML: The Definitive Guide" の 164 ページに書いてある内容に従うことにしたようです.

そんなわけで, PHP の urlencode/urldecode と, Java の URLEncoder#encode(String, String)/URLDecoder#decode(String, String) は, 「*(アスタリスク)」の解釈が異なるため, 注意が必要です.

世の中では, PHP で書かれた Webアプリケーションが多いと思われますし, 他の言語で URLエンコードした内容を PHP で urldecode というケースは多々あると思います.

これを言語別に見てみましょう.

Java の場合

Java の場合, 前出のように「*(アスタリスク)」を変換対象としません.

PHP の urldecode は, urlencode 関数の変換対象文字が変換されずに入ってきたとしても, 何もせずに返すので, 問題は発生しないでしょう.

安心して URLEncoder#encode(String, String) を使用できます. 文字エンコーディングは, UTF-8 の使用が推奨されています.

http://sdc.sun.co.jp/java/docs/j2se/1.4/ja/docs/ja/api/java/net/URLEncoder.html

C# の場合

C# の場合は, 一般的に HttpServerUtility.UrlEncode(String) メソッドを使用します.
このメソッドの英数字以外の非変換対象文字は下記のようです.

"'", ".", "-", "*", ")", "(", "_"

これだけ見ると, 安心して使えそうなのですが, 実際に実行してみると, 16進数に変換すべき部分が小文字になってしまいます.

これでは PHP の urldecode で処理できませんので, 自前でエンコードしてやる必要があります.

private String urlEncode(String argment) 
{ 
    foreach (Match m in (new Regex("[^a-zA-Z0-9._-]")).Matches(argment)) 
        argment = argment.Replace(m.Value, "%" + BitConverter.GetBytes(m.Value[0])[0].ToString("X")); 
    return argment.Replace("%20", "+"); 
} 

HttpServerUtility.UrlEncode Method (System.Web) | Microsoft Docs

Perl の場合

URI::Escape という関数がありますが, RFC2396 に厳密に準拠しており, 空白文字を「+」に変換してくれません...

http://search.cpan.org/dist/URI/URI/Escape.pm

PHPJava, C# のように, 空白文字を「+」へ置換するには自前で処理を書かなくてはなりません.

下記のような正規表現PHP の urlencode と同じ振舞いになります.

$str =~ s/([^a-zA-Z0-9-_. ])/'%' . uc(unpack('H2', $1))/eg; 
$str =~ tr/ /+/; 

おわりに

たまたま, いろいろな言語で URL エンコードしたものを PHP で urldecode しなければならない案件があり, いろいろ調べたみたのですが, 言語により振舞いがまちまちで, とっても面倒です...

本来, 最終的には, ブラウザが encode/decode する部分なので, ブラウザの振舞いにできるだけ近づけた(しかもコメントに理由がしっかり書いてある) Java の実装が素直で良い気がします.