LoginSignup
0
0

More than 1 year has passed since last update.

【Ruby のまずいコード】HTML エスケープ

Last updated at Posted at 2021-12-06

お題

引数として与えられた文字列を HTML エスケープするコードを書いてください。
HTML エスケープとは,以下の変換を施すこととします。

  • &&
  • <&lt;
  • >&gt;
  • "&quot;
  • '&#39;

コード

コード 1

def escape_html(str)
  result = ""
  str.each_char do |char|
    case char
    when '&'
      result << '&amp;'
    when '<'
      result << '&lt;'
    when '>'
      result << '&gt;'
    when '"'
      result << '&quot;'
    when "'"
      result << '&#39;'
    else
      result << char
    end
  end
  result
end

コード 2

def escape_html(str)
  escape_table = {
    '&' => '&amp;',
    '<' => '&lt;',
    '>' => '&gt;',
    '"' => '&quot;',
    "'" => '&#39;'
  }
  str.gsub(/[&<>"']/){ escape_table[$&] }
end

問題点

コード 1

コード 1 は,ある意味で素直というか素朴なコードですね。プログラミング言語をどれか一つ新しく学び始めたとき,誰でもこれに近い発想のコードを書くのではないでしょうか。

Ruby らしい点といえば,String#each_char というイテレーターメソッドを使っていることや,条件分岐に case を使っていること,また,文字列を破壊的に結合する << を使っていることでしょうか。
これらを知らなければ

for i in 0..(str.length - 1)
  char = str[i]
  if char == '&'
    result = result + '&amp;'
  elsif char == '<'
  # ...

のようなコードを書いてしまうかもしれません。

いずれにしても,コード 1 は効率が悪く,長すぎます。

str が 1000 文字なら,1000 回もループします。str の中にエスケープすべき文字が一つも無くても 1000 回ループするのです。回るたびに文字列の結合が発生します。

コード 2

コード 2 は,コード 1 よりは簡潔で,効率も良くなっています。

最大のポイントは String#gsub を用いたことですね。エスケープすべき文字を正規表現 /[&<>"']/ で表し,これにマッチする箇所だけを変換します。str が 1000 文字でも,エスケープすべき文字が 3 個しかなければ,ブロックは 3 回しか呼ばれません。
もう一つのポイントは,条件分岐を用いず,ハッシュを使って変換していることです。

このコードは,String#gsub の使い方として最適とは言えません(後述)。

メソッドが呼ばれるたびにハッシュ(escape_table)が生成される点も気になります。
メソッドが 1000 回呼ばれたらこのハッシュは 1000 回生成されます。使い捨てなのでガーベジコレクションの対象になります。

このハッシュ式をうっかり gsub のブロック内に書こうものなら,ブロックが呼ばれるたびに新たにハッシュを生成してしまいます。

こういった効率の悪さが現実のプログラムで性能低下を実際に引き起こすかどうかはケースバイケースです。
短い文字列に対してメソッドが数回呼ばれる程度なら,実行時間の差は測定限界を下回るでしょう。
とはいえ,効率の良いやり方があるなら最初からそのように書きたいものです。

改善

小手先の改善

コード 2 を少し改善してみます。
ハッシュが毎回生成される問題を回避する簡単な方法は,ハッシュ式をメソッドの外に出すことです。

それから,String#gsub は第二引数にハッシュを与える用法を採用しましょう。
使い方はリンク先を参照ください。

すると,コードはこんなふうに書けます。

ESCAPE_TABLE = {
  '&' => '&amp;',
  '<' => '&lt;',
  '>' => '&gt;',
  '"' => '&quot;',
  "'" => '&#39;'
}

def escape_html(str)
  str.gsub(/[&<>"']/, ESCAPE_TABLE)
end

欠点は,escape_html メソッドでのみ使う定数 ESCAPE_TABLE がメソッドの外で定義されていること。
内部で使うものがトップレベル1にあるのはあまりよいスタイルではありません。
本記事のテーマからは離れますが,モジュールを定義して

module HTMLUtil
  ESCAPE_TABLE = {
    # 云々
  }

  def self.escape_html(str)
    # 云々
  end
end

のようにすると,この点は改善されます。

最善策

実はこんなことをしなくても,標準添付ライブラリーに専用メソッド CGI.escapeHTML があるのでした(しかも高速)。
こんなふうに使います:

require "cgi/escape"

p CGI.escapeHTML("Q&A") # => "Q&amp;A"

ですので,メソッドを定義する必要すらありません。
既にあるものをそのまま使えばいいのでした。

このメソッドについては,ぜひ以下の拙記事もご覧ください。
素の Ruby で HTML エスケープするなら cgi/escapeが最強 - Qiita


  1. すべてのモジュール定義、クラス定義、メソッド定義の外側のこと。 

0
0
0

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
0
0