編集(管理者用) | 編集 | 差分 | 新規作成 | 一覧 | RSS | 表紙 | 検索 | 更新履歴

&の実体参照への変換

"&"の文字を実体参照に変換する場合に、数値文字参照の形で書かれた場合は避けるようにしてみる。

動機

PerlでCGIを作成する場合、HTMLで使用される特殊な文字を(安全のため)実体参照に変換する以下のような処理がよく使われると思います。

sub sanitize {
    my $arg = shift;
    $arg =~ s/&/&/g;
    $arg =~ s/</&lt;/g;
    $arg =~ s/>/&gt;/g;
    $arg =~ s/"/&quot;/g;
    return $arg;
}

しかし、例えばハートマーク♥(u+2665)を数値文字参照(&#9829; or &#x2665;)を使って入力したい、という要求に対しては、上記の処理では応えられないことになります。もしそれを実現するとしたら、「"&"は"&amp;"に変換するが、数値文字参照の形式で入力された場合はこの&を変換しない」という処理を行う必要があります。

Perlの正規表現には否定先読みの指定( (?!regexp) )があるので、これを使用して

sub sanitize {
    my $arg = shift;
    $arg =~ s/&(?!#\d+;)/&amp;/g;
    ...

とすれば、"&"のうち、そのあとに『"#"、数字の1字以上の並び、";"』が続かないものを変換、とすることができます。

しかしながら、UnicodeのうちXML, HTMLで使用できる文字コードには上限があり、またそれ以下のコードの中にも実際には使用できないものあるため、上記のような単純な正規表現では対処不十分な可能性があります。

というわけで、実際に使える文字コードの範囲をきちっと確認した上で、それにマッチする正規表現を作ることを考えてみました。

HTMLで有効な文字の範囲

HTML 4.01の場合。SGML宣言を参照しました。

十六進十進
x9 - xA9 - 10
xD13
x20 - x7E32 - 126
xA0 - xD7FF160 - 55295
xE000 - xFFFD57344 - 65533
x10000 - x10FFFF65536 - 1114111

XML(XHTML)との互換のため、xFFFE と xFFFF を除いています。詳しくは以下を参照。

十進数

まずは十進数形式の文字参照をそのまま入力できることを目標にしてみます。十六進形式の文字参照は対応していないブラウザもありそうなので。

※以下はメモ段階。

1-2桁

範囲: 9, 10, 13, 32 - 99

a. 9        9
b. 11, 13   1[03]
c. 32 - 39  3[2-9]
d. 40 - 99  [4-9][0-9]

a+b+c+d : 9|1[03]|3[2-9]|[4-9][0-9]

3桁

範囲: 100 - 126, 160 - 999

a. 100 - 119  1[01]\d
b. 120 - 126  12[0-6]
c. 160 - 199  1[6-9]\d
d. 200 - 999  [2-9]\d\d

a+c : 1[016-9]\d
a+b+c : 1([016-9]\d|2[0-6])

a+...+d :
  1([016-9]\d|2[0-6])|[2-9]\d\d

4桁

All OK. \d\d\d\d

5桁

範囲: 10000 - 55295, 57344 - 65533, 65536 - 99999

a. 10000 - 49999  [1-4]\d\d\d\d

b. 50000 - 54999  5[0-4]\d\d\d
c. 55000 - 55199  55[01]\d\d
d. 55200 - 55289  552[0-8]\d
e. 55290 - 55295  5529[0-5]

f. 57344 - 57349  5734[4-9]
g. 57350 - 57399  573[5-9]\d
h. 57400 - 57999  57[4-9]\d\d
i. 58000 - 59999  5[89]\d\d\d

j. 60000 - 64999  6[0-4]\d\d\d
k. 65000 - 65499  65[0-4]\d\d
l. 65500 - 65529  655[0-2]\d
m. 65530 - 65533  6553[0-3]

n. 65536 - 65539  6553[6-9]
o. 65540 - 65599  655[4-9]\d
p. 65600 - 65999  65[6-9]\d\d
q. 66000 - 69999  6[6-9]\d\d\d

r. 70000 - 99999  [7-9]\d\d\d\d

a+r :
  [1-47-9]\d\d\d\d

b+i   : 5[0-489]\d\d\d
c+d+e : 55([01]\d\d
           |2([0-8]\d
              |9[0-5]))
f+g+h : 57(3(4[4-9]
             |[5-9]\d)
           |[4-9]\d\d)

5から :
  5([0-489]\d\d\d
    |5([01]\d\d
       |2([0-8]\d
          |9[0-5]))
    |7(3(4[4-9]
         |[5-9]\d)
       |[4-9]\d\d))

j+q : 6[0-46-9]\d\d\d
k+p : 65[0-46-9]\d\d
l+o : 655[0-24-9]\d
m+n : 6553[0-36-9]

6から :
  6([0-46-9]\d\d\d
    |5([0-46-9]\d\d
       |5([0-24-9]\d
          |3[0-36-9])))

5桁まとめ :
  [1-47-9]\d\d\d\d
  |5([0-489]\d\d\d
     |5([01]\d\d
        |2([0-8]\d
           |9[0-5]))
     |7(3(4[4-9]
          |[5-9]\d)
        |[4-9]\d\d))
  |6([0-46-9]\d\d\d
     |5([0-46-9]\d\d
        |5([0-24-9]\d
           |3[0-36-9])))

6桁

All OK. \d\d\d\d\d\d

4桁の場合とまとめると、\d\d\d\d(\d\d)?

7桁

範囲: 1000000 - 1114111

a. 1000000 - 1099999  10\d\d\d\d\d
b. 1100000 - 1109999  110\d\d\d\d
c. 1110000 - 1113999  111[0-3]\d\d\d
d. 1114000 - 1114099  11140\d\d
e. 1114100 - 1114109  111410\d
f. 1114110 - 1114111  111411[01]

a+...+f :
  1(0\d\d\d\d\d
     |1(0\d\d\d\d
        |1([0-3]\d\d\d
           |4(0\d\d
              |1(0\d
                 |1[01])))))

十進数まとめ

0*(
  9|1[03]|3[2-9]|[4-9][0-9]
  |1([016-9]\d|2[0-6])|[2-9]\d\d
  |\d\d\d\d(\d\d)?
  |[1-47-9]\d\d\d\d
  |5([0-489]\d\d\d
     |5([01]\d\d
        |2([0-8]\d
           |9[0-5]))
     |7(3(4[4-9]
          |[5-9]\d)
        |[4-9]\d\d))
  |6([0-46-9]\d\d\d
     |5([0-46-9]\d\d
        |5([0-24-9]\d
           |3[0-36-9])))
  1(0\d\d\d\d\d
     |1(0\d\d\d\d
        |1([0-3]\d\d\d
           |4(0\d\d
              |1(0\d
                 |1[01])))))
)

十六進数

調子に乗って十六進数の場合も探ってみるのですよ。以下接頭辞のxは省略。

1-2桁

範囲: 9, A, D, 20 - 7E, A0 - FF

a. 9, A, D  [9ADad]
b. 20 - 6F  [2-6][\dA-Fa-f]
c. 70 - 7E  7[\dA-Ea-e]
d. A0 - FF  [A-Fa-f][\dA-Fa-f]

b+d : [2-6A-Fa-f][\dA-Fa-f]

a+b+c+d : 
  [9ADad]|[2-6A-Fa-f][\dA-Fa-f]|7[\dA-Ea-e]

3桁

All OK.

[\dA-Fa-f][\dA-Fa-f][\dA-Fa-f]

∴[\dA-Fa-f]{3}

4桁

範囲: 1000 - D7FF, E000 - FFFD

a. 1000 - CFFF  [\dA-Ca-c][\dA-Fa-f][\dA-Fa-f][\dA-Fa-f]
b. D000 - D7FF  [Dd][0-7][\dA-Fa-f][\dA-Fa-f]
c. E000 - EFFF  [Ee][\dA-Fa-f][\dA-Fa-f][\dA-Fa-f]
d. F000 - FEFF  [Ff][\dA-Ea-e][\dA-Fa-f][\dA-Fa-f]
e. FF00 - FFE0  [Ff]{2}[\dA-Ea-e][\dA-Fa-f]
f. FFF0 - FFFD  [Ff]{3}[\dA-Da-d]

a+b+c+d : ([\dA-Ca-c][\dA-Fa-f]
           |[Dd][0-7]
           |[Ee][\dA-Fa-f]
           |[Ff][\dA-Ea-e])[\dA-Fa-f][\dA-Fa-f]
e+f     : [Ff]{2}([\dA-Ea-e][\dA-Fa-f]
                  |[Ff][\dA-Da-d])

5桁

All OK.

[\dA-Fa-f][\dA-Fa-f][\dA-Fa-f][\dA-Fa-f][\dA-Fa-f]

∴[\dA-Fa-f]{5}

3桁と合わせると[\dA-Fa-f]{3}([\dA-Fa-f]{2})?

6桁

範囲: 100000 - 10FFFF

10[\dA-Fa-f][\dA-Fa-f][\dA-Fa-f][\dA-Fa-f]

∴    10[\dA-Fa-f]{4}

十六進数まとめ

x0*(
  [9ADad]|[2-6A-Fa-f][\dA-Fa-f]|7[\dA-Ea-e]
  |[\dA-Fa-f]{3}([\dA-Fa-f]{2})?
  |([\dA-Ca-c][\dA-Fa-f]
    |[Dd][0-7]
    |[Ee][\dA-Fa-f]
    |[Ff][\dA-Ea-e])[\dA-Fa-f][\dA-Fa-f]
  |[Ff]{2}([\dA-Ea-e][\dA-Fa-f]
           |[Ff][\dA-Da-d])
  |10[\dA-Fa-f]{4}
)

サンプルスクリプト

use strict;
use Benchmark;

my $a = <<__EOT__;
<">    NG    &#8;
<">    OK    &#9; &#10;
<">    NG    &#11; &#12;
<">    OK    &#13;
<">    NG    &#14; &#31;
<">    OK    &#32; &#99; &#100; &#126; &#160; &#999;
<">    OK    &#1000; &#9999; &#10000; &#55295;
<">    NG    &#55296; &#57343;
<">    OK    &#57344; &#65533;
<">    NG    &#65534; &#65535;
<">    OK    &#65536; &#99999; &#100000; &#999999;
<">    OK    &#1000000; &#1056768;
<">    NG    &#1056769;

<">    NG    &#x8;
<">    OK    &#x9; &#xA; &#xD;
<">    NG    &#xE; &xF; &#x10; &x1F;
<">    OK    &#x20; &#x7E; &#xA0; &#xFF;
<">    OK    &#x100; &#xFFF; &#x1000; &#xD7FF;
<">    OK    &#xd600; &#xe800; &#xF800; &#xff80; &#xfff8;
<">    NG    &#xD800;  &#xDFFF;
<">    OK    &#xE000; &#xFFFD; &#x10000; &#x108000;
<">    NG    &#x110000;
__EOT__

#print sanitize1($a), "\n";

# ベンチマーク用
timethese(2000000, {
    'test0' => 'sanitize0($a);',
    'test1' => 'sanitize1($a);',
});

# ふつうの変換
sub sanitize0 {
    my $arg = shift;
    $arg =~ s/&/&amp;/g;
    $arg =~ s/</&lt;/g;
    $arg =~ s/>/&gt;/g;
    $arg =~ s/"/&quot;/g;
    return $arg;
}

# 数値文字参照の場合はよける変換
sub sanitize1 {
    my $arg = shift;
    $arg =~ s/&(?!\#
        # 部分マッチの後方参照は必要ないため、
        # (a|b)ではなく(?:a|b)を使用
        (?:
            # 十進
            0*(?:
              9|1[03]|3[2-9]|[4-9][0-9]
              |1(?:[016-9]\d|2[0-6])|[2-9]\d\d
              |\d\d\d\d(?:\d\d)?
              |[1-47-9]\d\d\d\d
              |5(?:[0-489]\d\d\d
                   |5(?:[01]\d\d
                        |2(?:[0-8]\d
                             |9[0-5]))
                   |7(?:3(?:4[4-9]
                            |[5-9]\d)
                        |[4-9]\d\d))
              |6(?:[0-46-9]\d\d\d
                   |5(?:[0-46-9]\d\d
                        |5(?:[0-24-9]\d
                             |3[0-36-9])))
              |1(?:0\d\d\d\d\d
                   |1(?:0\d\d\d\d
                        |1(?:[0-3]\d\d\d
                             |4(?:0\d\d
                                  |1(?:0\d
                                       |1[01])))))
            )
        |
            # 十六進
            x0*(?:
              [9ADad]|[2-6A-Fa-f][\dA-Fa-f]|7[\dA-Ea-e]
              |[\dA-Fa-f]{3}(?:[\dA-Fa-f]{2})?
              |(?:[\dA-Ca-c][\dA-Fa-f]
                  |[Dd][0-7]
                  |[Ee][\dA-Fa-f]
                  |[Ff][\dA-Ea-e])[\dA-Fa-f][\dA-Fa-f]
              |[Ff]{2}(?:[\dA-Ea-e][\dA-Fa-f]
                    |[Ff][\dA-Da-d])
              |10[\dA-Fa-f]{4}
            )
        )
    ;)/&amp;/gx;
    $arg =~ s/</&lt;/g;
    $arg =~ s/>/&gt;/g;
    $arg =~ s/"/&quot;/g;
    return $arg;
}

正規表現の長さの割には、速度差は大して無いようです。

$ perl a.pl
Benchmark: timing 2000000 iterations of test0, test1...
     test0: 14 wallclock secs (13.32 usr +  0.00 sys = 13.32 CPU) @ 150150.15/s
(n=2000000)
     test1: 13 wallclock secs (13.29 usr +  0.00 sys = 13.29 CPU) @ 150489.09/s
(n=2000000)
$ perl a.pl
Benchmark: timing 2000000 iterations of test0, test1...
     test0: 13 wallclock secs (13.33 usr +  0.00 sys = 13.33 CPU) @ 150037.51/s
(n=2000000)
     test1: 13 wallclock secs (13.27 usr +  0.00 sys = 13.27 CPU) @ 150715.90/s
(n=2000000)

変更履歴