Facebook JSON 编码错误

2025-03-12 08:55:00
admin
原创
14
摘要:问题描述:我下载了我的 Facebook Messenger 数据(在你的 Facebook 帐户中,转到设置,然后转到你的 Facebook 信息,然后下载你的信息,然后创建一个至少选中消息框的文件)来做一些有趣的统计但是编码方面存在一个小问题。我不确定,但看起来 Facebook 对此数据使用了错误的编码...

问题描述:

我下载了我的 Facebook Messenger 数据(在你的 Facebook 帐户中,转到设置,然后转到你的 Facebook 信息,然后下载你的信息,然后创建一个至少选中消息框的文件)来做一些有趣的统计

但是编码方面存在一个小问题。我不确定,但看起来 Facebook 对此数据使用了错误的编码。当我用文本编辑器打开它时,我看到的是这样的:Rados/u00c5/u0082aw。当我尝试用 python (UTF-8) 打开它时,我得到的是RadosÅx82aw。但是我应该得到的是:Radosław

我的python脚本:

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

我尝试了几种最常见的编码。示例数据如下:

{
  "sender_name": "Rados/u00c5/u0082aw",
  "timestamp": 1524558089,
  "content": "No to trzeba ostatnie treningi zrobi/u00c4/u0087 xD",
  "type": "Generic"
}

解决方案 1:

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

这意味着字符串数据中的任何非 ASCII 字符都被编码了两次。首先编码为 UTF-8,然后通过使用 JSON转义符号(即文字反斜杠、文字小写字母,后跟 4 个十六进制数字 0-9 和 af),将 UTF-8 字节再次解释为 Latin-1 编码数据(将 256 个字符精确映射到 256 个可能的字节值) 。因为第二步编码的字节值在 0-255 范围内,所以这导致了一系列序列(文字反斜杠、文字小写字母、两个零数字和两个十六进制数字)。/uHHHH`u/u00HHu`0

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

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

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

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

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

  1. 将整个 JSON 文档作为二进制数据加载,将所有/u00hhJSON 序列替换为最后两个十六进制数字所代表的字节,然后解码为 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 或更早版本,则必须repaired bytes从 UTF-8 解码对象,因此请使用json.loads(repaired.decode()))。

从您的样本数据中可以得出:

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

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

解决方案 2:

这是使用 jq 和 iconv 的命令行解决方案。已在 Linux 上测试。

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

解决方案 3:

我想用下面的递归代码片段来扩展@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 恒等函数,这些字典将按“原样”返回。

解决方案 4:

我解析对象的解决方案是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 sv/u00c4/u009bte"}'

# 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

解决方案 5:

基于@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('\\');
    }
}

解决方案 6:

Facebook 程序员似乎混淆了 Unicode编码转义序列的概念,可能是在实现他们自己的临时序列化程序时。更多详细信息请参阅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):
            # /u00c4/u0085
            # 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)

解决方案 7:

这是 @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"))

解决方案 8:

扩展 Martijn 解决方案 #1,我发现它可以导致递归对象处理(它最初确实引导了我):

你可以将它应用于 json 对象的整个字符串,如果你不这样做ensure_ascii

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

然后将其写入文件或类似文件。

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

解决方案 9:

这是我针对 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);

解决方案 10:

根据 Martijn Pieters 的回答,这里是用 Node.js 编写的解决方案

import { readFileSync } from 'node:fs'
import { Buffer } from 'node:buffer'
let data = JSON.parse(readFileSync('message_1.json'),(key, value) => {
    if (typeof value === "string") {
        let buff = Buffer.from(value,'latin1')
        return buff.toString('utf-8')
    } else { return value}
})
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1590  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1361  
  信创产品在政府采购中的占比分析随着信息技术的飞速发展以及国家对信息安全重视程度的不断提高,信创产业应运而生并迅速崛起。信创,即信息技术应用创新,旨在实现信息技术领域的自主可控,减少对国外技术的依赖,保障国家信息安全。政府采购作为推动信创产业发展的重要力量,其对信创产品的采购占比情况备受关注。这不仅关系到信创产业的发展前...
信创和国产化的区别   18  
  信创,即信息技术应用创新产业,旨在实现信息技术领域的自主可控,摆脱对国外技术的依赖。近年来,国货国用信创发展势头迅猛,在诸多领域取得了显著成果。这一发展趋势对科技创新产生了深远的推动作用,不仅提升了我国在信息技术领域的自主创新能力,还为经济社会的数字化转型提供了坚实支撑。信创推动核心技术突破信创产业的发展促使企业和科研...
信创工作   18  
  信创技术,即信息技术应用创新产业,旨在实现信息技术领域的自主可控与安全可靠。近年来,信创技术发展迅猛,对中小企业产生了深远的影响,带来了诸多不可忽视的价值。在数字化转型的浪潮中,中小企业面临着激烈的市场竞争和复杂多变的环境,信创技术的出现为它们提供了新的发展机遇和支撑。信创技术对中小企业的影响技术架构变革信创技术促使中...
信创国产化   19  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用