c++的简单JSON字符串转义
Simple JSON string escape for C++?
我有一个非常简单的程序,输出简单的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"}
]
- 如何用转义符替换字符串中的所有特殊字符
- 如何在C++中将 Python 字符串转换为其转义版本?
- 如何解释C++字符串中的 \u 转义码?
- PostgresSQL - SQL Ready 语句与字符串转义,防止 SQL 注入攻击
- 是否可以在原始字符串文本中插入转义序列?
- 转义std::字符串中的特殊字符
- 使变量字符串忽略转义序列
- 在字符串流的 SSH 命令中转义 bash 脚本中的引号
- 转义 R "()" 在 C++ 中的原始字符串中
- 将转义的 UTF-8 八位字节的字符数组转换为 C++ 的字符串
- 使用 RE2::全局替换的转义字符串
- c++ JsonCpp 解析带有转义引号作为数组的字符串
- 如何检测字符串中的转义
- 将带有转义字符的大字符串转换为字节数组
- 由转义字符组成的字符串文字的大小
- 没有数据库连接的字符串转义
- 以编程方式获取 C++ 中的转义字符串表示形式
- C++转义短语子字符串
- c++的简单JSON字符串转义
- c++编译器什么时候开始考虑在字符串转义中使用两个以上的十六进制数字?