Facebook在数据导出的JSON文件中使用什么编码?



我已经使用Facebook功能下载了我的所有数据。生成的 zip 文件包含 JSON 文件中的元信息。问题是这些 JSON 文件中字符串中的 unicode 字符以一种奇怪的方式进行转义。

下面是此类字符串的示例:

"nejniu00c5u00beu00c5u00a1u00c3u00ad bod: 0 mnm Benu00c3u00a1tkyn"

当我尝试解析字符串时,例如使用 javascript 的JSON.parse()并将其打印出来,我得到:

"nejnižší bod: 0 mnm Benátkyn"

虽然应该是

"nejnižší bod: 0 mnm Benátkyn"

我可以看到u00c5u00be应该以某种方式对应于ž但我无法弄清楚一般模式。

到目前为止,我已经能够弄清楚这些字符:

'u00c2u00b0' : '°',
'u00c3u0081' : 'Á',
'u00c3u00a1' : 'á',
'u00c3u0089' : 'É',
'u00c3u00a9' : 'é',
'u00c3u00ad' : 'í',
'u00c3u00ba' : 'ú',
'u00c3u00bd' : 'ý',
'u00c4u008c' : 'Č',
'u00c4u008d' : 'č',
'u00c4u008f' : 'ď',
'u00c4u009b' : 'ě',
'u00c5u0098' : 'Ř',
'u00c5u0099' : 'ř',
'u00c5u00a0' : 'Š',
'u00c5u00a1' : 'š',
'u00c5u00af' : 'ů',
'u00c5u00be' : 'ž',

那么这种奇怪的编码是什么?是否有任何已知的工具可以正确解码它?

编码是有效的 UTF-8。问题是,JavaScript不使用UTF-8,它使用UTF-16。所以你必须从有效的 UTF-8 转换为 JavaScript UTF-16:

function decode(s) {
let d = new TextDecoder;
let a = s.split('').map(r => r.charCodeAt());
return d.decode(new Uint8Array(a));
}
let s = "nejniu00c5u00beu00c5u00a1u00c3u00ad bod: 0 mnm Benu00c3u00a1tkyn";
s = decode(s);
console.log(s);

https://developer.mozilla.org/docs/Web/API/TextDecoder

感谢Jen的出色问题和Shawn的评论。

基本上,Facebook似乎获取Unicode字符串表示的每个单独字节,然后导出为JSON,就好像这些字节是单独的Unicode代码点一样。

我们需要做的是取每个六重奏的最后两个字符(例如c3fromu00c3(,将它们连接在一起并读取为 Unicode 字符串。

这就是我在 Ruby 中的做法(见要点(:

require 'json'
require 'uri'
bytes_re = /((?:\\)+|[^\])(?:\u[0-9a-f]{4})+/
txt = File.read('export.json').gsub(bytes_re) do |bad_unicode|
$1 + eval(%Q{"#{bad_unicode[$1.size..-1].gsub('u00', 'x')}"}).to_json[1...-1]
end
good_data = JSON.load(txt)

有了bytes_re我们捕获了所有错误的Unicode字符序列。

然后对于每个序列,将"\u00"替换为"\x"(例如xc3(,"用引号括起来,并使用 Ruby 的内置字符串解析,以便将xc3xbe...字符串转换为实际字节,这些字节稍后将保留为 JSON 中的 Unicode 字符或由#to_json方法正确引用。

[1...-1]是删除由#to_json插入的引号

我想解释代码,因为问题不是特定于 ruby 的,读者可能会使用另一种语言。

我想有人可以用足够丑陋的sed命令来做到这一点。

您可以使用正则表达式查找几乎由 unicode 字符组成的组,将它们解码为 Latin-1,然后编码回 UTF-8

以下代码应该在python3.x中工作:

import re
re.sub(r'[xc2-xf4][x80-xbf]+',lambda m: m.group(0).encode('latin1').decode('utf8'), s)

JSON 文件本身是 UTF-8,但字符串是 UTF-16 字符,转换为字节序列,然后使用转义序列转换为 UTF-8。

此命令修复了 Emacs 中这样的文件:

(defun k/format-facebook-backup ()
"Normalize a Facebook backup JSON file."
(interactive)
(save-excursion
(goto-char (point-min))
(let ((inhibit-read-only t)
(size (point-max))
bounds str)
(while (search-forward ""\u" nil t)
(message "%.f%%" (* 100 (/ (point) size 1.0)))
(setq bounds (bounds-of-thing-at-point 'string))
(when bounds
(setq str (--> (json-parse-string (buffer-substring (car bounds)
(cdr bounds)))
(string-to-list it)
(apply #'unibyte-string it)
(decode-coding-string it 'utf-8)))
(setf (buffer-substring (car bounds) (cdr bounds))
(json-serialize str))))))
(save-buffer))

只需添加如何从"\u00c5\u0098"到"Ř"的一般规则。将 \u 部分的最后两个字母放在一起得到 c5 和 98,它们是 utf-8 表示的两个字节。UTF-8 将代码点编码为两个字节,如下所示:110xxxxx 10xxxxxxx,其中 x 是字符代码的实际位。你可以取两个字节,使用 & 来获取 x 部分,将它们一个接一个地放置,并将其读取为一个数字,你会得到0x158,这是 'Ř' 的代码。

我的JavaScript实现:

function fixEncoding(s) {
var reg = /\u00([a-f0-9]{2})\u00([a-f0-9]{2})/gi;
return s.replace(reg, function(a, m1, m2){
b1 = parseInt(m1,16);
b2 = parseInt(m2,16);
var maskedb1 = b1 & 0x1F;
var maskedb2 = b2 & 0x3F;
var result = (maskedb1 << 6) | maskedb2;
return String.fromCharCode(result);
})
}

以防万一,如果有人正在寻找PHP解决方案;)

$result = preg_replace_callback(
'/\u00([[:xdigit:]]{2})/',
function ($matches) {
return chr(hexdec($matches[1]));
},
$str
);
var_dump($result);

该模式为:"查找以u00开头并继续到十六进制数字的内容"。然后将这些数字转换为相应的字节。

如果有人正在寻找代码的 GO 版本,这里是:

func decode(s string) string {
// Create a slice to hold the individual runes
var runeSlice []rune
// Convert the string to a slice of runes
for _, r := range s {
runeSlice = append(runeSlice, r)
}
// Create a byte slice from the rune slice
byteSlice := make([]byte, len(runeSlice))
for i, r := range runeSlice {
byteSlice[i] = byte(r)
}
// Convert the byte slice to a UTF-8 string
utf8String := string(byteSlice)
// Validate that the string is valid UTF-8
if !utf8.ValidString(utf8String) {
// Handle invalid UTF-8
fmt.Println("Invalid UTF-8 string")
return ""
}
return utf8String

}

最新更新