在python中读取一个unicode文件,它以与python源相同的方式声明其编码

时间:2011-05-21 00:51:58

标签: python unicode

我希望编写一个python程序来读取包含unicode文本的文件。这些文件通常用UTF-8编码,但可能不是;如果不是,则将在文件的开头显式声明备用编码。更确切地说,它将使用与Python本身使用的完全相同的规则来声明,以允许Python源代码具有显式声明的编码(如在PEP 0263中,有关更多详细信息,请参阅https://www.python.org/dev/peps/pep-0263/)。为了清楚起见,正在处理的文件实际上并不是python源,但它们使用相同的规则声明了它们的编码(当不是UTF-8时)。

如果在打开文件之前知道文件的编码,Python提供了一种非常简单的方法来自动解码文件:codecs.open命令;例如,有人可能会这样做:

import codecs
f = codecs.open('unicode.rst', encoding='utf-8')
for line in f:
    print repr(line)

我们在循环中得到的每个line都是一个unicode字符串。是否有一个Python库做类似的事情,但根据上面的规则选择编码(我认为这是Python 3.0的规则)? (例如,Python是否公开了'用于自我声明编码的'读取文件'来读取语言的来源?)如果没有,那么实现所需效果的最简单方法是什么?

一种想法是使用通常的open打开文件,读取前两行,将它们解释为UTF-8,使用PEP中的regexp查找编码声明,如果找到一个开头使用声明的编码解码所有后续行。为了确保这一点,我们需要知道,对于Python在Python源代码中允许的所有编码,通常的Python readline会正确地将文件拆分成行 - 也就是说,我们需要知道所有这些Python允许在Python源代码中编码,字节字符串'\ n'总是真正意味着换行符,而不是编码另一个字符的多字节序列的一部分。 (事实上​​我也需要担心'\ r \ n'。)有人知道这是否属实?文档不是很具体。

另一个想法是查看Python源代码。有谁知道在源代码中源代码编码处理的位置在哪里?

5 个答案:

答案 0 :(得分:7)

您应该能够在Python中使用自己的解码器。如果您只支持8位编码,这些编码是ASCII的超集,则下面的代码应该按原样运行。

如果您需要支持2字节encodings like UTF-16,则需要将模式扩充为匹配\x00c\x00o..或相反,具体取决于the byte order mark。 首先,生成一些宣传其编码的测试文件:

import codecs, sys
for encoding in ('utf-8', 'cp1252'):
    out = codecs.open('%s.txt' % encoding, 'w', encoding)
    out.write('# coding = %s\n' % encoding)
    out.write(u'\u201chello se\u00f1nor\u201d')
    out.close()

然后写解码器:

import codecs, re

def open_detect(path):
    fin = open(path, 'rb')
    prefix = fin.read(80)
    encs = re.findall('#\s*coding\s*=\s*([\w\d\-]+)\s+', prefix)
    encoding = encs[0] if encs else 'utf-8'
    fin.seek(0)
    return codecs.EncodedFile(fin, 'utf-8', encoding)

for path in ('utf-8.txt','cp1252.txt'):
    fin = open_detect(path)
    print repr(fin.readlines())

输出:

['# coding = utf-8\n', '\xe2\x80\x9chello se\xc3\xb1nor\xe2\x80\x9d']
['# coding = cp1252\n', '\xe2\x80\x9chello se\xc3\xb1nor\xe2\x80\x9d']

答案 1 :(得分:3)

我检查了tokenizer.c的来源(感谢@Ninefingers在另一个答案中建议这个并给出源浏览器的链接)。似乎Python使用的确切算法(等效于)以下内容。在不同的地方,我会将算法描述为逐字节读取---显然,人们希望在实践中做一些缓冲,但这种方式更容易描述。文件的初始部分按如下方式处理:

  1. 打开文件后,尝试识别文件开头的UTF-8 BOM。如果你看到它,吃它并记下你看到它的事实。 识别UTF-16字节顺序标记。
  2. 从文件中读取“一行”文字。 'a line'定义如下:你继续读取字节,直到你看到其中一个字符串'\ n','\ r'或'\ r \ n'(尝试匹配尽可能长的字符串---这个意味着如果你看到'\ r'你必须推测性地阅读下一个字符,如果它不是'\ n',则把它放回去)。与通常的Python实践一样,终结符包含在行中。
  3. 使用UTF-8编解码器解码此字符串。除非您看过UTF-8 BOM,否则如果您看到任何非ASCII字符(即127以上的任何字符),则会生成错误消息。 (Python 3.0当然不会在这里生成错误。)将此解码后的行传递给用户进行处理。
  4. 尝试使用PEP 0263中的正则表达式将此行解释为包含编码声明的注释。如果您找到编码声明,请跳至下面的说明“我找到了编码声明”。
  5. 好的,所以你没找到编码声明。使用与上面步骤2中相同的规则从输入中读取另一行。
  6. 使用与步骤3相同的规则对其进行解码,然后将其传递给用户进行处理。
  7. 再次尝试将此行作为编码声明注释插入,如第4步所示。如果找到,请跳至下面的说明“我找到了编码声明”。
  8. 行。我们现在检查了前两行。根据PEP 0263,如果有一个编码声明,它将在前两行,所以我们现在知道我们不会看到一个。我们现在使用与读取前两行相同的读取指令读取文件的其余部分:我们使用步骤2中的规则读取行,使用步骤3中的规则进行解码(如果我们看到非错误则会出错)除非我们看到BOM,否则为ASCII字节。
  9. 现在有关'我发现编码声明'时该做什么的规则:

    1. 如果我们之前看过UTF-8 BOM,请检查编码声明是否以某种形式显示为“utf-8”。否则会抛出错误。 (''utf-8'以某种形式'表示在转换为小写并将下划线转换为连字符后,是文字字符串'utf-8'或以'utf-8-'开头的内容。)
    2. 使用与Python codecs模块中给定编码关联的解码器读取文件的其余部分。特别是,将文件中其余字节划分为行是新编码的工作。
    3. 最后一个皱纹:通用换行类型的东西。这里的规则如下。如果编码是某种形式的“utf-8”或某种形式的“latin-1”之外的任何内容,则根本不要使用通用换行符;只是准确地传出来自codecs模块中解码器的行。另一方面,如果编码是某种形式的'utf-8'或某种形式的'latin-1',则将'\ r'或'\ r \ n'结尾的行转换为以'\ n'结尾的行。 (''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' {1}},'latin-1''iso-latin-1',或以'iso-8859-1''latin-1-''iso-latin-1-'之一开头的任何字符串。
    4. 对于我正在做的事情,忠实于Python的行为非常重要。我的计划是在Python中推出上面算法的实现,并使用它。感谢所有回答的人!

答案 2 :(得分:2)

来自PEP (0268)

  

Python的tokenizer / compiler组合将会   需要更新才能按如下方式工作:

     
      
  1. 阅读文件

  2.   假设每个文件编码固定

  3. 将其解码为Unicode   

  4. 将其转换为UTF-8字节字符串

  5.   
  6. 将UTF-8内容标记为

  7.   
  8. 编译它,从给定的Unicode数据创建Unicode对象         并从Unicode文字数据创建字符串对象         首先将UTF-8数据重新编码为8位字符串数据         使用给定的文件编码

  9.   

的确,如果您在Python源代码中检查Parser/tokenizer.c,您将找到函数get_coding_speccheck_coding_spec,它们负责在{{3}中正在检查的行上查找此信息}。

看起来这个功能不会作为python API暴露给你(至少这些特定功能不是Py前缀 - 所以你的选择是第三方库和/或重新将这些功能作为扩展。我个人不知道任何第三方库 - 我在标准库中也看不到这个功能。

答案 3 :(得分:1)

从Python 3.4开始,有一个功能可以让你做你想要的事情 - importlib.util.decode_source

根据documentation

  

importlib.util.decode_source(source_bytes)
  解码代表源代码的给定字节,并将其作为带有通用换行符的字符串返回(根据importlib.abc.InspectLoader.get_source()的要求)。

Brett Cannon talks在他的演讲中谈到了这个功能从源代码:CPython的编译器如何工作

答案 4 :(得分:1)

标准库中支持这一点,即使在Python 2中也是如此。以下是您可以使用的代码:

try:

    # Python 3
    from tokenize import open as open_with_encoding_check

except ImportError:

    # Python 2
    from lib2to3.pgen2.tokenize import detect_encoding
    import io


    def open_with_encoding_check(filename):
        """Open a file in read only mode using the encoding detected by
        detect_encoding().
        """
        fp = io.open(filename, 'rb')
        try:
            encoding, lines = detect_encoding(fp.readline)
            fp.seek(0)
            text = io.TextIOWrapper(fp, encoding, line_buffering=True)
            text.mode = 'r'
            return text
        except:
            fp.close()
            raise

然后我个人需要解析并编译这个源代码。在Python 2中,编译包含编码声明的unicode文本是一个错误,因此必须首先将包含声明的行留空(不删除,因为这会更改行号)。所以我也做了这个功能:

def read_source_file(filename):
    from lib2to3.pgen2.tokenize import cookie_re

    with open_with_encoding_check(filename) as f:
        return ''.join([
            '\n' if i < 2 and cookie_re.match(line)
            else line
            for i, line in enumerate(f)
        ])

我在我的包中使用这些,最新来源(如果我发现我需要更改它们)可以找到here,而测试是here