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のサーバーに日本語のファイル名のファイルなんかおいてしまったら、怖いことになるかもしれません。