转换双斜线UTF-8编码

10

我无法让它工作!我有一个文本文件,其中包含来自保存游戏文件解析器的许多UTF-8中文名称的字节形式,例如在source.txt中:

\xe6\x89\x8e\xe5\x8a\xa0\xe6\x8b\x89

但是,无论我如何将其导入Python(3或2),我得到的最好结果也是这个字符串:

\\xe6\\x89\\x8e\\xe5\\x8a\\xa0\\xe6\\x8b\\x89

我已尝试像其他帖子建议的那样,将字符串重新编码为UTF-8,然后使用unicode转义进行解码,如下所示:

stringName.encode("utf-8").decode("unicode_escape")

但这会破坏原有的编码,并将其作为字符串给出:

'æ\x89\x8eå\x8a\xa0æ\x8b\x89'(打印此字符串结果为:æå æ)

现在,如果我手动复制并粘贴 b + 文件名中的原始字符串,并对其进行编码,则可以获得正确的编码。例如:

b'\xe6\x89\x8e\xe5\x8a\xa0\xe6\x8b\x89'.encode("utf-8")

结果为:'扎加拉'

但是,我无法通过程序实现这一点。我甚至无法去掉双斜杠。

需要明确的是,source.txt 包含单个反斜杠。 我已尝试多种导入方式,但这是最常见的方法:

with open('source.txt','r',encoding='utf-8') as f_open:
    source = f_open.read()

好的,我点击了下面的答案(我想是这个),但这是有效的:

from ast import literal_eval
decodedString = literal_eval("b'{}'".format(stringVariable)).decode('utf-8')

由于其他编码问题,我无法对整个文件进行操作,但是将每个名称作为字符串(stringVariable)提取,然后进行操作是可行的!谢谢!

更明确地说,原始文件不仅包含这些混乱的UTF编码。 它只在某些字段中使用它们。 例如,以下是该文件的开头:

{'m_cacheHandles': ['s2ma\x00\x00CN\x1f\x1b"\x8d\xdb\x1fr \\\xbf\xd4D\x05R\x87\x10\x0b\x0f9\x95\x9b\xe8\x16T\x81b\xe4\x08\x1e\xa8U\x11',
                's2ma\x00\x00CN\x1a\xd9L\x12n\xb9\x8aL\x1d\xe7\xb8\xe6\xf8\xaa\xa1S\xdb\xa5+\t\xd3\x82^\x0c\x89\xdb\xc5\x82\x8d\xb7\x0fv',
                's2ma\x00\x00CN\x92\xd8\x17D\xc1D\x1b\xf6(\xedj\xb7\xe9\xd1\x94\x85\xc8`\x91M\x8btZ\x91\xf65\x1f\xf9\xdc\xd4\xe6\xbb',
                's2ma\x00\x00CN\xa1\xe9\xab\xcd?\xd2PS\xc9\x03\xab\x13R\xa6\x85u7(K2\x9d\x08\xb8k+\xe2\xdeI\xc3\xab\x7fC',
                's2ma\x00\x00CNN\xa5\xe7\xaf\xa0\x84\xe5\xbc\xe9HX\xb93S*sj\xe3\xf8\xe7\x84`\xf1Ye\x15~\xb93\x1f\xc90',
                's2ma\x00\x00CN8\xc6\x13F\x19\x1f\x97AH\xfa\x81m\xac\xc9\xa6\xa8\x90s\xfdd\x06\rL]z\xbb\x15\xdcI\x93\xd3V'],
'm_campaignIndex': 0,
'm_defaultDifficulty': 7,
'm_description': '',
'm_difficulty': '',
'm_gameSpeed': 4,
'm_imageFilePath': '',
'm_isBlizzardMap': True,
'm_mapFileName': '',
'm_miniSave': False,
'm_modPaths': None,
'm_playerList': [{'m_color': {'m_a': 255, 'm_b': 255, 'm_g': 92,   'm_r': 36},
               'm_control': 2,
               'm_handicap': 0,
               'm_hero': '\xe6\x89\x8e\xe5\x8a\xa0\xe6\x8b\x89',

在“m_hero”字段之前的所有信息都不是UTF-8编码。因此,如果文件仅由这些虚假的UTF编码组成,则使用ShadowRanger的解决方案有效,但当我已将m_hero解析为字符串并尝试进行转换时,它无法正常工作。Karin的解决方案可以解决这个问题。

source.txt 中是否实际包含反斜杠,还是这只是 Python 显示的方式?另外,你是如何将其“导入”到 Python 中的?你正在从文件中读取吗?那个文件是怎么打开的? - mhawke
6个回答

14
问题在于 unicode_escape编解码器隐式地将转义修复的结果解码为latin-1而非utf-8。您可以通过以下方式进行修复:
# Read the file as bytes:
with open(myfile, 'rb') as f:
    data = f.read()

# Decode with unicode-escape to get Py2 unicode/Py3 str, but interpreted
# incorrectly as latin-1
badlatin = data.decode('unicode-escape')

# Encode back as latin-1 to get back the raw bytes (it's a 1-1 encoding),
# then decode them properly as utf-8
goodutf8 = badlatin.encode('latin-1').decode('utf-8')

假设文件包含文字反斜杠和代码,而不是它们表示的字节,那么你会得到 '\u624e\u52a0\u62c9'(这应该是正确的,我只是在一个没有支持这些字符的字体的系统上,所以这只是基于Unicode转义的安全repr)。你可以通过在第一阶段使用string-escape编解码器来跳过Py2中的一步(我相信这将允许您省略.encode('latin-1')步骤),但是此解决方案应该具有可移植性,并且成本不应太高。


显然,在Py3中(或者在Py2中使用io.open),您可以通过以文本模式打开并使用encoding ='unicode-escape'来删除显式的decode,假设您的文件应该仅在该模式下解释。上面的示例被拆分成几个步骤,只是为了让步骤更加明显。 - ShadowRanger
这对我的文件不起作用,因为文件中有其他数据,它会抛出一个错误:'utf-8'编解码器无法解码第32个位置的0x8d字节:无效的起始字节。它也不能像Karin的建议那样逐个字符串地工作。 - Jeremy Schutte
1
@JeremySchutte文件中还有其他未显示的数据吗?答案是应该让我们猜的吗?对于所提出的问题,这个答案是正确的。 - Mark Tolonen
@MarkTolonen - 你是正确的。我更新了原始问题以反映这一点。 - Jeremy Schutte

7
我假设你正在使用Python 3。在Python 2中,默认情况下字符串是字节,因此它会正常工作。但在Python 3中,字符串是unicode并被解释为unicode,这使得如果你有一个字节字符串被读取为unicode,这个问题就更难了。
这个解决方案受到mgilson答案的启发。我们可以通过使用literal_eval来字面上评估您的unicode字符串作为字节字符串:
from ast import literal_eval

with open('source.txt', 'r', encoding='utf-8') as f_open:
    source = f_open.read()
    string = literal_eval("b'{}'".format(source)).decode('utf-8')
    print(string)  # 扎加拉

好的,这段代码在源文件上并不起作用,因为源文件中还有其他数据。但是它可以在字符串级别上工作,就像这样:from ast import literal_eval string = literal_eval("b'{}'".format(heroName)).decode('utf-8') - Jeremy Schutte
这是次优的。伪造一个表示字节对象的字符串,以使用文字解析而不是使用unicode-escape(或string-escape)来进行解码非常棘手,并且如果输入数据恰好没有使用纯转义,则存在风险;其中包含一个文字单引号,就会导致解码失败,而使用正确的编解码器则没有这样的边缘情况。 - ShadowRanger
@ShadowRanger - 是的,这对整个文件不起作用;它立即抛出错误bad syntax,因为文件中有其他数据,这些数据不是UTF-8编码,而它试图将其解码为UTF-8:b'{'m_cacheHandles': ['s2ma\x00\x00CN\x1f\x1b"\x8d\xdb\x1fr \\xbf\xd4D\x05R\x87\x10\x0b\x0f9\x95\x9b\xe8\x16T\x81b\xe4\x08\x1e\xa8U\x11', ^ SyntaxError: invalid syntax但是,它可以逐个字符串地工作,这已经足够满足我的需求。 - Jeremy Schutte
@ShadowRanger 很高兴给你的答案点赞。我不知道 OP 是如何达到这种状态的,因此我没有做出任何假设。正如其他人所指出的那样,最好修复文件的实际编写方式,以便可以轻松地读取为 utf-8 编码。但是,如果我们只有一个字节表示的字符串,并且需要将其作为字节使用,那么这就是我能想到的最简单的通用方法。显然,问题可能是更具体的(因此有更具体的解决方案),正如你所推断的那样。 - Karin
谢谢!在尝试了几个类似问题的stackoverflow解决方案之后,这个最终对我有用! - md1630

1

您可以做一些愚蠢的事情,比如评估字符串:

import ast
s = r'\xe6\x89\x8e\xe5\x8a\xa0\xe6\x8b\x89'
print ast.literal_eval('"%s"' % s).decode('utf-8')
  • 如果您不想让攻击者访问您的系统,建议使用ast.literal_eval

在您的情况下使用可能如下所示:

with open('file') as file_handle:
    data = ast.literal_eval('"%s"' % file.read()).decode('utf-8')

我认为这里的真正问题很可能是您有一个包含表示字节的字符串的文件(而不是仅存储字节本身的文件)。因此,修复首先生成该文件的代码可能是更好的选择。但是,如果无法解决该问题,以下是我能想到的下一步最佳方法...

我无法将双斜杠转换为单斜杠,而不复制控制台中的内容,因此这种方法行不通。 - Jeremy Schutte
@JeremySchutte:r让你感到困惑,但你确实有类似于所示的字节。 - Ignacio Vazquez-Abrams
我不能硬编码这个: r'\xe6\x89\x8e\xe5\x8a\xa0\xe6\x8b\x89' 而且显然输入: rSTRINGVARIABLE 也不起作用。 - Jeremy Schutte
@JeremySchutte -- 我并不是在建议你这样做。我是说,根据我理解你的问题,s 应该类似于文件中的一行。因此,for line in your_file: print ast.literal_eval('"%s"' % line) 看起来应该可以解决问题(除非我误解了问题)。 - mgilson

1
使用Python3进行解决方案,仅使用字符串操作和编码转换,而不使用邪恶的eval :)
import binascii

str = '\\xe6\\x89\\x8e\\xe5\\x8a\\xa0\\xe6\\x8b\\x89'
str = str.replace('\\x', '')  # str == 'e6898ee58aa0e68b89'

# we can use any encoding as long as it translate ascii as is,
# for example we can do str.encode('ascii') here
str = str.encode('utf8')  # str == b'e6898ee58aa0e68b89'

str = binascii.a2b_hex(str)  # str == b'\xe6\x89\x8e\xe5\x8a\xa0\xe6\x8b\x89'
str = str.decode('utf8')  # str == '扎加拉'

如果你喜欢一行代码,那么我们可以简单地表述为:
binascii.a2b_hex(str.replace('\\x', '').encode()).decode('utf8')

0

在一天结束时,你得到的是一个字符串对吧?我会使用string.replace方法将双斜杠转换为单斜杠,并添加b前缀使其正常工作。


我如何能够在不复制粘贴每个字符串的情况下完成这个任务? - Jeremy Schutte
当您打印源代码时,输出是什么? - slopeofhope

0

因此,解释数据“以字节形式存在”有几种不同的方式。让我们假设你确实这样做:

s = b'\xe6\x89\x8e\xe5\x8a\xa0\xe6\x8b\x89'

b前缀表示这些是字节。不涉及字节与代码点/字符之间的混乱以及Python 2和3之间的长期差异,b前缀字符串表示这些是意图为字节(例如原始UTF-8字节)。

然后只需解码它,将UTF-8编码(已经在字节中)转换为真正的Unicode字符。例如,在Python 2.7中:

print s.decode('utf-8')

产生:

扎加拉

你的一个示例进行了编码后解码,这只会带来悲伤和痛苦。如果你的变量保存了真正的UTF-8字节,那么你只需要解码。

更新 根据讨论,似乎数据并不是真正的UTF-8字节,而是同样的字符串序列化版本。有很多方法可以从字符串序列化转换为字节。以下是我的方法:

from struct import pack

def byteize(s):
    """
    Given a backslash-escaped string serialization of bytes,
    decode it into a genuine byte string.
    """
    bvals = [int(s[i:i+2], 16) for i in range(2, len(s), 4)]
    return pack(str(len(bvals)) + 'B', *bvals)

然后:

print byteize(s).decode('utf-8')

如之前所述:

扎加拉

这个 byteize() 不像基于 literal_eval()accepted answer 那样通用,但是 %timeit 基准测试显示它在短字符串上的速度约快33%。在 Python 2 下,通过将 range 替换为 xrange 可以进一步加速。然而,考虑到其较低级别的特性,literal_eval 方法在长字符串上胜出。

100000 loops, best of 3: 6.19 µs per loop
100000 loops, best of 3: 8.3 µs per loop

1
我认为 OP 没有以“字节”形式拥有它。你那里有一串字节 -- OP 似乎有一个包含字节表示的字符串文件。我不完全确定 OP 是如何陷入那种困境的,但是假设在源头修复它不是一个选项,我认为这并不能帮助解决问题。 - mgilson
是的,我无法获得像这样表示的字符串: s = b'\xe6\x89\x8e\xe5\x8a\xa0\xe6\x8b\x89' 最好的情况下,我能够获得: s = "b'\xe6\x89\x8e\xe5\x8a\xa0\xe6\x8b\x89'" - Jeremy Schutte
我不太清楚。OP声称它是“以字节形式”存在的。如果还没有转换成UTF-8字节,而是UTF-8的进一步序列化,那么显然必须先将其转换为真正的UTF-8数据。 - Jonathan Eunice

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接