LoginSignup
13
10

More than 5 years have passed since last update.

必要があればURLエンコーディングしたい

Last updated at Posted at 2014-12-25

ChromeとSafariでアドレスバーの振る舞いに違う部分があるのにお気づきだろうか?それぞれのブラウザで、Wikipediaの日本語のページにアクセスし、アドレスバーのURLをコピーして、エディタに貼り付けてみると違いに気づく。実際に試した方のエディタはこのような結果になっているはずだ:

Chrome: http://ja.wikipedia.org/wiki/%E6%97%A5%E6%9C%AC%E8%AA%9E
Safari: http://ja.wikipedia.org/wiki/日本語

ご覧のようにChromeはパーセントエンコーディングしたURLになるが、一方のSafariはエンコーディングしていないURLになる。

最近のブラウザは賢く、URLがエンコードされていないものであっても、ちゃんと解釈して意図したページを開いてくれる。なので、もし、Safariユーザが投稿の本文に日本語がそのままのURLを貼り付けても、ブラウザのおかげで問題になることは少ない。

しかし、ユーザが入力したURLを元にそのページにアクセスしようとするプログラムで単純なものは、エンコードされたURLでなければ失敗してしまう恐れがある。開発者はURLエンコーディングされていなければ、正しくURLエンコードして、そういったプログラムに渡すようにしなければならない。

http://ja.wikipedia.org/wiki/文字という入力を受け取ったら、http://ja.wikipedia.org/wiki/%E6%96%87%E5%AD%97に変換するが、入力がhttp://ja.wikipedia.org/wiki/%E6%96%87%E5%AD%97だったら何もしない。そのような関数が必要になる。

Scalaでこれを実現する関数を作ってみた。下に示したIdempotentURLEncoderがその実装になる:

IdempotentURLEncoder.scala
import java.net.URL
import scala.util.parsing.combinator.RegexParsers

object IdempotentURLEncoder extends RegexParsers {
  override def skipWhitespace = false
  private def segment = rep(char)
  private def char = unreserved | escape | any ^^ { java.net.URLEncoder.encode(_, "UTF-8") }
  private def unreserved = """[A-Za-z0-9._~!$&'()*+,;=:@-]""".r
  private def escape = """%[A-Fa-f0-9]{2}""".r
  private def any = """.""".r
  private def encodeSegment(input: String): String = parseAll(segment, input).get.mkString
  private def encodeSearch(input: String): String = encodeSegment(input)
  def encode(url: String): String = {
    val u = new URL(url)
    val path = u.getPath.split("/", -1).map(encodeSegment).mkString("/")
    val query = u.getQuery match {
      case null      => ""
      case q: String => "?" + encodeSearch(q)
    }
    val hash = u.getRef match {
      case null      => ""
      case h: String => "#" + encodeSegment(h)
    }
    s"${u.getProtocol}://${u.getAuthority}$path$query$hash"
  }
}

このエンコーダーは冪等性がある。エンコードする必要があるときだけ文字を置換するので、一度エンコードした文字が再びエンコードされることがない。次のコードのurl1url2は同じ値になる。

import IdempotentURLEncoder._
val url1 = encode("http://ja.wikipedia.org/wiki/文字")
val url2 = encode(encode("http://ja.wikipedia.org/wiki/文字"))

他にも、エンコード後の文字とエンコード前の文字が混在しているURLでも、未エンコードの文字だけ変換するような振る舞いもあるが、詳しい振る舞いについてはテストコードで提示しておく↓

IdempotentURLEncoderSpec
import org.scalatest.{ FunSuite, Matchers }

class IdempotentURLEncoderSpec extends FunSuite with Matchers {
  import IdempotentURLEncoder._

  test("Idempotent operation") {
    val url = "http://ja.wikipedia.org/wiki/文字"
    assert(encode(url) == encode(encode(url)))
    assert(encode(url) == encode(encode(encode(url))))
  }

  test("Segment encoding") {
    encode("http://ja.wikipedia.org/wiki/文字")
      .shouldBe("http://ja.wikipedia.org/wiki/%E6%96%87%E5%AD%97")
  }

  test("Query string encoding") {
    encode("http://qiita.com/search?utf8=✓&sort=rel&q=開発&sort=rel")
      .shouldBe("http://qiita.com/search?utf8=%E2%9C%93&sort=rel&q=%E9%96%8B%E7%99%BA&sort=rel")
  }

  test("Hash encoding") {
    encode("https://www.google.co.jp/#q=文字")
      .shouldBe("https://www.google.co.jp/#q=%E6%96%87%E5%AD%97")
  }

  test("Partial encoding") {
    encode("http://en.wiktionary.org/wiki/français")
      .shouldBe("http://en.wiktionary.org/wiki/fran%C3%A7ais")
  }

  test("Space is encoded as +") {
    encode("http://example.com/foo bar buz")
      .shouldBe("http://example.com/foo+bar+buz")
  }

  test("Multibyte domain names are not supported yet :(") {
    encode("http://日本語.jp")
      .shouldBe("http://日本語.jp")
  }
}

エンコーダの実装にはScalaのパーサコンビネータを使ったが、これについては以前書いた投稿『面倒くさいパーサの実装もDSLで書くだけ!そう、Scalaならね - Qiita』で触れたので、ここでは割愛する。

13
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10