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

Read a unicode file in python which declares its encoding in the same way as python source

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

如果在打开文件之前就知道它的编码,那么python提供了一种非常简单的方法,可以通过自动解码来读取文件:codecs.open命令;例如,可以这样做:

1
2
3
4
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源代码中允许的所有编码,字节字符串''总是意味着换行,并且不是某些多字节的一部分编码另一个字符的序列。(事实上,我也需要担心'
')有人知道这是不是真的吗?医生不是很具体。

另一种想法是查看Python源代码。有人知道在python源代码中源代码编码处理在哪里完成吗?


您应该能够在Python中滚动自己的解码器。如果您只支持8位编码(这是ASCII的超集),那么下面的代码应该可以正常工作。

如果您需要支持2字节编码(如utf-16),则需要根据字节顺序标记增加模式以匹配\x00c\x00o..或相反。首先,生成一些宣传其编码的测试文件:

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

然后编写解码器:

1
2
3
4
5
6
7
8
9
10
11
12
13
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())

输出:

1
2
3
4
['# coding = utf-8
'
, '\xe2\x80\x9chello se\xc3\xb1nor\xe2\x80\x9d']
['# coding = cp1252
'
, '\xe2\x80\x9chello se\xc3\xb1nor\xe2\x80\x9d']


我检查了tokenizer.c的来源(感谢@ninefingers在另一个答案中提出这一点,并提供了一个到源浏览器的链接)。python使用的精确算法(相当于)如下。在不同的地方,我将把算法描述为逐字节读取——显然,人们希望在实践中做一些缓冲的事情,但这样更容易描述。文件的初始部分处理如下:

  • 打开文件后,尝试在文件开头识别UTF-8 BOM。如果你看到它,吃了它,记下你看到的事实。不识别UTF-16字节顺序标记。
  • 从文件中读取"一行"文本。"一行"的定义如下:您一直在读取字节,直到看到字符串"、"
    "或"r"(试图尽可能长地匹配字符串,这意味着如果看到"r",则必须推测地读取下一个字符,如果它不是"n",则将其放回原处)。和通常的Python实践一样,终结者包含在行中。
  • 使用UTF-8编解码器解码此字符串。除非您已经看到了UTF-8BOM,否则如果您看到任何非ASCII字符(即127以上的任何字符),就会生成一条错误消息。(当然,python 3.0不会在这里生成错误。)将这条解码后的行传递给用户进行处理。
  • 尝试使用PEP 0263中的regexp将此行解释为包含编码声明的注释。如果您找到一个编码声明,请跳到下面的"我找到了一个编码声明"说明。
  • 好吧,所以你没有找到编码声明。使用与上面步骤2中相同的规则,从输入中读取另一行。
  • 使用与步骤3相同的规则对其进行解码,并将其传递给用户进行处理。
  • 再次尝试将此行作为编码声明注释插入,如步骤4所示。如果您找到了一个,请跳到下面的"我找到了一个编码声明"说明。
  • 好啊。我们现在检查了前两行。根据PEP 0263,如果要有一个编码声明,它将在前两行,所以我们现在知道我们不会看到一个。现在,我们使用与读取前两行相同的读取指令读取文件的其余部分:使用步骤2中的规则读取行,使用步骤3中的规则解码(如果我们看到非ASCII字节,除非我们看到一个BOM,否则会出错)。
  • 现在,当"我找到一个编码声明"时要做什么的规则是:

  • 如果我们以前看到过一个utf-8bom,请检查编码声明是否以某种形式显示"utf-8"。否则抛出一个错误。(某些形式的"utf-8"是指在转换为小写并将下划线转换为连字符后,文本字符串'utf-8'或以'utf-8-'开头的任何内容。)
  • 使用与python codecs模块中给定编码相关联的解码器读取文件的其余部分。特别是,将文件中的其余字节划分为行是新编码的工作。
  • 最后一个问题是:通用换行类型的东西。这里的规则如下。如果编码不是某种形式的"utf-8"或某种形式的"拉丁-1",那么根本就不要使用通用换行符;只需按照来自codecs模块中解码器的方式传递行。另一方面,如果编码是某种形式的"utf-8"或某种形式的"拉丁-1",则将以"r"或"r"结尾的行转换为以"n"结尾的行。(某些形式的"utf-8"与之前的含义相同。拉丁语-1"某种形式"是指在转换为小写并将下划线转换为连字符后,是文字字符串'latin-1''iso-latin-1''iso-8859-1'之一的任何字符串,或以'latin-1-''iso-latin-1-''iso-8859-1-'之一开头的任何字符串。
  • 对于我所做的,忠实于Python的行为是很重要的。我的计划是在python中滚动上述算法的实现,并使用它。感谢所有回答的人!


    来自所述PEP(0268):

    Python's tokenizer/compiler combo will
    need to be updated to work as follows:

  • read the file

  • decode it into Unicode assuming a fixed per-file encoding

  • convert it into a UTF-8 byte string

  • tokenize the UTF-8 content

  • compile it, creating Unicode objects from the given Unicode data
    and creating string objects from the Unicode literal data
    by first reencoding the UTF-8 data into 8-bit string data
    using the given file encoding

  • 实际上,如果您在python源代码中检查Parser/tokenizer.c,您会发现函数get_coding_speccheck_coding_spec,它们负责在decoding_fgets中正在检查的行上查找此信息。

    它看起来不像是以python API(至少这些特定的函数不是以Py为前缀的)的形式在任何地方向您公开的,所以您的选项是第三方库和/或将这些函数重新用作扩展。我个人不知道任何第三方库-在标准库中我也看不到这个功能。


    在标准库中,甚至在Python2中,都支持这种方法。以下是您可以使用的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def read_source_file(filename):
        from lib2to3.pgen2.tokenize import cookie_re

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

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


    从python 3.4开始,有一个函数允许您执行您所要求的操作–importlib.util.decode_source

    根据文件:

    importlib.util.decode_source(source_bytes)
    Decode the given bytes representing source code and return it as a string with universal newlines (as required by importlib.abc.InspectLoader.get_source()).

    BrettCannon在他从源代码到代码的演讲中谈到了这个函数:cpython的编译器是如何工作的。