长话短说

长话短说

尽管 JSON 字符串不能直接表示任意文件路径、进程名称、命令行参数以及更常见的 C 字符串(可能包含以各种字符集编码的文本或根本不意味着文本。

例如,许多 util-linux、Linux LVM、systemd实用程序curl、GNU parallel、ripgrep、sqlite3、tree、许多 FreeBSD 实用程序及其--libxo=json选项...可以以 JSON 格式输出数据,然后据称可以以编程方式“可靠地”对其进行解析。

但是,如果它们要输出的某些字符串(例如文件名)包含未以 UTF-8 编码的文本,那么似乎一切都会崩溃。

在这种情况下,我看到公用事业之间存在不同类型的行为:

  • 那些通过将无法解码的字节替换为替换字符例如?(like exiftool) 或 U+FFFD (�) 或有时以不可逆的方式使用某种形式的编码("\\x80"例如在 中column
  • 那些切换到不同表示形式的内容,例如 from"json-string"[65, 234]字节数组 injournalctl或 from {"text":"foo"}to {"bytes":"base64-encoded"}in rg
  • 那些以虚假方式处理它的人,例如 curl
  • 绝大多数只是转储那些不按原样构成有效 UTF-8 的字节,即包含无效 UTF-8 的 JSON 字符串

大多数util-linux实用程序都属于最后一类。例如lsfd

$ sh -c 'lsfd -Joname -p "$$" --filter "(ASSOC == \"3\")"' 3> $'\x80' | sed -n l
{$
   "lsfd": [$
      {$
         "name": "/home/chazelas/tmp/\200"$
      }$
   ]$
}$

这意味着它们输出无效的 UTF-8,因此输出无效的 JSON。

现在,尽管严格无效,但该输出仍然明确,并且理论上可以进行后处理。

然而,我检查了很多 JSON 处理实用程序,但没有一个能够处理它。他们要么:

  • 因解码错误而出错
  • 将这些字节替换为 U+FFFD
  • 以某种悲惨的方式失败

我觉得我失去了一些东西。当然,当选择这种格式时,一定已经考虑到了这一点吗?

长话短说

所以我的问题是:

  • 带有未正确 UTF-8 编码的字符串(某些字节值 >= 0x80 不构成有效 UTF-8 编码字符的一部分)的 JSON 格式是否有名称?
  • 是否有任何工具或编程语言模块(最好是perl,但我对其他人开放)可以可靠地处理该格式?
  • 或者可以将该格式转换为有效的 JSON 或从有效的 JSON 转换,以便可以由 JSON 处理实用程序(例如jqjson_xsmlr...)进行处理,最好以保留有效 JSON 字符串且不会丢失信息的方式进行处理?

附加信息

以下是我自己的调查情况。这只是您可能会觉得有用的支持数据。这只是一个快速转储,命令采用zsh语法,并在 Debian 不稳定系统(以及某些 FreeBSD 12.4-RELEASE-p5)上运行。抱歉造成混乱。

lsfd(以及大多数 util-linux 实用程序):输出原始数据:

$ sh -c 'lsfd -Joname -p "$$" --filter "(ASSOC == \"3\")"' 3> $'\x80' | sed -n l
{$
   "lsfd": [$
      {$
         "name": "/home/chazelas/\200"$
      }$
   ]$
}$

列: 不明确地转义:

$ printf '%s\n' $'St\351phane' 'St\xe9phane' $'a\0b' | column -JC name=firstname
{
   "table": [
      {
         "firstname": "St\\xe9phane"
      },{
         "firstname": "St\\xe9phane"
      },{
         "firstname": "a"
      }
   ]
}

使用 latin1 (或任何覆盖整个字节范围的单字节字符集)切换到语言环境有助于获取原始格式:

$ printf '%s\n' $'St\351phane' $'St\ue9phane' | LC_ALL=C.iso88591 column -JC name=firstname  | sed -n l
{$
   "table": [$
      {$
         "firstname": "St\351phane"$
      },{$
         "firstname": "St\303\251phane"$
      }$
   ]$
}$

Journalctl:字节数组:

$ logger $'St\xe9phane'
$ journalctl -r -o json | jq 'select(._COMM == "logger").MESSAGE'
[
  83,
  116,
  233,
  112,
  104,
  97,
  110,
  101
]

卷曲:假的

$ printf '%s\r\n' 'HTTP/1.0 200' $'Test: St\xe9phane' '' |  socat -u - tcp-listen:8000,reuseaddr &
$ curl -w '%{header_json}' http://localhost:8000
{"test":["St\uffffffe9phane"]
}

可能是有意义的,\U除了现在 unicode 仅限于代码点\U0010FFFF


cvtsudoers:原始

$ printf 'Defaults secure_path="/home/St\351phane/bin"' | cvtsudoers -f json  | sed -n l
{$
    "Defaults": [$
        {$
            "Options": [$
                { "secure_path": "/home/St\351phane/bin" }$
            ]$
        }$
    ]$
}$

dmesg:原始

$ printf 'St\351phane\n' | sudo tee /dev/kmsg
$ sudo dmesg -J | sed -n /phane/l
         "msg": "St\351phane"$

iproute2:原始且有缺陷

至少ip link,即使是控制字符 0x1 .. 0x1f(只有其中一些在接口名称中不允许)也是原始输出,这在 JSON 中是无效的。

$ ifname=$'\1\xe9'
$ sudo ip link add name $ifname type dummy
$ sudo ip link add name $ifname type dummy

(添加了两次!第一次更名为__)。

$ ip l
[...]
14: __: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 12:22:77:40:6f:8c brd ff:ff:ff:ff:ff:ff
15: �: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 12:22:77:40:6f:8c brd ff:ff:ff:ff:ff:ff
$ ip -j l | sed -n l
[...]
dcast":"ff:ff:ff:ff:ff:ff"},{"ifindex":14,"ifname":"__","flags":["BRO\
ADCAST","NOARP"],"mtu":1500,"qdisc":"noop","operstate":"DOWN","linkmo\
de":"DEFAULT","group":"default","txqlen":1000,"link_type":"ether","ad\
dress":"12:22:77:40:6f:8c","broadcast":"ff:ff:ff:ff:ff:ff"},{"ifindex\
":15,"ifname":"\001\351","flags":["BROADCAST","NOARP"],"mtu":1500,"qd\
isc":"noop","operstate":"DOWN","linkmode":"DEFAULT","group":"default"\
,"txqlen":1000,"link_type":"ether","address":"12:22:77:40:6f:8c","bro\
adcast":"ff:ff:ff:ff:ff:ff"}]$
$ ip -V
ip utility, iproute2-6.5.0, libbpf 1.2.2

exiftool:将字节更改为?

$ exiftool -j $'St\xe9phane.txt'
[{
  "SourceFile": "St?phane.txt",
  "ExifToolVersion": 12.65,
  "FileName": "St?phane.txt",
  "Directory": ".",
  "FileSize": "0 bytes",
  "FileModifyDate": "2023:09:30 10:04:21+01:00",
  "FileAccessDate": "2023:09:30 10:04:26+01:00",
  "FileInodeChangeDate": "2023:09:30 10:04:21+01:00",
  "FilePermissions": "-rw-r--r--",
  "Error": "File is empty"
}]

lsar:将字节值解释为 tar 的 unicode 代码点:

$ tar cf f.tar $'St\xe9phane.txt' $'St\ue9phane.txt'
$ lsar --json f.tar| grep FileNa
      "XADFileName": "Stéphane.txt",
      "XADFileName": "Stéphane.txt",

对于 zip:URI 编码

$ bsdtar --format=zip -cf a.zip St$'\351'phane.txt Stéphane.txt
$ lsar --json a.zip | grep FileNa
      "XADFileName": "St%e9phane.txt",
      "XADFileName": "Stéphane.txt",

lsipc:原始

$ ln -s /usr/lib/firefox-esr/firefox-esr $'St\xe9phane'
$ ./$'St\xe9phane' -new-instance
$ lsipc -mJ | grep -a phane | sed -n l
         "command": "./St\351phane -new-instance"$
         "command": "./St\351phane -new-instance"$

GNU 并行:原始

$ parallel --results -.json echo {} ::: $'\xe9' | sed -n l
{ "Seq": 1, "Host": ":", "Starttime": 1696068481.231, "JobRuntime": 0\
.001, "Send": 0, "Receive": 2, "Exitval": 0, "Signal": 0, "Command": \
"echo '\351'", "V": [ "\351" ], "Stdout": "\351\\u000a", "Stderr": ""\
 }$

rg:从“text”:“...”切换到“bytes”:“base64...”

$ echo $'St\ue9phane' | rg --json '.*'
{"type":"begin","data":{"path":{"text":"<stdin>"}}}
{"type":"match","data":{"path":{"text":"<stdin>"},"lines":{"text":"Stéphane\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"Stéphane"},"start":0,"end":9}]}}
{"type":"end","data":{"path":{"text":"<stdin>"},"binary_offset":null,"stats":{"elapsed":{"secs":0,"nanos":137546,"human":"0.000138s"},"searches":1,"searches_with_match":1,"bytes_searched":10,"bytes_printed":235,"matched_lines":1,"matches":1}}}
{"data":{"elapsed_total":{"human":"0.002445s","nanos":2445402,"secs":0},"stats":{"bytes_printed":235,"bytes_searched":10,"elapsed":{"human":"0.000138s","nanos":137546,"secs":0},"matched_lines":1,"matches":1,"searches":1,"searches_with_match":1}},"type":"summary"}
$ echo $'St\xe9phane' | LC_ALL=C rg --json '.*'
{"type":"begin","data":{"path":{"text":"<stdin>"}}}
{"type":"match","data":{"path":{"text":"<stdin>"},"lines":{"bytes":"U3TpcGhhbmUK"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"St"},"start":0,"end":2},{"match":{"text":"phane"},"start":3,"end":8}]}}
{"type":"end","data":{"path":{"text":"<stdin>"},"binary_offset":null,"stats":{"elapsed":{"secs":0,"nanos":121361,"human":"0.000121s"},"searches":1,"searches_with_match":1,"bytes_searched":9,"bytes_printed":275,"matched_lines":1,"matches":2}}}
{"data":{"elapsed_total":{"human":"0.002471s","nanos":2471435,"secs":0},"stats":{"bytes_printed":275,"bytes_searched":9,"elapsed":{"human":"0.000121s","nanos":121361,"secs":0},"matched_lines":1,"matches":2,"searches":1,"searches_with_match":1}},"type":"summary"}

有趣的“x-用户定义”编码:

$ echo $'St\xe9\xeaphane' | rg -E x-user-defined --json '.*'  | jq -a .data.lines.text
null
"St\uf7e9\uf7eaphane\n"
null
null

包含非 ASCII 文本专用区域中的字符。https://www.w3.org/International/docs/encoding/#x-user-defined


sqlite3:原始

$ sqlite3 -json a.sqlite3 'select * from a' | sed -n l
[{"a":"a"},$
{"a":"\351"}]$

树:原始

$ tree -J | sed -n l
[$
  {"type":"directory","name":".","contents":[$
    {"type":"file","name":"\355\240\200\355\260\200"},$
    {"type":"file","name":"a.zip"},$
    {"type":"file","name":"f.tar"},$
    {"type":"file","name":"St\303\251phane.txt"},$
    {"type":"link","name":"St\351phane","target":"/usr/lib/firefox-es\
r/firefox-esr"},$
    {"type":"file","name":"St\351phane.txt"}$
  ]}$
,$
  {"type":"report","directories":1,"files":6}$
]$

lslocks:原始

$ lslocks --json | sed -n /phane/l
         "path": "/home/chazelas/1/St\351phane.txt"$

@raf生皮: 生的

$ rh -j | sed -n l
[...]
{"path":"./St\351phane", "name":"St\351phane", "start":".", "depth":1\
[...]

FreeBSD ps --libxo=json: 转义:

$ sh -c 'sleep 1000; exit' $'\xe9' &
$ ps --libxo=json -o args -p $!
{"process-information": {"process": [{"arguments":"sh -c sleep 1000; exit \\M-i"}]}
}
$ sh -c 'sleep 1000; exit' '\M-i' &
$ ps --libxo=json -o args -p $!
{"process-information": {"process": [{"arguments":"sh -c sleep 1000; exit \\\\M-i"}]}
}

FreeBSD wc --libxo=json:原始

$ wc --libxo=json  $'\xe9' | LC_ALL=C sed -n l
{"wc": {"file": [{"lines":10,"words":10,"characters":21,"filename":"\351"}]}$
}$

也可以看看该错误报告关于sesutil map --libxo报告者和开发者都期望输出应该是 UTF-8。和介绍 libxo 的讨论其中讨论了编码问题但没有真正的结论。


JSON处理工具

jsec:接受但转换为 U+FFFD

$ jsesc  -j $'\xe9'
"\uFFFD"

jq:接受,转换为 U+FFFD 但伪造:

$ print '"a\351b"' | jq -a .
"a\ufffd"
$ print '"a\351bc"' | jq -a .
"a\ufffdbc"

gojq:同样没有错误

$ echo '"\xe9ab"' | gojq -j . | uconv -x hex
\uFFFD\u0061\u0062

json_pp: 接受,转换为 U+FFFD

$ print '"a\351b"' | json_pp -json_opt ascii,pretty
"a\ufffdb"

json_xs:相同

$ print '"a\351b"' | json_xs | uconv -x hex
\u0022\u0061\uFFFD\u0062\u0022\u000A

-e与:相同

$ print '"\351"' | PERL_UNICODE= json_xs -t none -e 'printf "%x\n", ord($_)'
fffd

jshon: 错误

$ printf '{"file":"St\351phane"}' | jshon -e file -u
json read error: line 1 column 11: unable to decode byte 0xe9 near '"St'

json5:接受,转换为 U+FFFD

$ echo '"\xe9"' | json5 | uconv -x hex
\u0022\uFFFD\u0022

jc: 错误

$ echo 'St\xe9phane' | jc --ls
jc:  Error - ls parser could not parse the input data.
             If this is the correct parser, try setting the locale to C (LC_ALL=C).
             For details use the -d or -dd option. Use "jc -h --ls" for help.

mlr:接受,转换为 U+FFFD

$ echo '{"f":"St\xe9phane"}' | mlr --json cat | sed -n l
[$
{$
  "f": "St\357\277\275phane"$
}$
]$

vd:错误

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 1: invalid continuation byte

JSON::解析:错误

$ echo '"\xe9"'| perl -MJSON::Parse=parse_json -l -0777 -ne 'print parse_json($_)'
JSON error at line 1, byte 3/4: Unexpected character '"' parsing string starting from byte 1: expecting bytes in range 80-bf: 'x80-\xbf' at -e line 1, <> chunk 1.

乔:错误

$ echo '\xe9' | jo -a
jo: json.c:1209: emit_string: Assertion `utf8_validate(str)' failed.
zsh: done             echo '\xe9' |
zsh: IOT instruction  jo -a

可以使用base64:

$ echo '\xe9' | jo a=@-
jo: json.c:1209: emit_string: Assertion `utf8_validate(str)' failed.
zsh: done             echo '\xe9' |
zsh: IOT instruction  jo a=@-
$ echo '\xe9' | jo a=%-
{"a":"6Qo="}

jsed:接受并转换为 U+FFFD

$ echo '{"a":"\xe9"}' | ./jsed get --path a | uconv -x hex
\uFFFD%

1 请参阅 参考资料zgrep -li json ${(s[:])^"$(man -w)"}/man[18]*/*(N)可能正在处理 JSON 的命令列表。

² C 字符串不能表示任意 JSON 字符串,因为与 JSON 字符串相反,C 字符串不能包含 NUL

³ 尽管它的处理可能会出现问题,因为连接两个这样的字符串最终可能会形成有效字符并打破一些假设。

答案1

我对这个话题研究得越多,我就越确信lsfdetc. 的行为是不正确的;RFC 8259 8.1说:

在不属于封闭生态系统的系统之间交换的 JSON 文本必须使用 UTF-8 进行编码

您遇到问题的事实表明这些输出没有封装在封闭的生态系统中,因此 JSON 文本违反了 RFC 8259。

在我看来,确保针对各个项目打开错误报告以提醒他们注意问题是一个很好的做法。然后由项目维护人员决定是否以及如何处理该问题。

我认为从项目维护者的角度来看,这应该是可以解决的:lsfd可以遵守 LC_CTYPE / LANG 环境变量,假设输入来自该语言环境并将其转换为 UTF-8。


带有未正确 UTF-8 编码的字符串(某些字节值 >= 0x80 不构成有效 UTF-8 编码字符的一部分)的 JSON 格式是否有名称?

答案:“坏了”

我是在开玩笑,但只是一点点。事实上,这里发生的情况是 JSON 是以 UTF-8 编写的,但没有执行检查来确保所有输入也是 UTF-8。所以从技术上来说你看到的是混合字符集不是以非标准字符集编码的 json 文件。

是否有任何工具或编程语言模块(最好是 perl,但我对其他人开放)可以可靠地处理该格式?

在特定情况下,一些可能会得到令人满意的结果,例如基于输入完全是 LATIN-1 的(错误)假设进行专门处理。这是有效的,因为 JSON 中的所有特殊字符都是单个 UTF-8 字节代码值,与低于 128 的 ASKII 字符代码相同。许多单字节字符集的前 127 个字节代码具有相同的含义。

但我们要明确的是,我们正在讨论的是处理本来应该是 UTF-8 但实际上不是 UTF-8 的输出。因此,这里的解决方案主要靠运气而不是设计!这类似于“未定义的行为”。


对于某些字符集可能有解决方法。为了使这些变通办法取得成功,字符集要么需要将每个字节代码映射到 unicode,要么您需要确信在实践中将使用非未映射的字节代码。该字符集还需要与 UTF-8 共享单字节字符代码,特别是[]{}:""''\.

LATIN-1 是我所知道的唯一一个可以工作的,而且这只特别有效,因为 Unicode 有一个名为的特殊块Latin-1 补充。这允许通过简单地将字节值复制为 unicode 代码点来将 LATIN-1 转换为 Unicode。

然而,类似的 cp1252 存在无法映射到 unicode 的间隙,并且解决方案很快就会崩溃。


我建议处理这种损坏行为的方法是使用 Python3,它专门理解字节序列和用于表示文本的字符串之间的区别。

您可以在 Python3 中读取原始字节,然后假设您选择的编码解码为字符串:

import sys
import json

data = sys.stdin.buffer.read()
string_data = data.decode("LATIN1")
decoded_structure = json.loads(string_data)

然后,您可以主要使用运算符来操作 json []。例如:对于带有 latin-1 的 json Ç

{
   "lsfd": [
      {
         "name": "/home/chazelas/tmp/Ç"
      }
   ]
}

您可以使用以下命令打印姓名:

import sys
import json

data = sys.stdin.buffer.read()
string_data = data.decode("LATIN1")
decoded_structure = json.loads(string_data)
print(decoded_structure["lsfd"]["name"].encode("LATIN1"))

这种方法还允许您在将数据视为字符串之前将其处理为字节。当事情变得非常混乱时,这很有用,例如输入应该编码为cp1252但包含 cp1252 的无效字节。

import sys
import json

data = sys.stdin.buffer.read()
data = data.replace(b'\x90', b'\\x90')
data = data.replace(b'\x9D', b'\\x9D')
string_data = data.decode("cp1252")
decoded_structure = json.loads(string_data)
print(decoded_structure["lsfd"]["name"].encode("cp1252"))

答案2

如果不需要将 JSON 中的任何字符串视为文本,则一种可能的(不完全令人满意)方法是预处理 JSON 处理工具的输入(jq, mlr...)iconv -f latin1 -t utf-8并对其输出进行后处理with iconv -f utf-8 -t latin,即将所有 >= 0x80 的字节转换为具有相应 Unicode 代码点的字符,或者换句话说,将输入视为以 latin1 编码。

$ exec 3> $'\x80\xff'
$ ls -ld "$(lsfd -Jp "$$" | jq -r '.lsfd[]|select(.assoc=="3").name')"
ls: cannot access '/home/chazelas/1/��': No such file or directory

不起作用,因为jq将这些字节转换为 U+FFFD,但是:

$ ls -ld "$(lsfd -Jp "$$" | iconv -fl1  | jq -r '.lsfd[]|select(.assoc=="3").name' | iconv -tl1)"
-rw-r--r-- 1 chazelas chazelas 0 Sep 30 15:51 '/home/chazelas/tmp/'$'\200\377'

作品。现在有很多方法可能会崩溃:

  • 字符串的长度(以字节数和字符数为单位)在此过程中确实会发生变化,因此您要进行的任何长度检查都可能不准确(尽管 JSON 字符串的字符长度将对应于 JSON 字符串的字节长度)文件名)。
  • 您需要确保 JSON 处理工具不会转义字符\uxxxx(例如不要使用-ain jq),否则字符随后不会转换回字节
  • JSON 处理工具也不得生成包含代码点 >= 0x80 的字符的新字符串,或者如果生成,则需要进行双重编码。就像:jq -r '"Fichier trouvé : " + .file'而不是jq -r '"Fichier trouvé : " + .file'如果你希望它们在经过iconv -f utf-8 -t latin1.
  • 任何基于文本的检查或操作(例如字符类测试、排序等)都将无效。

使用x-user-defined可在 HTML 中使用的字符集而不是 latin1 可以避免其中一些问题,因为所有 >= 0x80 的字节都将映射到专用区域中的连续字符(因此不会被错误地分类为 alpha/空白和不会包含在某些[a-z]/ ... 范围中),但据我所知,//[0-9]都不支持该字符集。iconvuconvrecode

使用 latin1 的优点是您可以根据代码点检查字节值。例如,要查找名称包含字节 0x80 的打开文件:

$ lsfd -Jp "$$" | iconv -fl1 -tu8 | jq -r '.lsfd[]|select(.name|contains("\u0080"))' | iconv -fu8 -tl1
{
  "command": "zsh",
  "pid": 8127,
  "user": "chazelas",
  "assoc": "3",
  "mode": "-w-",
  "type": "REG",
  "source": "0:38",
  "mntid": 42,
  "inode": 2501864,
  "name": "/home/chazelas/tmp/��"
}

��这是我的 UTF-8 终端模拟器在此处呈现这些字节的方式;u8 和 l1 分别是 UTF-8 和 Latin1 又名 ISO-8859-1 的缩写,它们可能不受所有iconv实现的支持)。

您可以定义一个binaryksh(或任何支持pipefail即将成为标准选项的 shell,即除 之外的大多数 shell dash)帮助程序脚本,例如:

#! /bin/ksh -
set -o pipefail
iconv -f latin1 -t utf-8 |
  "$@" |
  iconv -f utf-8 -t latin1

然后使用类似的东西:

lsfd -J |
  binary jq -j '
    .lsfd[] |
    select(
      .assoc=="1" and
      .type=="REG" and
      (.name|match("[^\u0000-\u007f]"))
    ) | (.name + "\u0000")' |
  LC_ALL=C sort -zu |
  xargs -r0 ls -ldU --

列出在任何进程的标准输出上打开的常规文件,其路径包含第 8 位设置的字节(大于 0x7f / 127)。


同样,JSONperl 模块(及其底层JSON::XSJSON::PP实现)及其面向对象的接口,本身并不进行文本解码/编码,它适用于已经解码的文本。默认情况下,只要PERL_UNICODE未设置环境变量,输入/输出都会以 latin1 进行解码/编码。

json_xs/等实用程序json_pp将这些模块公开为命令行工具,显式解码/编码为 UTF-8,但如果直接使用这些模块,则可以跳过该步骤并在 latin1 中工作:

$ exec 3> $'\x80\xff'
$ lsfd -Jp "$$" | perl -MJSON -l -0777 -ne '
   $_ = JSON->new->decode($_);
   print $_->{name} for grep {$_->{assoc} == 3} @{$_->{lsfd}}' |
   sed -n l
/home/chazelas/tmp/\200\377$

它们甚至有一个latin1类似于该标志的显式标志ascii,以确保它们生成的 JSON 在以 latin1 编码时可以通过将 U+0000 .. U+00FF 范围表示为 来表示 U+0000 .. U+00FF 范围之外的字符\uxxxx。如果没有该标志,这些字符最终将以 UTF-8 编码并带有警告消息。

使用 latin1 还使得处理journalctl消息的表示相对容易,因为[1, 2, 3]我们只需将这些字节值转换为具有相应 Unicode 代码点的字符(并且当编码为 latin1 时,您会得到正确的字节)

上面提到的一些限制也适用于此,它相当于iconv内部完成的命令,perl或者更准确地说,我们直接从字节到具有相同值的字符,而不通过字节到 utf-8 和 utf-8 到字符脚步。

$ logger $'St\xe9phane'
$ journalctl --since today -o json | perl -MJSON -MData::Dumper -lne '
   BEGIN{$j = JSON->new}
   $j->incr_parse($_);
   while ($obj = $j->incr_parse) {
     $msg = $obj->{MESSAGE};
     # handle array of integer representation
     $msg = join "", map(chr, @$msg) if ref $msg eq "ARRAY";
     print $msg
   }' |
   sed -n '/phane/l'
St\351phane$

通过这个镜头,我们可以回答所有问题:

  • 该格式的名称是什么?这是 latin1 编码的 JSON,而不是 UTF-8 编码的 JSON,或者是 ASCII 超集的任何单字节字符集,并且具有覆盖我们决定使用的整个字节范围的 Unicode 映射(将输入解释为并生成输出) 。

    与 UTF-8 相比,它们的优点在于每个字节序列都是这些编码中的有效文本,因此它们可用于将任何 Unix 文件名、命令参数、由这些实用程序生成的 C 字符串表示为文本。

    JSON RFC 并不严格禁止使用 UTF-8 以外的编码,只要它在一个封闭的生态系统。它只会对互操作性无效。之前版本的 RFC 对此更加宽松。如果那些生成该格式的工具正确记录了它们所做的事情,那么这可以被视为不是一个错误。

  • 什么工具可以处理这种格式?任何可以以任意字符集(而不仅仅是 UTF-8)解码/编码 JSON 的东西。如上所示,当前版本jello做。JSON// JSON::XSperl模块JSON::PP明确支持latin1。

  • 如何预处理该格式以便常规 JSON 实用程序可以处理它?通过从 Latin1(或其他单字节字符集)重新编码为 UTF-8 来预处理输入,并通过重新编码回来对输出进行后处理。

答案3

python3(至少是我正在测试的版本 3.11.5)及其json模块中,行为类似于perl及其 JSON 模块。输入/输出在 python 模块外部进行解码/编码,在这种情况下,根据语言环境的字符集,尽管字符编码可以被覆盖环境PYTHONIOENCODING变量

C 和 C.UTF-8 语言环境(与使用 UTF-8 作为字符集的其他语言环境相反)似乎是一种特殊情况,其中输入/输出以 UTF-8 进行解码/编码(即使 C 语言环境的字符集始终是 ASCII),但不构成有效 UTF-8 一部分的字节将使用 0xDC80 到 0xDCFF 范围内的代码点进行解码(这些代码点落在用于 UTF-16 代理项对后半部分的代码点中,因此不有效的字符代码点,这使得它们可以在这里安全使用)。

通过设置可以在不更改语言环境的情况下实现相同的效果

PYTHONIOENCODING=utf-8:surrogateescape

然后我们可以处理整体以 UTF-8 编码但可能包含非 UTF-8 字符串的 JSON。

$ printf '"\xe9"' | PYTHONIOENCODING=utf-8:surrogateescape  python3 -c '
import json, sys; _ = json.load(sys.stdin); print(hex(ord(_)))'
0xdce9

0xe9 字节解码为字符 0xdce9。

$ printf '"\xe9"' | PYTHONIOENCODING=utf-8:surrogateescape  python3 -c '
import json, sys; _ = json.load(sys.stdin); print(_)' | od -An -vtx1
 e9 0a

0xdce9 在输出时被编码回 0xe9 字节。

处理输出的示例lsfd

$ exec 3> $'\x80\xff'
$ lsfd -Jp "$$" | PYTHONIOENCODING=utf-8:surrogateescape python3 -c '
import json, sys
_ = json.load(sys.stdin)
for e in _["lsfd"]:
  if e["assoc"] == "3":
    print(e["name"])' | sed -n l
/home/chazelas/tmp/\200\377$

注意:如果在输出上生成一些 JSON,您需要传递ensure_ascii=False或以其他方式传递无法解码为 UTF-8 的字节,您将得到:

$ printf '"\xe9"' | PYTHONIOENCODING=utf-8:surrogateescape python3 -c '
import json, sys; _ = json.load(sys.stdin); print(json.dumps(_))'
"\udce9"

外界的大多数事物python都会拒绝这一点。

$ printf '"\xe9"' | PYTHONIOENCODING=utf-8:surrogateescape python3 -c '
import json, sys
_ = json.load(sys.stdin)
print(json.dumps(_, ensure_ascii=False))' | sed -n l
"\351"$

另外,如问题中所述,如果您有两个 JSON 字符串,它们是在字符中间分割的 UTF-8 编码字符串的结果,则在 JSON 中连接它们不会将这些字节序列合并到字符中,直到它们被编码回 UTF-8:

$ printf '{"a":"St\xc3","b":"\xa9phane"}' | PYTHONIOENCODING=utf-8:surrogateescape python3 -c '
import json, sys
_ = json.load(sys.stdin)
myname = _["a"] + _["b"]; print(len(myname), myname)'
9 Stéphane

我的名字在输出中已重构正常,但请注意长度如何不正确,因为myname包含\udcc3\udca9转义字符而不是重构\u00e9字符。

您可以通过使用 IO 编码执行encode以下步骤来强制合并:decode

$ printf '{"a":"St\xc3","b":"\xa9phane"}' |
   PYTHONIOENCODING=utf-8:surrogateescape python3 -c '
import json,sys
_ = json.load(sys.stdin)
myname = (_["a"] + _["b"]).encode(sys.stdout.encoding,sys.stdout.errors).decode(sys.stdout.encoding,sys.stdout.errors)
print(len(myname), myname)'
8 Stéphane

在任何情况下,也可以像 in 那样以 latin1 进行编码/解码,perl以便通过在使用该字符集的语言环境中调用它或使用PYTHONIOENCODING=latin1.

vd(visidata),虽然是用 编写的python3,但当输入来自 stdin 时似乎并不尊重$PYTHONIOENCODING,并且在 C 或 C.UTF-8 语言环境中似乎没有执行代理转义(请参阅这个问题--encoding=latin1),但用 2.5 或更高版本调用它(其中那个问题已修复)或在使用 latin1 字符集的语言环境中似乎可以工作,因此您可以执行以下操作:

lsfd -J | binary jq .lsfd | LC_CTYPE=C.iso88591 vd -f json

如果输出中存在非 UTF-8 编码文本的命令或文件名,视觉lsfd效果不会崩溃。lsfd -J

当将 JSON 文件作为文件路径作为参数传递时,它似乎根据--encoding--encoding-errors选项对输入进行解码,默认情况下分别是utf-8surrogateescape,并尊重语言环境的输出字符集。

因此,在支持进程替换的 shell 中,例如 ksh、zsh、bash(或rc, es,akanga使用不同的语法),您可以执行以下操作:

vd -f json <(lsfd -J | binary jq .lsfd)

但是,我发现对于非常规文件(例如那些管道),它有时会随机失败(请参阅另一个问题)。使用带有一个 json perl 行 ( ) 的格式jsonl效果更好:

vd -f jsonl <(lsfd -J | binary jq -c '.lsfd[]')

或者=(...)在 zsh 中(或(...|psub -f)在 中fish,与当前版本相同(...|psub))使用进程替换的形式,使用临时文件而不是管道:

vd -f json =(lsfd -J | binary jq .lsfd)

相关内容