home changes contents help options

065:文字列を最大nバイトに切り詰める

最初に思いつく方法は、文字列の先頭から n バイト切り出す方法です。しかし、日本語が対象となっている時は、切れ目が文字の先頭とは限らないので問題が起こります。

うまくいかない例

>>> li='あいうえお'
>>> li[0]
'\x82'
>>> li[:1]
'\x82'
>>> li[:2]
'あ'
>>> li[:3]
'あ\x82'
>>> li[:4]
'あい'

2.5 以降ならば codecs モジュールの iterencode, getincrementaldecoder らを使用して、この問題を簡単に解決できます。次の関数 take_nbytes_str, take_nbytes_unicode を使用してください。

# coding: utf-8

from codecs import getincrementaldecoder, iterencode

def _iter_mbchar(iterator, encoding, errors='strict', **kwargs):
    decoder = getincrementaldecoder(encoding)(errors, **kwargs)
    mbchar = []
    for input in iterator:
        mbchar.append(input)
        output = decoder.decode(input)
        if output:
            yield ''.join(mbchar)
            del mbchar[:]
    output = decoder.decode("", True)
    if output:
        yield ''.join(mbchar)

def take_nbytes_str(str_, n, encoding, errors='strict', **kwargs):
    u"""
    文字列 str_ を最大 n バイトに切り詰める

    str_:     8 ビット文字列
    n:        最大のバイト数。これより長い場合、切り詰める
    encoding: str_ の文字コード
    errors:   コーデックが用いるエラー処理方法
    **kwargs: コーデックに渡すオプション
    """
    length = 0
    temp = []
    for mbchar in _iter_mbchar(str_, encoding, errors, **kwargs):
        length += len(mbchar)
        if length > n:
            break
        temp.append(mbchar)
    return ''.join(temp)

def take_nbytes_unicode(unicode_, n, encoding, errors='strict', **kwargs):
    u"""
    ユニコード文字列 unicode_ をエンコードする、ただし最大 n バイトまで

    unicode_: ユニコード文字列
    n:        最大のバイト数。出力結果がこれより長くなることはない
    encoding: 文字コード
    errors:   コーデックが用いるエラー処理方法
    **kwargs: コーデックに渡すオプション
    """
    length = 0
    temp = []
    for mbchar in iterencode(unicode_, encoding, errors, **kwargs):
        length += len(mbchar)
        if length > n:
            break
        temp.append(mbchar)
    return ''.join(temp)

def _test():
    def _write(f, code):
        s = u'あいうえおかきくけこさしすせそたちつてと'.encode(code)
        for i in range(1, 40):
            f.write(''.join(take_nbytes_str(s, i, code)))
            f.write('\n')
        u = u'あいうえおかきくけこさしすせそたちつてと'
        for i in range(1, 40):
            f.write(''.join(take_nbytes_unicode(u, i, code)))
            f.write('\n')
    f_sjis = open('_sjis.txt', 'w')
    try:
        _write(f_sjis, 'shift_jis')
    finally:
        f_sjis.close()
    f_euc = open('_euc.txt', 'w')
    try:
        _write(f_euc, 'euc_jp')
    finally:
        f_euc.close()
    f_utf8 = open('_utf8.txt', 'w')
    try:
        _write(f_utf8, 'utf_8')
    finally:
        f_utf8.close()

if __name__ == '__main__':
    _test()

以下は codecs の新機能の存在を知らずに作成してしまった関数 take_nbytes1 です。恥ずかしながら残しておきます。 Python 2.4 以下では役に立つかもしれません。

# coding: utf-8

def _shift_jis(c):
    if c <= '\x7f' or '\xa1' <= c <= '\xdf':
        return 1
    elif '\x81' <= c <= '\x9f' or '\xe0' <= c <= '\xfc':
        return 2
    raise ValueError(repr(c))

def _euc_jp(c):
    if c <= '\x7f':
        return 1
    elif c == '\x8e' or '\xa1' <= c <= '\xfe':
        return 2
    elif c == '\x8f':
        return 3
    raise ValueError(repr(c))

def _utf_8(c):
    if c <= '\x7f':
        return 1
    elif '\xc0' <= c <= '\xdf':
        return 2
    elif '\xe0' <= c <= '\xef':
        return 3
    elif '\xf0' <= c <= '\xf7':
        return 4
    elif '\xf8' <= c <= '\xfb':
        return 5
    elif '\xfc' <= c <= '\xfd':
        return 6
    raise ValueError(repr(c))

_code = {
        'shift_jis': _shift_jis,
        'euc_jp': _euc_jp,
        'utf_8': _utf_8,
        }

def take_nbytes1(str_, n, code):
    u"""文字列 str_ を最大 n バイトに切り詰める

    str_: 8 ビット文字列
    n:    最大のバイト数。これより長い場合、切り詰める
    code: code: str_ の文字コード ['shift_jis', 'euc_jp', 'utf_8']
    """
    length = 0
    buf = []
    f_code = _code[code]
    i = iter(str_)
    try:
        while True:
            c = i.next()
            width = f_code(c)
            length += width
            if length > n:
                break
            buf.append(c)
            for _ in range(width - 1):
                buf.append(i.next())
    except StopIteration:
        pass
    return ''.join(buf)

def _test():
    def _write(f, code):
        s = u'あいうえおかきくけこさしすせそたちつてと'.encode(code)
        for i in range(1, 40):
            f.write(take_nbytes1(s, i, code))
            f.write('\n')
    f_sjis = open('_sjis.txt', 'w')
    try:
        _write(f_sjis, 'shift_jis')
    finally:
        f_sjis.close()
    f_euc = open('_euc.txt', 'w')
    try:
        _write(f_euc, 'euc_jp')
    finally:
        f_euc.close()
    f_utf8 = open('_utf8.txt', 'w')
    try:
        _write(f_utf8, 'utf_8')
    finally:
        f_utf8.close()

if __name__ == '__main__':
    _test()