c++的简单JSON字符串转义

Simple JSON string escape for C++?

本文关键字:字符串 转义 JSON 简单 c++      更新时间:2023-10-16

我有一个非常简单的程序,输出简单的JSON字符串,我手动连接在一起,并通过std::cout流输出(输出真的很简单),但我有字符串,可以包含双引号,花括号和其他字符,可以打破JSON字符串。因此,我需要一个库(或者更准确地说是一个函数)来根据JSON标准转义字符串,尽可能轻量级,不多也不少。

我发现了一些用于将整个对象编码为JSON的库,但考虑到我的程序是900行cpp文件,我宁愿不依赖于一个比我的程序大几倍的库,只是为了实现像这样简单的东西。

注意事项

无论您采用何种解决方案,请记住JSON标准要求您转义所有控制字符。这似乎是一个普遍的误解。很多开发人员都犯了这个错误。

所有控制字符表示从'x00''x1f'的所有字符,而不仅仅是那些具有短表示的字符,如'x0a'(也称为'n')。例如,必须将中的'x02'字符转义为u0002

参见:ECMA-404 - JSON数据交换语法,第二版,2017年12月,第4页

简单的解决方案

如果你确定你的输入字符串是UTF-8编码的,你可以保持事情简单。

由于JSON允许您通过uXXXX,甚至"转义所有内容,一个简单的解决方案是:

#include <sstream>
#include <iomanip>
std::string escape_json(const std::string &s) {
    std::ostringstream o;
    for (auto c = s.cbegin(); c != s.cend(); c++) {
        if (*c == '"' || *c == '' || ('x00' <= *c && *c <= 'x1f')) {
            o << "\u"
              << std::hex << std::setw(4) << std::setfill('0') << static_cast<int>(*c);
        } else {
            o << *c;
        }
    }
    return o.str();
}
<<p> 最短表示/strong> 对于最短的表示,您可以使用JSON快捷方式,例如"而不是u0022。以下函数生成UTF-8编码字符串s的最短JSON表示:
#include <sstream>
#include <iomanip>
std::string escape_json(const std::string &s) {
    std::ostringstream o;
    for (auto c = s.cbegin(); c != s.cend(); c++) {
        switch (*c) {
        case '"': o << "\""; break;
        case '': o << "\\"; break;
        case 'b': o << "\b"; break;
        case 'f': o << "\f"; break;
        case 'n': o << "\n"; break;
        case 'r': o << "\r"; break;
        case 't': o << "\t"; break;
        default:
            if ('x00' <= *c && *c <= 'x1f') {
                o << "\u"
                  << std::hex << std::setw(4) << std::setfill('0') << static_cast<int>(*c);
            } else {
                o << *c;
            }
        }
    }
    return o.str();
}

纯切换语句

也可以使用纯switch语句,即不使用if<iomanip>。虽然这很麻烦,但从"简单和纯粹的安全性"的角度来看,这可能是可取的。观点:

#include <sstream>
std::string escape_json(const std::string &s) {
    std::ostringstream o;
    for (auto c = s.cbegin(); c != s.cend(); c++) {
        switch (*c) {
        case 'x00': o << "\u0000"; break;
        case 'x01': o << "\u0001"; break;
        ...
        case 'x0a': o << "\n"; break;
        ...
        case 'x1f': o << "\u001f"; break;
        case 'x22': o << "\""; break;
        case 'x5c': o << "\\"; break;
        default: o << *c;
        }
    }
    return o.str();
}
使用库

您可能想看看https://github.com/nlohmann/json,这是一个高效的仅头文件的c++库(MIT许可),似乎经过了很好的测试。

你可以直接调用他们的escape_string()方法(注意,这有点棘手,参见下面Lukas Salich的评论),或者你可以把他们的escape_string()实现作为你自己实现的起点:

https://github.com/nlohmann/json/blob/ec7a1d834773f9fee90d8ae908a0c9933c5646fc/src/json.hpp L4604-L4697

我写了一个简单的JSON转义和非转义函数。代码在GitHub上是公开的。对于任何感兴趣的人,这里是代码:

enum State {ESCAPED, UNESCAPED};
std::string escapeJSON(const std::string& input)
{
    std::string output;
    output.reserve(input.length());
    for (std::string::size_type i = 0; i < input.length(); ++i)
    {
        switch (input[i]) {
            case '"':
                output += "\"";
                break;
            case '/':
                output += "\/";
                break;
            case 'b':
                output += "\b";
                break;
            case 'f':
                output += "\f";
                break;
            case 'n':
                output += "\n";
                break;
            case 'r':
                output += "\r";
                break;
            case 't':
                output += "\t";
                break;
            case '':
                output += "\\";
                break;
            default:
                output += input[i];
                break;
        }
    }
    return output;
}
std::string unescapeJSON(const std::string& input)
{
    State s = UNESCAPED;
    std::string output;
    output.reserve(input.length());
    for (std::string::size_type i = 0; i < input.length(); ++i)
    {
        switch(s)
        {
            case ESCAPED:
                {
                    switch(input[i])
                    {
                        case '"':
                            output += '"';
                            break;
                        case '/':
                            output += '/';
                            break;
                        case 'b':
                            output += 'b';
                            break;
                        case 'f':
                            output += 'f';
                            break;
                        case 'n':
                            output += 'n';
                            break;
                        case 'r':
                            output += 'r';
                            break;
                        case 't':
                            output += 't';
                            break;
                        case '':
                            output += '';
                            break;
                        default:
                            output += input[i];
                            break;
                    }
                    s = UNESCAPED;
                    break;
                }
            case UNESCAPED:
                {
                    switch(input[i])
                    {
                        case '':
                            s = ESCAPED;
                            break;
                        default:
                            output += input[i];
                            break;
                    }
                }
        }
    }
    return output;
}

以vog的回答为基础:

为字符0到92 = null到反斜杠生成一个完整的跳转表

// generate full jump table for c++ json string escape
// license is public domain or CC0-1.0
//var s = require('fs').readFileSync('case-list.txt', 'utf8');
var s = ` // escape hell...
        case '"': o << "\\\""; break;
        case '': o << "\\\\"; break;
        case '\b': o << "\\b"; break;
        case '\f': o << "\\f"; break;
        case '\n': o << "\\n"; break;
        case '\r': o << "\\r"; break;
        case '\t': o << "\\t"; break;
`;
const charMap = new Map();
s.replace(/cases+'(.*?)':s+os+<<s+"(.*?)";s+break;/g, (...args) => {
  const [, charEsc, replaceEsc ] = args;
  const char = eval(`'${charEsc}'`);
  const replace = eval(`'${replaceEsc}'`);
  //console.dir({ char, replace, });
  charMap.set(char, replace);
});
iMax = Math.max(
  0x1f, // 31. 0 to 31: control characters
  '""'.charCodeAt(0), // 34
  ''.charCodeAt(0), // 92
);
const replace_function_name = 'String_showAsJson';
const replace_array_name = replace_function_name + '_replace_array';
// longest replace (u0000) has 6 chars + 1 null byte = 7 byte
var res = `
// ${iMax + 1} * 7 = ${(iMax + 1) * 7} byte / 4096 page = ${Math.round((iMax + 1) * 7 / 4096 * 100)}%
char ${replace_array_name}[${iMax + 1}][7] = {`;
res += 'n  ';
let i, lastEven;
for (i = 0; i <= iMax; i++) {
  const char = String.fromCharCode(i);
  const replace = charMap.has(char) ? charMap.get(char) :
    (i <= 0x1f) ? '\u' + i.toString(16).padStart(4, 0) :
    char // no replace
  ;
  const hex = '0x' + i.toString(16).padStart(2, 0);
  //res += `case ${hex}: o << ${JSON.stringify(replace)}; break; /`+`/ ${i}n`;
  //if (i > 0) res += ',';
  //res += `n  ${JSON.stringify(replace)}, // ${i}`;
  if (i > 0 && i % 5 == 0) {
    res += `// ${i - 5} - ${i - 1}n  `;
    lastEven = i;
  }
  res += `${JSON.stringify(replace)}, `;
}
res += `// ${lastEven} - ${i - 1}`;
res += `n};
void ${replace_function_name}(std::ostream & o, const std::string & s) {
  for (auto c = s.cbegin(); c != s.cend(); c++) {
    if ((std::uint8_t) *c <= ${iMax})
      o << ${replace_array_name}[(std::uint8_t) *c];
    else
      o << *c;
  }
}
`;
//console.log(res);
document.querySelector('#res').innerHTML = res;
<pre id="res"></pre>

您没有确切地说明您拼凑在一起的字符串最初来自的位置,因此这可能没有任何用处。但是,如果它们碰巧都存在于代码中,就像@isnullxbh在回答另一个问题的评论中提到的那样,另一个选择是利用一个可爱的c++ 11特性:原始字符串字面量。

我不会引用cppreference冗长的、基于标准的解释,你可以自己在那里阅读。不过,基本上,r- string给c++带来了与shell中的here-docs中所提供的完全没有限制的相同类型的程序员分隔的文字,并且Perl等语言非常有效地使用了这些文字。(使用花括号的前缀引用可能是Perl最伟大的发明:)

my qstring = q{Quoted 'string'!};
my qqstring = qq{Double "quoted" 'string'!};
my replacedstring = q{Regexps that /totally/! get eaten by your parser.};
replacedstring =~ s{/totally/!}{(won't!)}; 
# Heh. I see the syntax highlighter isn't quite up to the challege, though.

在c++ 11或更高版本中,原始字符串字面值在双引号前加上大写R,在引号内,字符串前面有一个自由格式的分隔符(一个或多个字符),后跟一个开括号。

从那里开始,您可以安全地按字面意思编写任何内容,除了结束父括号后面跟着您选择的分隔符。该序列(后跟一个结束的双引号)结束了原始文本,然后您就有了一个std::string,您可以放心地相信它将不受任何解析或字符串处理的干扰。

"原始"属性在随后的操作中也不会丢失。所以,借用Crockford的How JavaScript Works的章节列表,这是完全有效的:

std::string ch0_to_4 = R"json(
[
    {"number": 0, "chapter": "Read Me First!"},
    {"number": 1, "chapter": "How Names Work"},
    {"number": 2, "chapter": "How Numbers Work"},
    {"number": 3, "chapter": "How Big Integers Work"},
    {"number": 4, "chapter": "How Big Floating Point Works"},)json";
std::string ch5_and_6 = R"json(
    {"number": 5, "chapter": "How Big Rationals Work"},
    {"number": 6, "chapter": "How Booleans Work"})json";
std::string chapters = ch0_to_4 + ch5_and_6 + "n]";
std::cout << chapters;

字符串'chapters'将完全完整地从std::cout中出现:

[
    {"number": 0, "chapter": "Read Me First!"},
    {"number": 1, "chapter": "How Names Work"},
    {"number": 2, "chapter": "How Numbers Work"},
    {"number": 3, "chapter": "How Big Integers Work"},
    {"number": 4, "chapter": "How Big Floating Point Works"},
    {"number": 5, "chapter": "How Big Rationals Work"},
    {"number": 6, "chapter": "How Booleans Work"}
]