web-dev-qa-db-ja.com

UTF-8で3バイト以上かかるユニコード文字をフィルタリング(または置換)する方法は?

PythonとDjangoを使用していますが、MySQLの制限が原因で問題が発生しています。 MySQL 5.1のドキュメント によると、それらの_utf8_実装は4バイト文字をサポートしていません MySQL 5.5 は_utf8mb4_を使用して4バイト文字をサポートし、将来的には_utf8_もサポートする可能性があります。

しかし、私のサーバーはMySQL 5.5にアップグレードする準備ができていないため、3バイト以下のUTF-8文字に制限されています。

私の質問は:3バイト以上かかるユニコード文字をフィルタリング(または置換)する方法

すべての4バイト文字を公式の_\ufffd_(U + FFFD REPLACEMENT CHARACTER)または_?_に置き換えたい。

つまり、Python独自の str.encode() メソッド(_'replace'_パラメータを渡す場合)と非常によく似た動作が必要です。 編集:encode()と同様の動作が必要ですが、実際には文字列をエンコードしたくありません。フィルタリング後もUnicode文字列を保持したい

MySQLに格納する前に文字をエスケープしたくないので、データベースから取得するすべての文字列のエスケープを解除する必要があるため、非常に煩わしく、実行不可能です。

以下も参照してください。

[編集]提案されたソリューションに関するテストを追加しました

これまでのところ、良い答えを得ました。ありがとう、人々!次に、それらの1つを選択するために、簡単なテストを行って、最も単純で最速のものを見つけました。

_#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vi:ts=4 sw=4 et

import cProfile
import random
import re

# How many times to repeat each filtering
repeat_count = 256

# Percentage of "normal" chars, when compared to "large" unicode chars
normal_chars = 90

# Total number of characters in this string
string_size = 8 * 1024

# Generating a random testing string
test_string = u''.join(
        unichr(random.randrange(32,
            0x10ffff if random.randrange(100) > normal_chars else 0x0fff
        )) for i in xrange(string_size) )

# RegEx to find invalid characters
re_pattern = re.compile(u'[^\u0000-\uD7FF\uE000-\uFFFF]', re.UNICODE)

def filter_using_re(unicode_string):
    return re_pattern.sub(u'\uFFFD', unicode_string)

def filter_using_python(unicode_string):
    return u''.join(
        uc if uc < u'\ud800' or u'\ue000' <= uc <= u'\uffff' else u'\ufffd'
        for uc in unicode_string
    )

def repeat_test(func, unicode_string):
    for i in xrange(repeat_count):
        tmp = func(unicode_string)

print '='*10 + ' filter_using_re() ' + '='*10
cProfile.run('repeat_test(filter_using_re, test_string)')
print '='*10 + ' filter_using_python() ' + '='*10
cProfile.run('repeat_test(filter_using_python, test_string)')

#print test_string.encode('utf8')
#print filter_using_re(test_string).encode('utf8')
#print filter_using_python(test_string).encode('utf8')
_

結果:

  • filter_using_re()は515個の関数呼び出しを実行しました0.139 CPU秒sub()組み込みで0.138 CPU秒)
  • filter_using_python()は2097923関数呼び出しを3.413 CPU秒で実行しました(join()呼び出しで1.511 CPU秒と1.900 CPUジェネレータ式を評価する秒数)
  • 私はitertoolsを使用してテストしませんでした...ええと...その解決策は興味深いものの、非常に大きく複雑でした。

結論

RegExソリューションは、断然、最速のソリューションでした。

38

\ u0000-\uD7FFおよび\ uE000-\uFFFFの範囲のUnicode文字は、UTF8で3バイト(またはそれ以下)のエンコーディングになります。\uD800-\uDFFFの範囲は、マルチバイトUTF16用です。私はpythonを知りませんが、これらの範囲外で一致するように正規表現を設定できるはずです。

pattern = re.compile("[\uD800-\uDFFF].", re.UNICODE)
pattern = re.compile("[^\u0000-\uFFFF]", re.UNICODE)

質問本文にPython from DenilsonSáのスクリプトを追加して編集します。

re_pattern = re.compile(u'[^\u0000-\uD7FF\uE000-\uFFFF]', re.UNICODE)
filtered_string = re_pattern.sub(u'\uFFFD', unicode_string)    
34
drawnonward

デコードとエンコードのステップをスキップして、各文字の最初のバイト(8ビット文字列)の値を直接検出できます。 UTF-8によると:

#1-byte characters have the following format: 0xxxxxxx
#2-byte characters have the following format: 110xxxxx 10xxxxxx
#3-byte characters have the following format: 1110xxxx 10xxxxxx 10xxxxxx
#4-byte characters have the following format: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

それによると、4バイト文字をフィルターで除外するには、各文字の最初のバイトの値のみを確認する必要があります。

def filter_4byte_chars(s):
    i = 0
    j = len(s)
    # you need to convert
    # the immutable string
    # to a mutable list first
    s = list(s)
    while i < j:
        # get the value of this byte
        k = ord(s[i])
        # this is a 1-byte character, skip to the next byte
        if k <= 127:
            i += 1
        # this is a 2-byte character, skip ahead by 2 bytes
        Elif k < 224:
            i += 2
        # this is a 3-byte character, skip ahead by 3 bytes
        Elif k < 240:
            i += 3
        # this is a 4-byte character, remove it and update
        # the length of the string we need to check
        else:
            s[i:i+4] = []
            j -= 4
    return ''.join(s)

デコード部分とエンコード部分をスキップすると、時間を節約できます。また、ほとんどの場合1バイト文字を含む小さな文字列の場合、これは正規表現のフィルタリングよりも高速です。

6
kasioumis

MySQL 5.1ドキュメント によると:「ucs2およびutf8文字セットは、BMPの外にある補助文字をサポートしていません。」これは、サロゲートペアに問題がある可能性があることを示しています。

nicode標準5.2章 は、実際にはサロゲートペアを1つの4バイトUTF-8シーケンスではなく2つの3バイトUTF-8シーケンスとしてエンコードすることを禁止しています。 「サロゲートコードポイントはUnicodeスカラー値ではないため、コードポイントD800..DFFFにマッピングされるUTF-8バイトシーケンスは不正な形式です。

MySQLがサロゲートペアで何を行うかを確認することは良い考えかもしれません。それらが保持されない場合、このコードは単純な十分なチェックを提供します:

all(uc < u'\ud800' or u'\ue000' <= uc <= u'\uffff' for uc in unicode_string)

このコードは、「厄介」をu\ufffdに置き換えます。

u''.join(
    uc if uc < u'\ud800' or u'\ue000' <= uc <= u'\uffff' else u'\ufffd'
    for uc in unicode_string
    )
1
John Machin

UTF-16としてエンコードしてから、UTF-8として再エンコードします。

>>> t = u'????????????'
>>> e = t.encode('utf-16le')
>>> ''.join(unichr(x).encode('utf-8') for x in struct.unpack('<' + 'H' * (len(e) // 2), e))
'\xed\xa0\xb5\xed\xb0\x9f\xed\xa0\xb5\xed\xb0\xa8\xed\xa0\xb5\xed\xb0\xa8'

サロゲートペアは再エンコードする前にデコードされる可能性があるため、結合後はエンコードできないことに注意してください。

編集:

MySQL(少なくとも5.1.47)は、サロゲートペアの処理に問題がありません。

mysql> create table utf8test (t character(128)) collate utf8_general_ci;
Query OK, 0 rows affected (0.12 sec)

  ...

>>> cxn = MySQLdb.connect(..., charset='utf8')
>>> csr = cxn.cursor()
>>> t = u'????????????'
>>> e = t.encode('utf-16le')
>>> v = ''.join(unichr(x).encode('utf-8') for x in struct.unpack('<' + 'H' * (len(e) // 2), e))
>>> v
'\xed\xa0\xb5\xed\xb0\x9f\xed\xa0\xb5\xed\xb0\xa8\xed\xa0\xb5\xed\xb0\xa8'
>>> csr.execute('insert into utf8test (t) values (%s)', (v,))
1L
>>> csr.execute('select * from utf8test')
1L
>>> r = csr.fetchone()
>>> r
(u'\ud835\udc1f\ud835\udc28\ud835\udc28',)
>>> print r[0]
????????????

そして、それを楽しむために、itertools怪物:)

import itertools as it, operator as op

def max3bytes(unicode_string):

    # sequence of pairs of (char_in_string, u'\N{REPLACEMENT CHARACTER}')
    pairs= it.izip(unicode_string, it.repeat(u'\ufffd'))

    # is the argument less than or equal to 65535?
    selector= ft.partial(op.le, 65535)

    # using the character ordinals, return 0 or 1 based on `selector`
    indexer= it.imap(selector, it.imap(ord, unicode_string))

    # now pick the correct item for all pairs
    return u''.join(it.imap(Tuple.__getitem__, pairs, indexer))
1
tzot

私はそれが最速ではないと思いますが、非常に簡単です(「Pythonic」:):

def max3bytes(unicode_string):
    return u''.join(uc if uc <= u'\uffff' else u'\ufffd' for uc in unicode_string)

注意:このコードはnotがUnicodeがU + D800-U + DFFFの範囲の代理文字を持っているという事実を考慮に入れます。

0
tzot