在Python3中将Unicode序列转换为字符串,但允许使用字符串

时间:2018-09-20 12:35:45

标签: python python-3.x unicode

至少有one related question on SO被证明在尝试解码unicode序列时很有用。

我正在预处理许多具有很多不同体裁的文本。有些是经济的,有些是技术的,依此类推。注意事项之一是转换unicode序列:

'Korado's output has gone up from 180,000 radiators per year to almost 1.7 million today,' says Vojt\u0115ch \u010camek.

这种字符串需要转换为实际字符:

'Korado's output has gone up from 180,000 radiators per year to almost 1.7 million today,' says Vojtĕch Čamek.

可以这样做:

s = "'Korado's output has gone up from 180,000 radiators per year to almost 1.7 million today,' says Vojt\u0115ch \u010camek."
s = s.encode('utf-8').decode('unicode-escape')

(至少当s是从utf-8编码的文本文件中提取的输入行时,此方法有效。我似乎无法使它在REPL.it之类的在线服务上正常工作,其中输出的编码/解码方式不同。)

在大多数情况下,这可以正常工作。但是,如果在输入字符串中看到目录结构路径(通常是我的数据集中的技术文档的情况),则会出现UnicodeDecodeError

给出以下数据unicode.txt

'Korado's output has gone up from 180,000 radiators per year to almost 1.7 million today,' says Vojt\u0115ch \u010camek, Financial Director and Director of Controlling.
Voor alle bestanden kan de naam met de volledige padnaam (bijvoorbeeld: /u/slick/udfs/math.a (op UNIX), d:\udfs\math.dll (op Windows)).

字节串表示为:

b"'Korado's output has gone up from 180,000 radiators per year to almost 1.7 million today,' says Vojt\\u0115ch \\u010camek, Financial Director and Director of Controlling.\r\nVoor alle bestanden kan de naam met de volledige padnaam (bijvoorbeeld: /u/slick/udfs/math.a (op UNIX), d:\\udfs\\math.dll (op Windows))."

解码输入文件中的第二行时,以下脚本将失败:

with open('unicode.txt', 'r', encoding='utf-8') as fin, open('unicode-out.txt', 'w', encoding='utf-8') as fout:
    lines = ''.join(fin.readlines())
    lines = lines.encode('utf-8').decode('unicode-escape')

    fout.write(lines)

有痕迹:

Traceback (most recent call last):
  File "C:/Python/files/fast_aligning/unicode-encoding.py", line 3, in <module>
    lines = lines.encode('utf-8').decode('unicode-escape')
UnicodeDecodeError: 'unicodeescape' codec can't decode bytes in position 275-278: truncated \uXXXX escape

Process finished with exit code 1

如何确保如前所示,第一个句子仍被正确地“翻译”了,而第二个句子仍保持不变?给定的两行的预期输出将如下所示,其中第一行已更改而第二行未更改。

'Korado's output has gone up from 180,000 radiators per year to almost 1.7 million today,' says Vojtĕch Čamek.
Voor alle bestanden kan de naam met de volledige padnaam (bijvoorbeeld: /u/slick/udfs/math.a (op UNIX), d:\udfs\math.dll (op Windows)).

3 个答案:

答案 0 :(得分:1)

@HostBinding('attr.id') host_id; @HostBinding('class') host_classes = 'Block'; 模式下的raw_unicode_escape编解码器似乎可以解决问题。我在此处将输入内联为原始字节长字符串,根据我的推理,这应该等效于从二进制文件读取它。

ignore

输出

  

'Korado的产量已从每年18万个散热器增加到如今的近170万个,”财务总监兼控制总监VojtĕchČamek说。
  敬请原谅。(bijvoorbeeld:/u/slick/udfs/math.a(op UNIX),d:s \ math.dll(op Windows))。

请注意,input = br""" 'Korado's output has gone up from 180,000 radiators per year to almost 1.7 million today,' says Vojt\u0115ch \u010camek, Financial Director and Director of Controlling. Voor alle bestanden kan de naam met de volledige padnaam (bijvoorbeeld: /u/slick/udfs/math.a (op UNIX), d:\udfs\math.dll (op Windows)). """ print(input.decode('raw_unicode_escape', 'ignore')) 中的\udf受到了破坏,因为编解码器试图开始读取d:\udfs序列,但放弃了\uXXXX

一种替代方法(可能较慢)是使用正则表达式在解码数据中查找有效的Unicode序列。不过,这假定s将完整的输入字符串设为UTF-8是可能的。 (.decode()舞蹈是必需的,因为字符串不能被编码,只能是字节。一个人也可以使用.encode().decode()。)

chr(int(m.group(0)[2:], 16))

输出

  

'Korado的产量已从每年18万个散热器增加到如今的近170万个,”财务总监兼控制总监VojtĕchČamek说。
  请您耐心等候(bijvoorbeeld:/u/slick/udfs/math.a(op UNIX),d:\ udfs \ math.dll(op Windows))。

由于escape_re = re.compile(r'\\u[0-9a-f]{4}') output = escape_re.sub(lambda m: m.group(0).encode().decode('unicode_escape'), input.decode())) 没有4个十六进制字符,因此这里保留了\udf路径。

答案 1 :(得分:1)

输入不明确。在一般情况下,不存在正确的答案。我们可以使用启发式方法在大多数情况下产生看起来正确的输出,例如,如果\uxxxx序列(6个字符)是现有路径的一部分,则可以使用诸如“这样的规则将其解释为Unicode转义” ,对于\Uxxxxxxxx(10个字符)序列也是如此,例如,与问题中的输入类似的输入:b"c:\\U0001f60f\\math.dll"可以根据以下内容进行不同的解释磁盘上是否实际存在c:\U0001f60f\math.dll文件:

#!/usr/bin/env python3
import re
from pathlib import Path


def decode_unicode_escape_if_path_doesnt_exist(m):
    path = m.group(0)
    return path if Path(path).exists() else replace_unicode_escapes(path)


def replace_unicode_escapes(text):
    return re.sub(
        fr"{unicode_escape}+",
        lambda m: m.group(0).encode("latin-1").decode("raw-unicode-escape"),
        text,
    )


input_text = Path('broken.txt').read_text(encoding='ascii')
hex = "[0-9a-fA-F]"
unicode_escape = fr"(?:\\u{hex}{{4}}|\\U{hex}{{8}})"
drive_letter = "[a-zA-Z]"
print(
    re.sub(
        fr"{drive_letter}:\S*{unicode_escape}\S*",
        decode_unicode_escape_if_path_doesnt_exist,
        input_text,
    )
)

如果编码文本中包含非ASCII字符,请在broken.txt中指定read_text()文件的实际编码。

用于提取路径的特定正则表达式取决于您获得的输入类型。

您可以通过尝试一次替换一个可能的Unicode序列来使代码复杂化(在这种情况下,替换的数量与候选对象的数量成指数增长,例如,如果路径中有10个可能的Unicode转义序列,则有2**10尝试解码的路径)。

答案 2 :(得分:0)

当AKX发布他的答案时,我已经写了这段代码。我仍然认为这适用。

这个想法是用正则表达式捕获unicode序列候选(并尝试排除路径,例如,以任何字母和冒号开头的部分(例如c:\udfff)。如果解码失败,我们将返回原始字符串。

with open('unicode.txt', 'r', encoding='utf-8') as fin, open('unicode-out.txt', 'w', encoding='utf-8') as fout:
    lines = ''.join(fin.readlines())
    lines = lines.strip()
    lines = unicode_replace(lines)
    fout.write(lines)


def unicode_replace(s):
    # Directory paths in a text are seen as unicode sequences but will fail to decode, e.g. d:\udfs\math.dll
    # In case of such failure, we'll pass on these sentences - we don't try to decode them but leave them
    # as-is. Note that this may leave some unicode sequences alive in your text.
    def repl(match):
        match = match.group()
        try:
            return match.encode('utf-8').decode('unicode-escape')
        except UnicodeDecodeError:
            return match

    return re.sub(r'(?<!\b[a-zA-Z]:)(\\u[0-9A-Fa-f]{4})', repl, s)