C 的简单 JSON 字符串转义?

新手上路,请多包涵

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

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

原文由 ddinchev 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.5k
2 个回答

警告

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

All control characters means everything from '\x00' to '\x1f' , not just those with a short representation such as '\x0a' (also known as '\n' ) .例如,您 必须'\x02' 字符转义为 \u0002

另请参阅: ECMA-404 - JSON 数据交换语法,第 2 版,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();
}

最短表示

对于最短的表示,您可以使用 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语句

也可以用纯 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

原文由 vog 发布,翻译遵循 CC BY-SA 4.0 许可协议

以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(/case\s+'(.*?)':\s+o\s+<<\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>

原文由 milahu 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题