第 4 章 与其他语言的结合

除 R 语言外, knitr 软件包在 R Markdown 中还支持许多其他的语言。在三个反引号后的花括号中的第一个单词表示语言名称。 例如, ```{r} 中的小 r 表示该代码块为 R 代码块,而 ```{python} 是指该代码块为 Python 代码块。本章我们会介绍一些你可能不熟悉的语言。

在 knitr 中,每种语言都通过语言引擎得到支持。语言引擎本质上是一些函数,它们以源代码和块的选项作为输入,最后输出一个字符串。并通过 knitr::knit_engines 进行管理。你可以使用以下方式检查现有引擎:

names(knitr::knit_engines$get())
##  [1] "awk"       "bash"      "coffee"    "gawk"      "groovy"    "haskell"  
##  [7] "lein"      "mysql"     "node"      "octave"    "perl"      "psql"     
## [13] "Rscript"   "ruby"      "sas"       "scala"     "sed"       "sh"       
## [19] "stata"     "zsh"       "highlight" "Rcpp"      "tikz"      "dot"      
## [25] "c"         "cc"        "fortran"   "fortran95" "asy"       "cat"      
## [31] "asis"      "stan"      "block"     "block2"    "js"        "css"      
## [37] "sql"       "go"        "python"    "julia"     "sass"      "scss"     
## [43] "R"         "bslib"

目前,大多数非r语言的代码块都是独立执行的。例如,同一文档中的所有 bash 代码块都在各自的会话中单独执行,因此后面的 bash 代码块不能使用在先前 bash 代码块中创建的变量,更改后的工作目录(通过 cd )不会跨不同的bash块持久存在。只有 R、Python 和 Julia 代码块在同一个会话中执行。请注意,所有的R代码块都在同一个R会话中执行,所有的Python代码块都在同一个Python会话中执行,等等。R会话和Python会话是两个不同的会话,但是可以从另一个会话访问或操作一个会话的对象(参见15.2节)。

R Markdown权威指南 (Xie, Allaire, and Grolemund 2018) 的2.7节 展示了如何在R Markdown中使用Python, Shell, SQL, Rcpp, Stan, JavaScript, CSS, Julia, C,和Fortran代码。在本章中,我们将展示更多的语言引擎,你可以在下面的库中找到更多的例子: https://github.com/yihui/knitr-examples (查找包含单词引擎的文件名)。

首先,让我们通过注册一个自定义语言引擎来揭示语言引擎是如何工作的。

4.1 注册自定义语言引擎(*)

你可以通过使用 knitr::knit engines$set() 方法注册一个自定义语言引擎。它接受一个函数作为输入,例如:

这样就已经注册了 foo 引擎,现在你可以使用以 ```{foo} 开头的代码块了。

这个引擎函数中有一个 options 参数,它是代码块的块选项列表。你可以在 options$code 中以字符向量的形式访问块的源代码。例如,对于代码块:

```{foo}
1 + 1
2 + 2
```

options的代码元素是一个字符向量 c('1 + 1', '2 + 2')

语言引擎实际上不必处理计算机语言,但可以处理代码块中的任何文本。首先,我们展示一个简单的引擎示例,该引擎将代码块的内容转换为大写:

knitr::knit_engines$set(upper = function(options) {
  code <- paste(options$code, collapse = '\n')
  if (options$eval) toupper(code) else code
})

关键是我们将 toupper 函数应用于代码,并以单个字符串的形式返回结果(通过\n连接所有代码行)。注意toupper()仅在chunk选项 eval = TRUE 时才应用,否则返回原始字符串。这向您展示了如何在引擎函数中使用 eval 之类的块选项。类似地,你可以考虑在函数体中添加 if (options$results == 'hide') return() 来隐藏chunk选项时的输出 results = 'hide' 。下面是一个使用 upper 引擎及其输出的示例块

```{upper}
Hello, **knitr** engines!
```

HELLO, KNITR ENGINES!

接下来,我们展示一个名为 py 的另一种 python 引擎的示例1。这个引擎是通过R函数 system2() 调用 python 命令来实现的:

knitr::knit_engines$set(py = function(options) {
  code <- paste(options$code, collapse = '\n')
  out  <- system2(
    'python', c('-c', shQuote(code)), stdout = TRUE
  )
  knitr::engine_output(options, code, out)
})

为了充分理解以上引擎的功能,你需要了解以下内容:

  1. 给定 Python 代码作为字符串(上述函数中的代码),我们可以通过命令行调用 python -c 'code' 执行代码。 那就是 system2() 所做的。 我们通过在 system2() 中指定 stdout = TRUE 来收集(文本)输出。

  2. 你可以将块选项、源代码和文本输出传递给函数 knitr::engine_output() 以生成最终输出。这个函数处理常见的块选项,比如 echo = FALSEresults = 'hide',所以你不需要注意这些情况。

knitr 中的许多语言引擎都是这样定义的(例如,使用 system2() 来执行与语言对应的命令)。如果你对技术细节感兴趣,你可以在这里的R源代码中查看大多数语言引擎的源代码 https://github.com/yihui/knitr/blob/master/R/engine.R

现在我们可以使用新的引擎 py,例如:

```{py}
print(1 + 1)
```
## 2

如果你觉得你的版本比现有的更好你甚至可以通过 knitr::knit_engines$set() 重写现有的语言引擎。但是,通常我们不建议你这样做,因为这可能会让熟悉现有引擎的用户感到惊讶,但我们无论如何都想让你意识到这种可能性。

4.2 运行 Python 代码并与 Python 交互

我们知道你喜欢 Python,所以让我们把它说清楚: R Markdown 和 knitr 确实支持 Python。

要将 Python 代码块添加到R Markdown文档中,可以在块的头部变为```{python},例如:

```{python}
print("Hello Python!")
```

你可以像往常一样在chunk头中添加chunk选项,比如 echo = FALSEeval = FALSE ,并且也支持使用Python中的 matplotlib 包绘图。

R Markdown 和 knitr 中的Python支持是 reticulate(Ushey, Allaire, and Tang 2020),这个包的一个重要特性是它允许Python和R之间的双向通信。例如,你可以在R会话中通过reticulate包中的py对象访问或创建Python变量:

```{r, setup}
library(reticulate)
```

Create a variable `x` in the Python session:

```{python}
x = [1, 2, 3]
```

Access the Python variable `x` in an R code chunk:

```{r}
py$x
```

Create a new variable `y` in the Python session using R,
and pass a data frame to `y`:

```{r}
py$y <- head(cars)
```

Print the variable `y` in Python:

```{python}
print(y)
```

有关 reticulate 的更多信息,您可以在下面查看它的文档 https://rstudio.github.io/reticulate/

4.3 通过 asis 引擎有条件地执行内容

正如其名, asis 引擎按原样写出块内容。使用此引擎的优点是,你可以有条件地包含一些内容,块内容的显示由块选项 echo 决定。当 echo = FALSE 时,数据块将被隐藏。下面是一个简单的例子:

```{r}
getRandomNumber <- function() {
  sample(1:6, 1)
}
```

```{asis, echo = getRandomNumber() == 4}
According to https://xkcd.com/221/, we just generated
a **true** random number!
```

只有当条件 getRandomNumber() == 4 (随机)为真时, asis 块中的文本才会显示。

4.4 执行 Shell 脚本

你可以根据你的喜好,使用 bashshzsh 任何一种引擎运行Shell脚本。下面是一个带有 chunk 头 ```{bash} 的 bash 示例:

ls *.Rmd | head -n 5
## 06-HTML-document.Rmd
## 07-PDF-document.Rmd
## 08-presentations.Rmd
## 09-Word-document.Rmd
## 10-other-output.Rmd

注意,bash是用 R 函数 system2() 调用的。它将忽略配置文件,例如 ~/.bash_profile~/.bash_login,其中可能定义了命令别名或修改过的环境变量(如 PATH 变量)。如果你想要这些配置文件像你使用终端时一样被执行,你可以通过引擎将参数 -l 传递给 bash。例如:

```{bash, engine.opts='-l'}
echo $PATH
```

如果你想对所有 bash 块全局启用 -l 参数,你可以在文档开头的global chunk选项中设置为:

knitr::opts_chunk$set(engine.opts = list(bash = '-l'))

还可以将其他参数作为字符向量提供给chunk选项engine.opts,从而传递给bash

4.5 用 D3 可视化

r2d3(R-r2d3?) (Luraschi和Allaire 2018)是 D3 可视化的接口。这个包可以用于 R Markdown 文档以及其他应用程序(如 Shiny)。要在 R Markdown 中使用它,您可以在代码块中调用它的函数 r2d3(),或者使用它的 d3 引擎。后者要求你理解 D3 库和 JavaScript,这超出了本书的范围,我们将把它们留给读者去学习。下面是一个使用 d3 引擎绘制柱状图的例子:

---
title: Generate a chart with D3
output: html_document
---

First, load the package **r2d3** to set up the `d3` engine
for **knitr** automatically:

```{r setup}
library(r2d3)
```

Now we can generate data in R, pass it to D3, and draw
the chart:

```{d3, data=runif(30), options=list(color='steelblue')}
svg.selectAll('rect')
  .data(data)
  .enter()
    .append('rect')
      .attr('width', function(d) { return d * 672; })
      .attr('height', '10px')
      .attr('y', function(d, i) { return i * 16; })
      .attr('fill', options.color);
```

4.6 通过 cat 引擎将块内容写入文件

有时将代码块的内容写入外部文件,然后在其他代码块中使用此文件可能会很有用。当然,您可以通过 writeLines() 等 R 函数来实现这一点,但问题是,当内容相对较长或包含特殊字符时,传递给 writeLines() 的字符串可能看起来很笨拙。下面是将长字符串写入文件 my-file.txt 的示例:

writeLines("This is a long character string.
It has multiple lines. Remember to escape
double quotes \"\", but 'single quotes' are OK.
I hope you not to lose your sanity when thinking
about how many backslashes you need, e.g., is it
'\t' or '\\t' or '\\\\t'?",
con = "my-file.txt")

自R 4.0.0以来,这个问题已经大大缓解了,因为R开始支持 r"( )" 中的原始字符串(参见帮助页面 ?Quotes),而且你不需要记住所有关于特殊字符的规则。即使使用原始字符串,在代码块中显式地将长字符串写入文件仍然会让读者分心。

knitr 中的 cat 引擎为你提供了一种在代码块中呈现文本内容或将其写入外部文件的方法,而无需考虑有关R字符串的所有规则(例如,当需要字面上的反斜杠时,你需要双反斜杠)。

要将块内容写入文件,请在块选项 engine.opts 中指定文件路径,例如 engine.opts = list(file = 'path/to/file')。在引擎盖下,在 engine.opts 中指定的值列表将传递给该函数

在引擎盖之下,engine.opts 中指定的值列表将传递给函数base::cat() 并且 filebase::cat() 的参数之一。

接下来,我们将提供三个示例来说明 cat 引擎的用法。

4.6.1 写入 CSS 文件

如7.3节所示,你可以在 Rmd 文档中嵌入一个 css 代码块,以使用 CSS 样式化元素。另一种方法是通过一些 R Markdown 输出格式(如,html_document)的 CSS 选项为 Pandoc 提供一个定制的 CSS 文件。 cat 引擎可以用来从 Rmd 编写这个 CSS 文件。

下面的例子展示了如何从文档中的块生成 custom.css 文件,并将文件路径传递给 html_document 格式的 css 选项。

---
title: "Create a CSS file from a code chunk"
output: 
  html_document:
    css: custom.css
---

The chunk below will be written to `custom.css`, which
will be used during the Pandoc conversion.

```{cat, engine.opts = list(file = "custom.css")}
h2 {
  color: blue;
}
```

## And this title will blue

css 代码块方法与此方法之间的唯一区别是,前一种方法将 CSS 代码写在输出文档的 <body> 标记内的位置(即,在代码块的位置),并且将 CSS 代码写在输出文档的 <body> 标记内。 后一种方法将 CSS 写入输出文档的 <body> 区域。输出文档中不会有任何实际的视觉差异。

4.6.2 在序言中包含 LaTeX 代码

在6.1节中,我们介绍了如何将 LaTeX 代码添加到序言中,这需要一个外部 .tex 文件。也可以从 Rmd 生成此文件,这是一个示例:

---
title: "Create a .tex file from a chunk"
author: "Jane Doe"
classoption: twoside
output: 
  pdf_document:
    includes:
      in_header: preamble.tex
---

# How it works

Write a code chunk to a file `preamble.tex` to define
the header and footer of the PDF output document:

```{cat, engine.opts=list(file = 'preamble.tex')}
\usepackage{fancyhdr}
\usepackage{lipsum}
\pagestyle{fancy}
\fancyhead[CO,CE]{This is fancy header}
\fancyfoot[CO,CE]{And this is a fancy footer}
\fancyfoot[LE,RO]{\thepage}
\fancypagestyle{plain}{\pagestyle{fancy}}
```

\lipsum[1-15]

# More random content

\lipsum[16-30]

在上面的 cat 代码块中的 LaTeX 代码中,我们定义了 PDF 文档的页眉和页脚。如果我们还想在页脚中显示作者姓名,我们可以用选项engine.opts = list(file = 'preamble.tex', append = TRUE)code = sprintf('\\fancyfoot[LO,RE]{%s}', rmarkdown::metadata$author)将作者信息附加到另一个 cat 代码块中的 preamble.tex 中。要理解这是如何工作的,请回忆一下我们在本节前面提到的:engine.opts 被传递给 base::cat()。(因此 append = TRUE 被传递给 cat()),你可以通过阅读16.2节来理解chunk选项代码。

4.6.3 将 YAML 数据写入文件并显示它

默认情况下,cat 代码块的内容不会显示在输出文档中。如果还想在写出它之后显示它,则将块选项 class.source 设置为语言名称。语言名称用于语法高亮显示。在下面的例子中,我们指定为 yaml 语言:

```{cat, engine.opts=list(file='demo.yml'), class.source='yaml'}
a:
  aa: "something"
  bb: 1
b:
  aa: "something else"
  bb: 2
```

其输出显示在下面,并且还生成了一个文件 demo.yml

a:
  aa: "something"
  bb: 1
b:
  aa: "something else"
  bb: 2

为了显示文件 demo.yml确实已经生成,我们可以尝试使用 yaml(Stephens et al. 2020)将其读入 R。

xfun::tree(yaml::read_yaml('demo.yml'))
## List of 2
##  |-a:List of 2
##  |  |-aa: chr "something"
##  |  |-bb: int 1
##  |-b:List of 2
##     |-aa: chr "something else"
##     |-bb: int 2

4.7 运行 SAS 代码

你可以使用 sas 引擎运行 SAS (https://www.sas.com) 代码。你需要确保 SAS 可执行文件在你的环境变量 PATH 中,或者(如果你不知道 PATH 是什么意思)通过 chunk 选项 engine.path 来提供 SAS 可执行文件的完整路径,例如:engine.path = "C:\\Program Files\\SASHome\\x86\\SASFoundation\\9.3\\sas.exe"。下面是一个输出 “Hello World” 的示例:

```{sas}
data _null_;
put 'Hello, world!';
run;
```

4.8 运行 Stata 代码

如果你安装了Stata,你可以通过 stata 引擎 Stata (https://www.stata.com) 来运行 Stata (https://www.stata.com) 代码。除非可以通过环境变量 PATH 找到 stata 可执行文件,否则你需要通过 chunk 选项 engine.path 指定到可执行文件的完整路径,例如:engine.path = "C:/Program Files (x86)/Stata15/StataSE-64.exe"。下面给出一个例子:

```{stata}
sysuse auto
summarize
```

knitr 中的 stata 引擎是相当有限的。Doug Hemken已经通过Statamarkdown包 中对其进行了实质性的扩展,该包可以通过Github中获得,地址为: https://github.com/Hemken/Statamarkdown。通过在线搜索 “Stata R Markdown”,你可以找到关于这个包的教程。

4.9 用渐近线 Asymptote 创建图形

渐近线 Asymptote (https://asymptote.sourceforge.io) 是矢量图形的强大语言。如果您你已经安装了 Asymptote,你可以使用 asy 引擎在 R Markdown 中编写并运行 Asymptote 代码(有关安装的说明,请参阅其网站)。下面是从仓库 https://github.com/vectorgraphics/asymptote 中复制的示例,其输出如图 ?? 所示:

import graph3;
import grid3;
import palette;
settings.prc = false;

currentprojection=orthographic(0.8,1,2);
size(500,400,IgnoreAspect);

real f(pair z) {return cos(2*pi*z.x)*sin(2*pi*z.y);}

surface s=surface(f,(-1/2,-1/2),(1/2,1/2),50,Spline);

surface S=planeproject(unitsquare3)*s;
S.colors(palette(s.map(zpart),Rainbow()));
draw(S,nolight);
draw(s,lightgray+opacity(0.7));

grid3(XYZgrid);

注意,对于PDF输出,可能需要一些额外的 LaTeX 包,否则可能会出现如下错误:

! LaTeX Error: File `ocgbase.sty' not found.

如果出现这种错误,请参见 1.3 章节了解如何安装丢失的 LaTeX 包。

在上面的 asy 块中,我们使用了 settings.prc = false 的设置。如果没有此设置,当输出格式为 PDF 时,渐近线将生成交互式 3D 图形。但是,交互图形只能在 Acrobat Reader 中查看。如果使用 Acrobat Reader,则可以与图形交互。例如,你可以用鼠标旋转图??中的 3D 表面。

4.9.1 在R中生成数据并通过 Asymptote 读取

现在我们展示一个示例,其中我们首先将在 R 中生成的数据保存到 CSV 文件中(下面是一个 R 代码块):

x = seq(0, 5, l = 100)
y = sin(x)
writeLines(paste(x, y, sep = ','), 'sine.csv')

然后通过 Asymptote 读取,并根据图 ?? 所示的数据绘制图表(下面是一个 asy 的代码块):

import graph;
size(400,300,IgnoreAspect);
settings.prc = false;

// import data from csv file
file in=input("sine.csv").line().csv();
real[][] a=in.dimension(0,0);
a=transpose(a);

// generate a path
path rpath = graph(a[0],a[1]);
path lpath = (1,0)--(5,1);

// find intersection
pair pA=intersectionpoint(rpath,lpath);

// draw all
draw(rpath,red);
draw(lpath,dashed + blue);
dot("$\delta$",pA,NE);
xaxis("$x$",BottomTop,LeftTicks);
yaxis("$y$",LeftRight,RightTicks);

4.10 使用 Sass/SCSS 构建 HTML 页面风格

Sass (https://sass-lang.com) 是一种 CSS 扩展语言,它允许你以比普通 CSS 更灵活的方式创建 CSS 规则。如果你有兴趣学习它,请查看它的官方文档。

sass (Cheng et al. 2021) (Cheng et al. 2020)包可以用来编译 Sass 到 CSS。基于 sass 包,knitr 包含两个语言引擎: sassscss (分别对应于Sass和SCSS语法),将代码块编译为CSS。下面是一个 scss 代码块,头部块为```{scss}

$font-stack: "Comic Sans MS", cursive, sans-serif;
$primary-color: #00FF00;

.book.font-family-1 {
  font: 100% $font-stack;
  color: $primary-color;
}

你还可以使用 sass 引擎,并且 Sass 语法与 SCSS 语法略有不同,例如:

```{sass}
$font-stack: "Comic Sans MS", cursive, sans-serif
$primary-color: #00FF00

.book.font-family-1
  font: 100% $font-stack
  color: $primary-color
```

如果你正在阅读本节的HTML版本,你会注意到该页的字体已被更改为 Comic Sans,这可能会令人吃惊,但请不要恐慌,你并没有中风)。

sass/scss 代码块是通过sass::sass()函数编译而成的。目前,你可以通过chunk选项 engine.opts 定制 CSS 代码的输出样式,例如:engine.opts = list(style = "expanded")。默认的样式是 “compressed”。如果你不确定这意味着什么,请参阅帮助页面 ?sass::sass_options并寻找 output_style 对应的参数。


  1. 实际上,你应该改用内置的python引擎,该引擎基于 reticulate 软件包,并且更好地支持Python代码块(见15.2节)↩︎