긴 이야기 짧게

긴 이야기 짧게

JSON 문자열은 임의의 파일 경로, 프로세스 이름, 명령줄 인수 및 보다 일반적인 C 문자열(다양한 문자 집합으로 인코딩된 텍스트를 포함하거나 텍스트가 전혀 없음을 의미할 수 있음)을 직접적으로 나타낼 수는 없습니다.

예를 들어, 많은 util-linux, Linux LVM, systemdutilities curl, GNU parallel, ripgrep, sqlite3, tree,다양한 FreeBSD 유틸리티 및 해당 --libxo=json옵션...JSON 형식으로 데이터를 출력할 수 있으며, 프로그래밍 방식으로 "신뢰할 수 있게" 구문 분석할 수 있습니다.

그러나 출력하려는 ​​일부 문자열(예: 파일 이름)에 UTF-8로 인코딩되지 않은 텍스트가 포함되어 있으면 모든 것이 중단되는 것처럼 보입니다.

이 경우 유틸리티 간에 다양한 유형의 동작이 표시됩니다.

  • 디코딩할 수 없는 바이트를 다음으로 대체하는 것문자 바꾸기예를 들어 ?(예: exiftool) 또는 U+FFFD(�) 또는 때로는 되돌릴 수 없는 방식으로 일부 인코딩 형식을 사용합니다( 예 "\\x80":column
  • "json-string"from 에서 [65, 234]바이트 배열 in journalctl또는 from {"text":"foo"}에서 {"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로 인코딩된 문자열이 잘못 포함된 JSON 형식의 이름이 있습니까(일부 바이트 값 ​>= 0x80은 유효한 UTF-8 인코딩 문자의 일부를 형성하지 않음)?
  • perl이 형식을 안정적으로 처리할 수 있는 도구나 프로그래밍 언어 모듈(바람직하지만 다른 사람들에게도 열려 있음) 이 있습니까 ?
  • jq또는 JSON 처리 유틸리티(예: , json_xs, ...) 에서 처리할 수 있도록 형식을 유효한 JSON으로 변환하거나 그 반대로 변환할 수 있습니다. mlr바람직하게는 정보 손실 없이 유효한 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이제 유니코드가 코드 포인트로 제한된다는 점을 제외하면 말이 됩니다 \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의 유니코드 코드 포인트로 해석합니다.

$ 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-사용자 정의


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}$
]$

lslock:원본

$ 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

JSON: 오류

$ 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%

zgrep -li json ${(s[:])^"$(man -w)"}/man[18]*/*(N)1 JSON을 처리할 수 있는 명령 목록은 참고자료를 참조하세요 .

² C 문자열은 JSON 문자열과 달리 NUL을 포함할 수 없기 때문에 임의의 JSON 문자열을 나타낼 수 없습니다.

3 처리가 문제가 될 수 있지만 두 개의 문자열을 연결하면 유효한 문자로 끝나고 일부 가정이 깨질 수 있습니다.

답변1

이 주제에 대해 더 많이 조사할수록 lsfd등의 행동이 올바르지 않다는 확신이 더욱 커졌습니다.RFC 8259 8.1설명하다:

폐쇄형 생태계에 속하지 않는 시스템 간에 교환되는 JSON 텍스트는 UTF-8을 사용하여 인코딩되어야 합니다.

문제가 있다는 사실은 이러한 출력이 폐쇄된 생태계에 캡슐화되지 않았으므로 JSON 텍스트가 RFC 8259를 위반한다는 것을 의미합니다.

제 생각에는 개별 프로젝트에 대한 버그 보고서를 열어 문제를 알리는 것이 좋은 습관입니다. 그런 다음 문제를 처리할지 여부와 처리 방법을 결정하는 것은 프로젝트 유지관리자에게 달려 있습니다.

나는 이것이 프로젝트 유지관리자의 관점에서 해결 가능해야 한다고 생각합니다. lsfdLC_CTYPE/LANG 환경 변수를 존중하고 입력이 해당 로케일에서 온다고 가정하고 이를 UTF-8로 변환할 수 있습니다.


UTF-8로 인코딩된 문자열이 잘못 포함된 JSON 형식의 이름이 있습니까(일부 바이트 값 ​>= 0x80은 유효한 UTF-8 인코딩 문자의 일부를 형성하지 않음)?

대답: "깨진"

농담이지만 ​​조금뿐입니다. 실제로 여기서 일어나는 일은 JSON이 UTF-8로 작성되었지만 모든 입력도 UTF-8인지 확인하기 위한 검사가 수행되지 않는다는 것입니다. 기술적으로 여러분이 보고 있는 것은혼합문자 세트는 비표준 문자 세트로 인코딩된 json 파일이 아닙니다.

이 형식을 안정적으로 처리할 수 있는 도구나 프로그래밍 언어 모듈(Perl을 선호하지만 다른 사용자에게도 열려 있음)이 있습니까?

일부는 입력이 전적으로 LATIN-1이라는 (잘못된) 가정에 기초한 특수 처리와 같은 특정 경우에 만족스러운 결과를 얻을 수 있습니다. 이는 JSON의 모든 특수 문자가 단일 UTF-8 바이트 코드 값(128 이하의 ASKII 문자 코드와 동일)이기 때문에 작동합니다. 많은 단일 바이트 문자 집합의 처음 127바이트 코드는 동일한 의미를 갖습니다.

하지만 분명히 해두자면, 우리는 UTF-8이어야 하지만 UTF-8이 아닌 출력을 처리하는 것에 대해 이야기하고 있다는 것입니다. 따라서 여기서 해결책은 디자인보다는 운에 달려 있습니다! 이는 "정의되지 않은 동작"과 유사합니다.


특정 문자 집합에 대한 해결 방법이 있을 수 있습니다. 이러한 해결 방법이 성공하려면 문자 집합이 모든 바이트 코드를 유니코드로 매핑해야 하거나 실제로 매핑되지 않은 바이트 코드가 사용된다는 점을 확신해야 합니다. 문자 집합은 또한 UTF-8, 특히 1바이트 문자 코드를 공유해야 합니다 []{}:""''\.

LATIN-1은 내가 아는 유일한 것인데, 이는 유니코드에 LATIN-1이라는 특수 블록이 있기 때문에 특별히 작동합니다.라틴어-1 보충. 이를 통해 바이트 값을 유니코드 코드 포인트에 복사하기만 하면 LATIN-1을 유니코드로 변환할 수 있습니다.

그러나 유사한 cp1252에는 유니코드로 매핑할 수 없는 공백이 있어 솔루션이 빠르게 중단됩니다.


이 손상된 동작을 처리하기 위해 제가 제안하는 방법은 Python3을 사용하는 것입니다. 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하고 출력을 로 후처리하는 iconv -f utf-8 -t latin것입니다. 0x80보다 큰 모든 바이트를 해당 유니코드 코드 포인트가 있는 문자로 변환합니다. 즉, 입력을 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 처리 도구가 문자를 이스케이프하지 않는지 확인해야 합니다 (예: in 을 \uxxxx사용하지 않음 ). 그렇지 않으면 문자가 나중에 바이트로 다시 변환되지 않습니다.-ajq
  • 또한 JSON 처리 도구는 코드 포인트가 0x80보다 큰 문자를 포함하는 새 문자열을 생성해서는 안 됩니다. 생성하는 경우 이중 인코딩이 필요합니다. 예: 그들이 지나가기를 원한다면 jq -r '"Fichier trouvé : " + .file'안 됩니다 .jq -r '"Fichier trouvé : " + .file'iconv -f utf-8 -t latin1
  • 텍스트 기반 확인이나 작업(예: 문자 클래스 테스트, 정렬 등)은 효과가 없습니다.

latin1 대신 HTML에서 사용할 수 있는 문자 세트를 사용하면 x-user-defined이러한 문제 중 일부를 피할 수 있습니다. 0x80보다 큰 모든 바이트가 개인 영역의 연속 문자에 매핑되므로 알파/공백으로 잘못 분류되지 않고 일부 /... 범위에 포함되지만 [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표준 옵션을 지원하는 모든 쉘, 즉 다음을 제외한 대부분의 쉘) 를 정의할 수 있습니다.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::XS및 구현)은 텍스트 자체를 디코딩/인코딩하지 않고 이미 디코딩된 텍스트에서 작동합니다. JSON::PP기본적으로 PERL_UNICODE환경 변수가 설정되지 않은 한 입출력은 latin1로 디코딩/인코딩됩니다.

json_xs/와 같은 유틸리티는 json_ppUTF-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$

그들은 생성한 JSON이 latin1 외부 문자로 인코딩될 때 U+0000 .. U+00FF 범위를 표현하여 표현할 수 있도록 하기 위해 latin1이와 유사한 명시적 플래그 도 가지고 있습니다 . 이 플래그가 없으면 이러한 문자는 경고 메시지와 함께 UTF-8로 인코딩됩니다.ascii\uxxxx

journalctllatin1을 사용하면 메시지 표현을 상대적으로 쉽게 처리할 수 있습니다 . 왜냐하면 [1, 2, 3]이러한 바이트 값을 해당 유니코드 코드 포인트가 있는 문자로 변환하기 때문입니다(그리고 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$

이 렌즈를 통해 우리는 모든 질문에 답할 수 있습니다.

  • 이 형식의 이름은 무엇입니까? 이는 UTF-8로 인코딩된 JSON이 아닌 latin1로 인코딩된 JSON이거나 ASCII의 상위 집합인 단일 바이트 문자 집합이며 사용하기로 결정한 전체 바이트 범위를 포괄하는 유니코드 매핑이 있습니다(입력을 다음과 같이 해석하고 생성합니다). 산출).

    UTF-8에 비해 장점은 모든 바이트 시퀀스가 ​​이러한 인코딩에서 유효한 텍스트이므로 이러한 유틸리티에서 생성된 모든 Unix 파일 이름, 명령 인수, C 문자열을 text 로 나타내는 데 사용할 수 있다는 것입니다.

    JSON RFC는 UTF-8 이외의 인코딩 사용을 엄격하게 금지하지 않습니다.폐쇄된 생태계. 상호 운용성에는 아무런 영향을 미치지 않습니다.이전 버전의 RFC는 이에 대해 더 관대했습니다.. 형식을 생성하는 도구가 수행하는 작업을 올바르게 문서화하면 이는 버그가 아닌 것으로 간주될 수 있습니다.

  • 이 형식을 처리할 수 있는 도구는 무엇입니까? 모든 문자 세트(UTF-8뿐만 아니라)에서 JSON을 디코딩/인코딩할 수 있는 모든 것. 위와 같이 현재 버전은jello하다. JSON// JSON::XSPerl 모듈은 JSON::PPlatin1을 명시적으로 지원합니다.

  • 일반 JSON 유틸리티가 처리할 수 있도록 이 형식을 어떻게 사전 처리할 수 있나요? Latin1(또는 다른 단일 바이트 문자 집합)에서 UTF-8로 다시 코딩하여 입력을 전처리하고 다시 코딩하여 출력을 후처리합니다.

답변3

python3(적어도 내가 테스트하고 있는 버전 3.11.5) 에서 해당 모듈은 JSON 모듈 json처럼 작동합니다 . perl입력/출력은 Python 모듈 외부에서 디코딩/인코딩됩니다. 이 경우에는 로케일의 문자 세트에 따라 문자 인코딩을 재정의할 수 있습니다.환경 PYTHONIOENCODING변수.

C 및 C.UTF-8 로케일(UTF-8을 문자 집합으로 사용하는 다른 로케일과 달리)은 입력/출력이 UTF-8로 디코딩/인코딩되는 특별한 경우로 보입니다(C 로케일의 문자가 집합은 항상 ASCII임), 유효한 UTF-8의 일부를 형성하지 않는 바이트는 0xDC80에서 0xDCFF 범위의 코드 포인트를 사용하여 디코딩됩니다(이 코드 포인트는 UTF-16 대체의 후반부에 사용되는 코드 포인트에 속함) pair 이므로 유효한 문자 코드 포인트가 아니므로 여기에서 사용해도 안전합니다).

설정을 통해 로케일을 변경하지 않고도 동일한 효과를 얻을 수 있습니다.

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=FalseUTF-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"$

또한 질문에서 언급했듯이 UTF-8로 인코딩된 문자열이 문자 중간에 분할된 결과인 두 개의 JSON 문자열이 있는 경우 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

내 이름은 출력에서 ​​잘 재구성되지만 문자가 reconstructed 대신 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

perl어떤 경우든 해당 문자 집합을 사용하는 로케일에서 호출하거나 를 사용하여 latin1에서 인코딩/디코딩하는 것도 가능합니다 PYTHONIOENCODING=latin1.

vd(visidata)는 로 작성되었지만 python3입력이 stdin 에서 오는 경우를 고려하지 않는 것으로 보이며 $PYTHONIOENCODINGC 또는 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-8하고 surrogateescape로캘의 출력 문자 집합을 존중하는 것으로 보입니다.

rc따라서 ksh, zsh, bash( 또는 다른 구문을 사용하는 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)zsh )에서 프로세스 대체 형식을 사용합니다.fish(...|psub)

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

관련 정보