使用特殊字符的ConvertTo-Json和ConvertFrom-Json

13

我有一个文件,其中包含一些属性,其中某些属性的值包含转义字符,例如一些Url和正则表达式模式。

当读取内容并转换回json时,无论是否进行取消转义,都会导致内容不正确。如果进行取消转义后再转换为json,某些正则表达式会发生错误,如果使用取消转义,则会导致 Urls 和某些正则表达式出现问题。

我该如何解决这个问题?

最小完整可复现示例

以下是一些简单的代码块,可让您轻松重现此问题:

内容

$fileContent = 
@"
{
    "something":  "http://domain/?x=1&y=2",
    "pattern":  "^(?!(\\`|\\~|\\!|\\@|\\#|\\$|\\||\\\\|\\'|\\\")).*"
}
"@

使用Unescape函数

如果我阅读内容,然后使用以下命令将内容转换回JSON:

$fileContent | ConvertFrom-Json | ConvertTo-Json | %{[regex]::Unescape($_)}

输出结果(错误的)将会是:

{
    "something":  "http://domain/?x=1&y=2",
    "pattern":  "^(?!(\|\~|\!|\@|\#|\$|\||\\|\'|\")).*"
}

不进行Unescape处理

如果我读取这个内容,然后使用以下命令将其转换回JSON:

$fileContent | ConvertFrom-Json | ConvertTo-Json 

输出(错误的)结果将会是:

{
    "something":  "http://domain/?x=1\u0026y=2",
    "pattern":  "^(?!(\\|\\~|\\!|\\@|\\#|\\$|\\||\\\\|\\\u0027|\\\")).*"
}

期望结果

期望的结果应与输入文件内容相同。


1
我非常确定 Json cmdlets 没有通过往返测试,这就是你的问题所在。 - Maximilian Burszley
这是一个基本的期望,但它不能正确地转换回原始内容确实很奇怪。 - Reza Aghaei
可能是一个期望,但有一些 cmdlet 无法成功地进行往返测试(看着你,*-CliXml cmdlet)。 - Maximilian Burszley
3个回答

23

我决定不使用Unescape,而是将unicode \uxxxx字符替换为它们的字符串值,这样它就可以正常工作了:

$fileContent = 
@"
{
    "something":  "http://domain/?x=1&y=2",
    "pattern":  "^(?!(\\`|\\~|\\!|\\@|\\#|\\$|\\||\\\\|\\'|\\\")).*"
}
"@

$fileContent | ConvertFrom-Json | ConvertTo-Json | %{
    [Regex]::Replace($_, 
        "\\u(?<Value>[a-zA-Z0-9]{4})", {
            param($m) ([char]([int]::Parse($m.Groups['Value'].Value,
                [System.Globalization.NumberStyles]::HexNumber))).ToString() } )}

生成预期输出的内容:

{
    "something":  "http://domain/?x=1&y=\\2",
    "pattern":  "^(?!(\\|\\~|\\!|\\@|\\#|\\$|\\||\\\\|\\'|\\\")).*"
}

4
这个对于解决编辑 ARM(Azure 资源管理器)模板的问题非常有帮助。 - Stringfellow
2
这正是我寻找的,以便使PowerShell的JSON输出与我的Python解析器互操作...似乎微软确实让使用PowerShell处理JSON变得几乎不可能(默认情况下,ConvertTo-JSON编写带有BOM的UTF-8文件,在REST世界中也无法使用)...无论如何,非常感谢 :) - Orsiris de Jong
1
@OrsirisdeJong 确实,对于PowerShell来说,JSON转义似乎很麻烦。 - r3verse
2
@Ilyan,你的建议无法处理\u是字符串开头的情况。似乎这个正则表达式可以解决它:(?<![\])\u(?<Value>[a-zA-Z0-9]{4})。添加 **(?<![\])**。 - Maxim Ozerov
@MaximOzerov,ConvertTo-Json返回有效的JSON,它不能以\u开头。但是你的正则表达式还支持取消转义JSON子字符串,谢谢。 - Ilyan
显示剩余2条评论

3
注意:
- 不需要使用[regex] :: Unescape(),因为JSON的转义与正则表达式的转义无关。 - 也就是说,$fileContent | ConvertFrom-Json | ConvertTo-Json应该可以直接工作,但是由于Windows PowerShell中的一个怪异问题,导致输入字符串中的&被表示为其等效的转义序列,即\u0026,重新转换时产生问题;类似地,'\u0027),<\u003c)和>\u003e)也会受到影响。 简而言之: 问题不影响PowerShell(Core)6+(按需安装、跨平台PowerShell版本),它使用不同的实现ConvertTo-JsonConvertFrom-Json cmdlet,最新版本为PowerShell 7.2.x,基于Newtonsoft.JSON(在r3verse的答案中展示了其直接用法)。在那里,您的示例往返命令按预期工作。
只有Windows PowerShell中的ConvertTo-Json受到影响(附带了Windows的PowerShell版本,最新且最终版本为5.1)。但请注意,JSON表示法-虽然出乎意料-但在技术上是正确的
一个简单但强大的解决方案仅专注于取消转义那些ConvertTo-Json意外创建的Unicode转义序列-即& ' < >,同时排除错误的情况:
# The following sample JSON with undesired Unicode escape sequences for `& < > '`
# was created with Windows PowerShell's ConvertTo-Json as follows:
#   ConvertTo-Json "Ten o'clock at <night> & later. \u0027 \\u0027"
$json = '"Ten o\u0027clock at \u003cnight\u003e \u0026 later. \\u0027 \\\\u0027"'

[regex]::replace(
  $json, 
  '(?<=(?:^|[^\\])(?:\\\\)*)\\u(00(?:26|27|3c|3e))', 
  { param($match) [char] [int] ('0x' + $match.Groups[1].Value) },
  'IgnoreCase'
)

以上代码输出所需的JSON表示形式,而不需要对&'<>进行不必要的转义,并且不会错误地替换转义子字符串\\u0027\\\\u0027

"Ten o'clock at <night> & later. \\u0027 \\\\u0027"

背景信息:

Windows PowerShell中的ConvertTo-Json在JSON字符串中意外地用它们的Unicode转义序列来表示以下ASCII范围字符:

  • & (Unicode转义序列: \u0026)
  • ' (\u0027)
  • <> (\u003c\u003e)

没有必要这样做 (这些字符只需要在HTML/XML文本中进行转义)。

然而,任何符合规范的JSON解析器 - 包括 ConvertFrom-Json - 都会将这些转义序列转换回它们所代表的字符。

换句话说:虽然由Windows PowerShell的ConvertTo-Json创建的JSON文本是意外的并且可能影响可读性,但从技术上讲,它是正确的,并且 - 虽然不是完全相同 - 在表示数据方面与原始表示等效。


解决可读性问题:

顺带一提: 虽然[regex]::Unescape()的目的是只对正则表达式进行反转义,但它还将Unicode转义序列转换为它们所代表的字符,但它基本上不适用于有选择性地取消JSON字符串中的Unicode序列转义,因为所有其他\转义必须被保留以使JSON字符串保持语法上有效。

虽然你的答案在一般情况下都有效,但它有局限性(除了可以轻松纠正的问题之外,即a-zA-Z应该是a-fA-F,以限制匹配到那些有效的十六进制数字字母):

  • 它并不排除虚假的阳性,比如 \\u0027 或者 \\\\u0027\\ 代表转义符号 \, 所以 u0027 部分变成了原样字符串,不能被视为转义序列)。

  • 它会转换所有的 Unicode 转义序列,这会带来两个问题:

    • 需要转义的字符对应的转义序列也会被转换成原样的字符表示,这会破坏JSON表示,例如给定需要转义的字符 \, 由其生成的表示为\u005c,但实际上仍然需要继续进行转义。

    • 对于需要用一对Unicode转义序列表示的非BMP Unicode字符(所谓的代理项对),您的解决方案会错误地尝试单独地反转每个字符。

要想得到一个 健壮的解决方案 来克服这些限制,请参见此答案 (代理项序列保留为Unicode转义序列,需要转义的字符对应的Unicode转义序列会被转换为基于\的(C风格)转义字符,例如\n, 如果可能)。

然而,如果唯一要求是反转Windows PowerShell的ConvertTo-Json 意外创建的Unicode转义序列,则顶部的解决方案是足够的。


3
如果您不想依赖正则表达式(来自@Reza Aghaei的答案),您可以导入Newtonsoft JSON库。优点是默认StringEscapeHandling属性仅转义控制字符,避免了使用正则表达式进行潜在危险的字符串替换的问题。
StringEscapeHandling也是PowerShell Core(版本6及以上)的默认处理方式,因为他们从那时开始内部使用Newtonsoft。因此,另一种选择是使用PowerShell Core中的ConvertFrom-Json和ConvertTo-Json。
如果您导入Newtonsoft JSON库,则代码将类似于以下内容:
[Reflection.Assembly]::LoadFile("Newtonsoft.Json.dll")

$json = Get-Content -Raw -Path file.json -Encoding UTF8 # read file
$unescaped = [Newtonsoft.Json.Linq.JObject]::Parse($json) # similar to ConvertFrom-Json

$escapedElementValue = [Newtonsoft.Json.JsonConvert]::ToString($unescaped.apiName.Value) # similar to ConvertTo-Json
$escapedCompleteJson = [Newtonsoft.Json.JsonConvert]::SerializeObject($unescaped) # similar to ConvertTo-Json

Write-Output "Variable passed = $escapedElementValue"
Write-Output "Same JSON as Input = $escapedCompleteJson"

这个解决方案是可移植的吗?NewtonSoft 的 JSON 库每个 .Net 版本都有一个 DLL。根据目标操作系统,人们将不得不捆绑同一 DLL 的不同版本,是吗? - Orsiris de Jong
1
@OrsirisdeJong 大多数操作系统都支持4.5及以上版本,因此只需选择您需要的最低版本即可,因为.NET是向后兼容的。Newtonsoft甚至支持.NET 2.0!除非您需要针对Windows XP或更低版本的系统进行开发,否则我不会再寻找比4.5更低的版本了。 - r3verse
谢谢。最后一个问题,我的目标是NT6.1+,所以我可以使用.Net Framework 3.5。我针对32位和64位系统,但只找到了每个.net Framework版本的一个DLL版本,无论位数如何。这是一个在64位系统上加载的32位DLL,还是我错过了什么? - Orsiris de Jong
1
@OrsirisdeJong 没有明确说明,但我猜他们针对32位和64位系统。 (AnyCPU配置; 参见: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/platform-compiler-option) - r3verse
@RezaAghaei 感谢您的评论,我尝试重现错误,但是我在使用重音符(`)作为PowerShell转义字符时没有任何问题。我还更新了我的答案,包括两种情况,以输出元素值或完整的JSON。在这两种情况下,我都可以在Write-Output中得到重音符。 - r3verse
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接