iOSでURLエンコード

iOSでネットワークプログラミングをする上でかかせない、URLエンコーディング。 なんだか気になる動作もあるので、ちょっとまとめてみました。

URLエンコードとは

URLエンコード(URL encode)とは、文字列をhttp://www.apple.com/からhttp%3A%2F%2Fwww.apple.com%2Fのように変換することです。 URLエンコーディング(URL encoding)、パーセントエンコーディング (%-Encoding)とよばれることもあります。 (逆の変換はURLデコード(URL decode)とよばれます。)

なぜ URLエンコードをしなくてはいけないのかというと、URLで使える文字が決まっているからです。 変換方法についてはRFCで決められています。

URLエンコーディングの歴史

URLエンコーディングに関連するRFCはいくつかあるんですが、主なものはこの3つ。

RFC1738が1994年、RFC2396が1998年、RFC3986が2005年に決まっていて、それぞれのRFCで微妙に変換方法が違います。

RFC1738では、こう定義されています。

unreserved     = alpha | digit | safe | extra
safe           = "$" | "-" | "_" | "." | "+"
extra          = "!" | "*" | "'" | "(" | ")" | ","

reserved       = ";" | "/" | "?" | ":" | "@" | "&" | "="

unreservedはURLにそのまま使っていい文字で、alpha(アルファベット大文字小文字)とdigit(数字)と「$-_.+」の記号と「!*'(),」の記号が定義されてます。 reservedはURLエンコーディングで変換しなくてはいけない文字で、「;/?:@&=」になります。

こんな記述もあり、「%{}|^~[]`」もURLエンコーディングするように推奨されていました。

The character "%" is unsafe because it is used for encodings of other characters. Other characters are unsafe because gateways and other transport agents are known to sometimes modify such characters. These characters are "{", "}", "|", "\", "^", "~", "[", "]", and "`".

その次のRFC2396ではこうなりました。

unreserved  = alphanum | mark
mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"

reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
                    "$" | ","

URLにそのまま使っていいunreservedはalphanum(アルファベット大文字小文字、数字)と「-_.!~*'()」で定義されています。 URLエンコーディングで変換しなくてはいけないreservedは「;/?:@&=+$,」になりました。

最終のRFC3896ではこうなっています。

unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"


reserved    = gen-delims / sub-delims
gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"

sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
                  / "*" / "+" / "," / ";" / "="

URLにそのまま使っていいunreservedはALPHA(アルファベット大文字小文字)とDIGIT(数字)と「-._~」で定義されています。 URLエンコーディングで変換しなくてはいけないreservedは「:/?#@!$&'()*+,;=」になりました。

iOSでURLエンコード

さて、iOSで文字列のURLエンコーディングをする場合には CFURLCreateStringByAddingPercentEscapes を使うのが一般的です。 さきほどRFC3896にでてきたreservedの文字列、「:/?#@!$&'()*+,;=」を変換するように指定します。 「http://www.toyship.org/~user/?new-param=test_data!for%といしっぷ」という文字列を変換すると、日本語文字と指定した記号が変換されて「http%3A%2F%2Fwww.toyship.org%2F~user%2F%3Fnew-param%3Dtest_data%21for%25%E3%81%A8%E3%81%84%E3%81%97%E3%81%A3%E3%81%B7」になりました。

NSString* inputString = @"http://www.toyship.org/~user/?new-param=test_data!for%といしっぷ";
 
CFStringRef originalString = (__bridge CFStringRef)inputString;
    
CFStringRef encodedString = CFURLCreateStringByAddingPercentEscapes(
                                                                        kCFAllocatorDefault,
                                                                        originalString,
                                                                        NULL,
                                                                        CFSTR(":/?#[]@!$&'()*+,;="),
                                                                        kCFStringEncodingUTF8);
//encodedString:
//http%3A%2F%2Fwww.toyship.org%2F~user%2F%3Fnew-param%3Dtest_data%21for%25%E3%81%A8%E3%81%84%E3%81%97%E3%81%A3%E3%81%B7
    
CFStringRef decodedString = CFURLCreateStringByReplacingPercentEscapesUsingEncoding(
                                                                                        kCFAllocatorDefault,
                                                                                        encodedString,
                                                                                        CFSTR(""),
                                                                                        kCFStringEncodingUTF8);
//decodedString:
//http://www.toyship.org/~user/?new-param=test_data!for%といしっぷ

ちなみに、NSStringのそれらしい関数stringByAddingPercentEscapesUsingEncodingで変換するとNGです。 「:/」など、肝心の記号を変換してくれません。

NSString* inputString = @"http://www.toyship.org/~user/?new-param=test_data!for%といしっぷ";
 
NSString* encodeString = [inputString
                              stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    
// encodeString:
//http://www.toyship.org/~user/?new-param=test_data!for%25%E3%81%A8%E3%81%84%E3%81%97%E3%81%A3%E3%81%B7
 
NSString* decodeString = [encodeString stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// decodeString:
// http://www.toyship.org/~user/?new-param=test_data!for%といしっぷ

また、iOS7から使えるようになったstringByAddingPercentEncodingWithAllowedCharactersでは、ちょっと楽にURLエンコーディングできるかのように見えるんですが、「http://www.toyship.org/~user/?new-param=test_data!for%といしっぷ」という文字列のうち、本来変換しなくてもいいはずの「.」や「~」も変換されています。

NSString* inputString = @"http://www.toyship.org/~user/?new-param=test_data!for%といしっぷ";
 
NSString* encodeString = [inputString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet alphanumericCharacterSet]];
 
//encodeString:
//http%3A%2F%2Fwww%2Etoyship%2Eorg%2F%7Euser%2F%3Fnew%2Dparam%3Dtest%5Fdata%21for%25%E3%81%A8%E3%81%84%E3%81%97%E3%81%A3%E3%81%B7
 
NSString* decodeString = [encodeString stringByRemovingPercentEncoding];
 
//decodeString:
//http://www.toyship.org/~user/?new-param=test_data!for%といしっぷ

まとめ

iOSでのURLエンコーディングですが、iOS8ではまた動きが変わるかもしれませんが、とりあえずは当分今までの CFURLCreateStringByAddingPercentEscapes を使った方がよさそうな気がします。

あと、上記ではUTF-8で処理していますが、URLエンコードは文字コードにも依存します。 サーバーの文字コードが違う場合には注意してくださいね。 昔ならともかく、今はすべてのURLエンコードをUTF-8で処理してしまっても大丈夫だと思いますが、古いEUC-JPのサーバーに日本語のファイル名のファイルなんかおいてしまったら、怖いことになるかもしれません。

参考リンク

URL Loading System Programming Guide:Encoding URL Data