Python2系で文字参照のエスケープとか

車輪の再開発

ほぼ同様の関数はHTMLParser.HTMLParser.unescapelxml.html.fromstringあたりにもあるけれど、 前者は

  • U+10000-U+10FFFFが無理
  • HTMLParser.HTMLParser()を一度生成しないと使えない←結構気に食わない

だし、後者はunescapeのためにわざわざlxmlimportするのも気が引けるので。

2系に限っているのは3系には既にあるため。

基本戦略はHTMLParser.unescapeと同じく(←書いてから知った)re.subを使う方法で。

re.subは第3引数(置換後文字列)に関数オブジェクトを置けるというのがポイントで、そこに文字参照→対応するユニコード文字(列)なる関数を置いてやればよい。

def unescape_charref(escaped):
    return re.sub('CHARREF REGEXP',lambda s:REPLACE(s),escaped)
名前または数字の解決

正規表現で簡単に書ける。

r'&((?P<char>[a-z]+)|#(?P<decimal>\d+)|#x(?P<hex>[\da-f]+));'

マッチした場合入れ子になった3グループのうち必ず2グループは空文字列となり、1グループは空文字列にならない。 よって、この正規表現にマッチしたre.MatchObjectmatchobjとして、

from htmlentitydefs import name2codepoint

def getunichr(matchobj):
    if matchobj.group('char'):
        # Character entity references
        num=name2codepoint.get(matchobj.group('char'),0)
    elif matchobj.group('decimal'):
        # Numeric character reference (decimal)
        num=int(matchobj.group('decimal'))
    elif matchobj.group('hex'):
        # Numeric character reference (hexadecimal)
        num=int(matchobj.group('hex'),16)
    return num

という感じにすればよい。これで名前→数値はOK。

UCS-2とUCS-4

Python処理系はUCS-2あるいはUCS-4がビルド時にオプションとして指定されていて、その違いによりunichr等の挙動が異なる。

qiita.com

ちなみにPython.orgのWin-x64のバイナリは(俺のがそうなので多分)UCS-2が指定されている。この場合unichrは0<num<0x10000つまりU+0000-U+FFFFの部分しか対応していない。

UCS-2とUCS-4の区別はsys.maxunicodeでできる。

リンク先のコピペに近いけど、

from sys import maxunicode

def uchr(c):
    if 0<c<=maxunicode:
        # BMP(UCS-2) / whole(UCS-4)
        return unichr(c)
    elif maxunicode<c<=0x10ffff:
        # SMP(UCS-2) / None(UCS-4)
        c-=0x10000
        return unichr(c>>10|0xD800)+unichr(c&0x3FF|0xDC00)
    else:
        # c==0 or c is out of Unicode
        return ''

という感じ。Unicodeから外れてたらとりあえず空文字列を返す。

SMPに対応する意味あんの?とか実は書きながら思ってたんだけど、最近で言うと絵文字とかもここに属するらしいので、案外重要なのかもしれない。

まとめて

こういう感じになりました。

gist.github.com

本当に3系と同等にするのならばhtmlentitydefs.name2codepointでなく3系のhtml.entities.html5あたりも対応すべきかもしれないけど、面倒なので省略。