2

我有很多外部文件的 XML 文档和文件名,这些文件在导入过程中存在各种形式的文本损坏或 Mojibake 导致数据质量问题。我在 StackOverflow 上阅读了许多关于更正字符串的不同帖子,但它们未能真正概述如何以系统的方式清理文本,而 pythondecode似乎encode没有帮助。如何使用 Python 2.7 恢复包含 Latin-1 (ISO-8859-1) 范围内的字符但通常具有混合编码的 XML 文件和文件名?

4

1 回答 1

7

你必须做出假设

如果您无法对您将遇到的字母类型做出假设,您可能会遇到麻烦。所以在我们的文档中我们可以合理地假设挪威字母表是很好的A-Å。没有神奇的工具可以自动更正您遇到的每个文档。

因此,在此域中,我们知道文件可能包含åUTF-8 2 字节表示0xc3 0xa5UnicodeLatin-1Windows-1252将其表示为0xe5. 一般来说,这个角色查找非常好,如果你发现自己正在研究一个角色,它可能会成为一个很好的书签。

例子

  • 挪威人å
  • 损坏的版本Ã¥

您可以在这个方便的调试图表中找到一长串此类问题。

基本 Python 编码、解码

如果您确切知道出了什么问题,这是将字符串恢复原状的最简单方法。

our_broken_string = 'Ã¥'
broken_unicode = our_broken_string.decode('UTF-8')
print broken_unicode # u'\xc3\xa5' yikes -> two different unicode characters
down_converted_string = broken_unicode.encode('LATIN-1')
print down_converted_string # '\xc3\xa5' those are the right bytes
correct_unicode = down_converted_string.decode('UTF-8')
print correct_unicode # u'\xe5' correct unicode value

文件

在处理文档时,可以做出一些相对较好的假设。单词、空格和线条。即使文档是 XML,您仍然可以将其视为单词,而不必太担心标签,或者如果单词真的是单词,您只需要可以找到的最小单位。我们还可以假设,如果文件有文本编码问题,它也可能有行尾问题,这取决于有多少不同的操作系统损坏了该文件。我会打破行尾,rstrip并使用 print 将数组重新组合到StringIO文件句柄。

在保留空格时,可能很想通过漂亮打印函数运行 XML 文档,但您不应该这样做,我们只想更正小文本单元的编码而不更改任何其他内容。一个好的起点是查看您是否可以逐行、逐字地阅读文档,而不是在任意字节块中,并忽略您正在处理 XML 的事实。

在这里,我利用这样一个事实,即如果文本超出 UTF-8 的范围,您将获得 UnicodeDecodeErrors,然后尝试 LATIN-1。这在本文档中有效。

import unicodedata

encoding_priority = ['UTF-8', 'LATIN-1']
def clean_chunk(file_chunk):
    error_count = 0
    corrected_count = 0
    new_chunk = ''
    encoding = ''
    for encoding in encoding_priority:
        try:
            new_chunk = file_chunk.decode(encoding, errors='strict')
            corrected_count += 1
            break
        except UnicodeDecodeError, error:
            print('Input encoding %s failed -> %s' % (encoding, error))
            error_count += 1
    if encoding != '' and error_count > 0 and corrected_count > 0:
        print('Decoded. %s(%s) from hex(%s)' % (encoding, new_chunk, file_chunk.encode('HEX')))

    normalized = unicodedata.normalize('NFKC', new_chunk)

    return normalized, error_count, corrected_count


def clean_document(document):
    cleaned_text = StringIO()
    error_count = 0
    corrected_count = 0

    for line in document:
        normalized_words = []
        words = line.rstrip().split(' ')
        for word in words:
            normalized_word, error_count, corrected_count = clean_chunk(word)
            error_count += error_count
            corrected_count += corrected_count
            normalized_words.append(normalized_word)
        normalized_line = ' '.join(normalized_words)
        encoded_line = normalized_line.encode(output_encoding)
        print(encoded_line, file=cleaned_text)

    cleaned_document = cleaned_text.getvalue()
    cleaned_text.close()

    return cleaned_document, error_count, corrected_count

用于处理 Mojibake 的 FTFY

如果您的问题是真正的Mojibake,例如可能是错误的文件名。您可以使用FTFY尝试启发式地纠正您的问题。同样,我会采用逐字逐句的方法来获得最佳结果。

import os
import sys
import ftfy
import unicodedata


if __name__ == '__main__':
    path = sys.argv[1]
    file_system_encoding = sys.getfilesystemencoding()
    unicode_path = path.decode(file_system_encoding)

    for root, dirs, files in os.walk(unicode_path):
        for f in files:
            comparable_original_filename = unicodedata.normalize('NFC', f)
            comparable_new_filename = ftfy.fix_text(f, normalization='NFC')

            if comparable_original_filename != comparable_new_filename:
                original_path = os.path.join(root, f)
                new_path = os.path.join(root, comparable_new_filename)
                print "Renaming:" + original_path + " to:" + new_path
                os.rename(original_path, new_path)

å这通过目录更正了被破坏的更丑陋的错误A\xcc\x83\xc2\xa5。这是什么?大写字母A+ COMBINING LETTER TILDE0xcc 0x83 是Ã使用unicode equivalence表示的几种方法之一。这对 FTFY 来说确实是一项工作,因为它实际上会执行启发式算法并解决这类问题。

用于比较和文件系统的 Unicode 规范化

另一种方法是使用 unicode 的规范化来获取正确的字节。

import unicodedata

a_combining_tilde = 'A\xcc\x83'
# Assume: Expecting UTF-8 
unicode_version = a_combining_tilde.decode('UTF-8') # u'A\u0303' and this cannot be converted to LATIN-1 and get Ã
normalized = unicodedata.normalize('NFC', unicode_version) # u'\c3'
broken_but_better = normalized.encode('UTF-8') # '\xc3\x83` correct UTF-8 bytes for Ã.

因此,总而言之,如果您将其视为 UTF-8 编码字符串A\xcc\x83\xc2\xa5,对其进行规范化,然后向下转换为 LATIN-1 字符串,然后再返回 UTF-8,您将得到正确的 unicode。

您需要注意操作系统如何编码文件名。您可以通过以下方式检索该信息:

file_system_encoding = sys.getfilesystemencoding()

所以让我们说file_system_encodingUTF-8,很棒吧?然后你比较两个看似相同的 unicode 字符串,它们并不相等!FTFY,默认归一化为NFC,HFS 归一化为旧版本的NFD. 因此,仅仅知道编码相同是不够的,您必须以相同的方式进行归一化才能使比较有效。

  • Windows NTFS 存储没有规范化的 unicode。
  • Linux 存储没有规范化的 unicode。
  • Mac HFS 使用专有的 HFD 规范化存储 UTF-8。

Node.js 有一个很好的指南来处理不同的文件系统。综上所述,规范化比较,不要随意重新规范化文件名。

最后的笔记

谎言、该死的谎言和 XML 声明

在 XML 文档中,您会得到类似这样的东西,它应该通知 XML 解析器有关文本编码的信息。

<?xml version="1.0" encoding="ISO-8859-1"?>

如果您看到这一点,则应将其视为谎言,直到被证明是真实的。在将此文档交给 XML 解析器之前,您需要验证和处理编码问题,并且您需要更正声明。

谎言、该死的谎言和 BOM 标记

字节顺序标记听起来是个好主意,但就像它们的 XML 声明表亲一样,它们完全不可靠地指示文件编码情况。在 UTF-8 中,不推荐使用 BOM,并且对于字节顺序没有意义。它们唯一的价值是表明某些东西是用 UTF-8 编码的。但是,考虑到文本编码的问题,默认值是并且应该是期望 UTF-8。

于 2016-05-13T10:19:54.823 回答