要在json中编码字符串,需要使用反斜杠对几个保留字符进行转义,并且每个字符串都必须用双引号引起来。当前jsonlite
包使用基本R中的deparse
函数来实现此功能:
deparse_vector <- function(x) {
stopifnot(is.character(x))
vapply(x, deparse, character(1), USE.NAMES=FALSE)
}
这可以解决问题:
test <- c("line\nline", "foo\\bar", "I said: \"hi!\"")
cat(deparse_vector(test))
然而,对于大向量,deparse
变慢。另一种实现方式是分别gsub
每个字符:
deparse_vector2 <- function(x) {
stopifnot(is.character(x))
if(!length(x)) return(x)
x <- gsub("\\", "\\\\", x, fixed=TRUE)
x <- gsub("\"", "\\\"", x, fixed=TRUE)
x <- gsub("\n", "\\n", x, fixed=TRUE)
x <- gsub("\r", "\\r", x, fixed=TRUE)
x <- gsub("\t", "\\t", x, fixed=TRUE)
x <- gsub("\b", "\\b", x, fixed=TRUE)
x <- gsub("\f", "\\f", x, fixed=TRUE)
paste0("\"", x, "\"")
}
这会快一点,但也不会太多,也很难看。有什么更好的方法可以做到这一点? (最好没有其他依赖性)
此script可用于比较实现:
> system.time(out1 <- deparse_vector1(strings))
user system elapsed
6.517 0.000 6.523
> system.time(out2 <- deparse_vector2(strings))
user system elapsed
1.194 0.000 1.194
我不知道用R代码来完成此操作的更快方法,但是我确实决定尝试用C语言实现,并封装在名为deparse_vector3
的R函数中。这很粗糙(而且我离C语言专家还很远),但它似乎适用于您的示例:https://gist.github.com/wch/e3ec5b20eb712f1b22b2
在我的系统(Mac,R 3.1.1)上,deparse_vector2
比deparse_vector
快20倍,这比测试中得到的5倍大得多。
我的deparse_vector3
功能仅比deparse_vector2
快3倍。可能还有改进的空间。
> system.time(out1 <- deparse_vector1(strings))
user system elapsed
8.459 0.009 8.470
> system.time(out2 <- deparse_vector2(strings))
user system elapsed
0.368 0.007 0.374
> system.time(out3 <- deparse_vector3(strings))
user system elapsed
0.120 0.001 0.120
不过,我认为这不会正确处理非ASCII字符编码。这是R源中如何处理编码的示例:https://github.com/wch/r-source/blob/bfe73ecd848198cb9b68427cec7e70c40f96bd72/src/main/grep.c#L588-L630
编辑:这似乎可以处理UTF-8,虽然我可能在测试中遗漏了某些东西。
这里是Winston代码的C ++版本。这要简单得多,因为您可以有效地增长std::string
s。由于Rcpp会为您进行内存管理,因此崩溃的可能性也较小。
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
std::string escape_one(std::string x) {
std::string out = "\"";
int n = x.size();
for (int i = 0; i < n; ++i) {
char cur = x[i];
switch(cur) {
case '\\': out += "\\\\"; break;
case '"': out += "\\\""; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
case '\b': out += "\\b"; break;
case '\f': out += "\\f"; break;
default: out += cur;
}
}
out += '"';
return out;
}
// [[Rcpp::export]]
CharacterVector escape_chars(CharacterVector x) {
int n = x.size();
CharacterVector out(n);
for (int i = 0; i < n; ++i) {
String cur = x[i];
out[i] = escape_one(cur);
}
return out;
}
根据您的基准,deparse_vector2(strings)
花费0.8s,escape_chars(strings)
花费0.165s。
[您也可以尝试使用stri_escape_unicode
软件包中的stringi
(尽管您更喜欢没有附加依赖项的解决方案,但我认为它对将来的读者也很有用),它比deparse_vector2
快3倍,并且快7倍比deparse_vector
require(stringi)
定义功能
deparse_vector3 <- function(x){
paste0("\"",stri_escape_unicode(x), "\"")
}
检查所有功能是否均得到smae结果
all.equal(deparse_vector2(test), deparse_vector3(test))
## [1] TRUE
all.equal(deparse_vector(test), deparse_vector3(test))
## [1] TRUE
某些基准
library(microbenchmark)
microbenchmark(deparse_vector(test),
deparse_vector2(test),
deparse_vector3(test), times = 1000L)
# Unit: microseconds
# expr min lq median uq max neval
# deparse_vector(test) 98.548 102.654 104.707 111.380 2500.653 1000
# deparse_vector2(test) 43.114 46.707 48.761 51.327 401.377 1000
# deparse_vector3(test) 14.885 16.938 18.991 20.018 240.211 1000 <-- Clear winner
利用几个事实对这个问题再做一次调查。
给出长度为x
的字符串n
,我们知道输出字符串的长度至少为x
,最大为2 * x
。我们可以利用这一点来确保仅分配一次内存,而不依赖于增长的容器(尽管有效)。
[请注意,我在这里使用C ++ 11的shared_ptr
,因为我在使用原始内存做难看的事情(并希望确保自动清除它)。这也使我避免了尝试计算匹配数的初始阶段,但也迫使我过度分配了过多的位(每个字符都必须转义的情况很少见。)
我认为,将其适应于纯C解决方案相对容易,但要确保正确清理内存将比较棘手。
#include <memory>
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
void escape_one_fill(CharacterVector const& x, int i, CharacterVector& output) {
auto xi = CHAR(STRING_ELT(x, i));
int n = strlen(xi);
// Over-allocate memory -- we know that in the worst case the output
// string is 2x the length of x (plus 1 for \0)
auto out = std::make_shared<char*>(new char[n * 2 + 1]);
int counter = 0;
(*out)[counter++] = '"';
#define HANDLE_CASE(X, Y) \
case X: \
(*out)[counter++] = '\\'; \
(*out)[counter++] = Y; \
break;
for (int j = 0; j < n; ++j) {
switch (xi[j]) {
HANDLE_CASE('\\', '\\');
HANDLE_CASE('"', '"');
HANDLE_CASE('\n', 'n');
HANDLE_CASE('\r', 'r');
HANDLE_CASE('\t', 't');
HANDLE_CASE('\b', 'b');
HANDLE_CASE('\f', 'f');
default: (*out)[counter++] = xi[j];
}
}
(*out)[counter++] = '"';
// Set a NUL so that Rf_mkChar does what it should
(*out)[counter++] = '\0';
SET_STRING_ELT(output, i, Rf_mkChar(*out));
}
// [[Rcpp::export]]
CharacterVector escape_chars_with_fill(CharacterVector x) {
int n = x.size();
CharacterVector out(n);
for (int i = 0; i < n; ++i) {
escape_one_fill(x, i, out);
}
return out;
}
以此为基准,我得到了(仅与Hadley的展示次数进行比较:]
> mychars <- c(letters, " ", '"', "\\", "\t", "\n", "\r", "'", "/", "#", "$");
> createstring <- function(length){
+ paste(mychars[ceiling(runif(length, 0, length(mychars)))], collapse="")
+ }
> strings <- vapply(rep(1000, 10000), createstring, character(1), USE.NAMES=FALSE)
> system.time(escape_chars(strings))
user system elapsed
0.14 0.00 0.14
> system.time(escape_chars_with_fill(strings))
user system elapsed
0.080 0.001 0.081
> identical(escape_chars(strings), escape_chars_with_fill(strings))
[1] TRUE