Facebook JSON 编码错误



我下载了我的Facebook信使数据(在您的Facebook帐户中,转到设置,然后转到您的Facebook信息,然后下载您的信息,然后创建一个至少选中消息框的文件)以执行一些很酷的统计信息

但是,编码存在一个小问题。我不确定,但看起来Facebook对这些数据使用了错误的编码。当我使用文本编辑器打开它时,我看到如下所示的内容:Radosu00c5u0082aw.当我尝试用python(UTF-8)打开它时,我得到RadosÅx82aw.但是我应该得到:Radosław.

我的蟒蛇脚本:

text = open(os.path.join(subdir, file), encoding='utf-8')
conversations.append(json.load(text))

我尝试了一些最常见的编码。示例数据为:

{
"sender_name": "Radosu00c5u0082aw",
"timestamp": 1524558089,
"content": "No to trzeba ostatnie treningi zrobiu00c4u0087 xD",
"type": "Generic"
}

我确实可以确认Facebook下载数据的编码不正确; 一个Mojibake。原始数据采用 UTF-8 编码,但被解码为 Latin-1。我会确保提交错误报告。

这意味着字符串数据中的任何非 ASCII 字符都经过两次编码。首先是 UTF-8,然后是 UTF-8 字节,通过使用uHHHHJSON 转义表示法(因此文字反斜杠、文字小写字母u,后跟 4 个十六进制数字 0-9 和 a-f),将它们解释为 Latin-1 编码数据(将 256 个字符精确映射到 256 个可能的字节值)再次编码 UTF-8 字节。由于第二步编码的字节值在 0-255 范围内,这导致了一系列u00HH序列(文字反斜杠、文字小写字母u、两个0零位数字和两个十六进制数字)。

例如,名称Radosław中的 Unicode 字符 U+0142 拉丁小写字母 L 和笔画被编码为 UTF-8 字节值 C5 和 82(十六进制表示法),然后再次编码为u00c5u0082

您可以通过两种方式修复损坏:

  1. 将数据解码为 JSON,然后将任何字符串值重新编码为 Latin-1 二进制数据,然后再次解码为 UTF-8:

    >>> import json
    >>> data = r'"Radosu00c5u0082aw"'
    >>> json.loads(data).encode('latin1').decode('utf8')
    'Radosław'
    

    当然,这需要完全遍历数据结构才能找到所有这些字符串。

  2. 将整个 JSON 文档加载为二进制数据,将所有 JSON 序列替换为最后两个十六进制数字表示的字节u00hh然后解码为 JSON:

    import re
    from functools import partial
    fix_mojibake_escapes = partial(
    re.compile(rb'\u00([da-f]{2})').sub,
    lambda m: bytes.fromhex(m[1].decode()),
    )
    with open(os.path.join(subdir, file), 'rb') as binary_data:
    repaired = fix_mojibake_escapes(binary_data.read())
    data = json.loads(repaired)
    

    (如果您使用的是 Python 3.5 或更早版本,则必须从 UTF-8 解码repairedbytes对象,因此请使用json.loads(repaired.decode()))。

    从您的示例数据中,这将产生:

    {'content': 'No to trzeba ostatnie treningi zrobić xD',
    'sender_name': 'Radosław',
    'timestamp': 1524558089,
    'type': 'Generic'}
    

    正则表达式与二进制数据中的所有u00HH序列匹配,并用它们表示的字节替换这些序列,以便可以将数据正确解码为 UTF-8。当给定二进制数据时,第二个解码由json.loads()函数负责。

这是一个带有 jq 和 iconv 的命令行解决方案。在Linux上测试。

cat message_1.json | jq . | iconv -f utf8 -t latin1 > m1.json

我想用下面的递归代码片段来扩展@Geekmoss的答案,我曾经解码过我的Facebook数据。

import json
def parse_obj(obj):
if isinstance(obj, str):
return obj.encode('latin_1').decode('utf-8')
if isinstance(obj, list):
return [parse_obj(o) for o in obj]
if isinstance(obj, dict):
return {key: parse_obj(item) for key, item in obj.items()}
return obj
decoded_data = parse_obj(json.loads(file))

我注意到这效果更好,因为您下载的 facebook 数据可能包含字典列表,在这种情况下,由于 lambda 身份函数,这些字典将"按原样"返回。

我解析对象的解决方案在加载/加载函数上使用parse_hook回调:

import json

def parse_obj(dct):
for key in dct:
dct[key] = dct[key].encode('latin_1').decode('utf-8')
pass
return dct

data = '{"msg": "Ahoj svu00c4u009bte"}'
# String
json.loads(data)  
# Out: {'msg': 'Ahoj svÄx9bte'}
json.loads(data, object_hook=parse_obj)  
# Out: {'msg': 'Ahoj světe'}
# File
with open('/path/to/file.json') as f:
json.load(f, object_hook=parse_obj)
# Out: {'msg': 'Ahoj světe'}
pass

更新:

使用字符串解析列表的解决方案不起作用。所以这是更新的解决方案:

import json

def parse_obj(obj):
for key in obj:
if isinstance(obj[key], str):
obj[key] = obj[key].encode('latin_1').decode('utf-8')
elif isinstance(obj[key], list):
obj[key] = list(map(lambda x: x if type(x) != str else x.encode('latin_1').decode('utf-8'), obj[key]))
pass
return obj

基于@Martijn Pieters解决方案,我用Java编写了类似的东西。

public String getMessengerJson(Path path) throws IOException {
String badlyEncoded = Files.readString(path, StandardCharsets.UTF_8);
String unescaped = unescapeMessenger(badlyEncoded);
byte[] bytes = unescaped.getBytes(StandardCharsets.ISO_8859_1);
String fixed = new String(bytes, StandardCharsets.UTF_8);
return fixed;
}

unescape 方法的灵感来自 org.apache.commons.lang.StringEscapeUtils。

private String unescapeMessenger(String str) {
if (str == null) {
return null;
}
try {
StringWriter writer = new StringWriter(str.length());
unescapeMessenger(writer, str);
return writer.toString();
} catch (IOException ioe) {
// this should never ever happen while writing to a StringWriter
throw new UnhandledException(ioe);
}
}
private void unescapeMessenger(Writer out, String str) throws IOException {
if (out == null) {
throw new IllegalArgumentException("The Writer must not be null");
}
if (str == null) {
return;
}
int sz = str.length();
StrBuilder unicode = new StrBuilder(4);
boolean hadSlash = false;
boolean inUnicode = false;
for (int i = 0; i < sz; i++) {
char ch = str.charAt(i);
if (inUnicode) {
unicode.append(ch);
if (unicode.length() == 4) {
// unicode now contains the four hex digits
// which represents our unicode character
try {
int value = Integer.parseInt(unicode.toString(), 16);
out.write((char) value);
unicode.setLength(0);
inUnicode = false;
hadSlash = false;
} catch (NumberFormatException nfe) {
throw new NestableRuntimeException("Unable to parse unicode value: " + unicode, nfe);
}
}
continue;
}
if (hadSlash) {
hadSlash = false;
if (ch == 'u') {
inUnicode = true;
} else {
out.write("\");
out.write(ch);
}
continue;
} else if (ch == '\') {
hadSlash = true;
continue;
}
out.write(ch);
}
if (hadSlash) {
// then we're in the weird case of a  at the end of the
// string, let's output it anyway.
out.write('\');
}
}

Facebook程序员似乎混淆了Unicode编码转义序列的概念,可能是在实现自己的ad-hoc序列化程序时。有关更多详细信息,请参阅 Facebook 数据导出中的无效 Unicode 编码。

试试这个:

import json
import io
class FacebookIO(io.FileIO):
def read(self, size: int = -1) -> bytes:
data: bytes = super(FacebookIO, self).readall()
new_data: bytes = b''
i: int = 0
while i < len(data):
# u00c4u0085
# 0123456789ab
if data[i:].startswith(b'\u00'):
u: int = 0
new_char: bytes = b''
while data[i+u:].startswith(b'\u00'):
hex = int(bytes([data[i+u+4], data[i+u+5]]), 16)
new_char = b''.join([new_char, bytes([hex])])
u += 6
char : str = new_char.decode('utf-8')
new_chars: bytes = bytes(json.dumps(char).strip('"'), 'ascii')
new_data += new_chars
i += u
else:
new_data = b''.join([new_data, bytes([data[i]])])
i += 1
return new_data
if __name__ == '__main__':
f = FacebookIO('data.json','rb')
d = json.load(f)
print(d)

这是@Geekmoss的答案,但适用于Python 3:

def parse_facebook_json(json_file_path):
def parse_obj(obj):
for key in obj:
if isinstance(obj[key], str):
obj[key] = obj[key].encode('latin_1').decode('utf-8')
elif isinstance(obj[key], list):
obj[key] = list(map(lambda x: x if type(x) != str else x.encode('latin_1').decode('utf-8'), obj[key]))
pass
return obj
with json_file_path.open('rb') as json_file:
return json.load(json_file, object_hook=parse_obj)
# Usage
parse_facebook_json(Path("/.../message_1.json"))

扩展 Martijn 解决方案 #1,我看到它可能导致递归对象处理(它最初肯定会引导我):

如果您不ensure_ascii,则可以将其应用于整个 json 对象字符串

json.dumps(obj, ensure_ascii=False, indent=2).encode('latin-1').decode('utf-8')

然后将其写入文件或其他内容。

PS:这应该是对@Martijn答案的评论:https://stackoverflow.com/a/50011987/1309932(但我无法添加评论)

这是我对 Node 17.0.1 的方法,基于 @hotigeftas 递归代码,使用 iconv-lite 包。

import iconv from 'iconv-lite';
function parseObject(object) {
if (typeof object == 'string') {
return iconv.decode(iconv.encode(object, 'latin1'), 'utf8');;
}
if (typeof object == 'object') {
for (let key in object) {
object[key] = parseObject(object[key]);
}
return object;
}
return object;
}
//usage
let file = JSON.parse(fs.readFileSync(fileName));
file = parseObject(file);

最新更新