Shiny从入门到入定——9-上传和下载
发表于:2024-04-27 | 分类: IT
字数统计: 3.5k | 阅读时长: 14分钟 | 阅读量:

9 上传和下载

在应用中,用户处上传和下载文件是一项常用功能。你可以用它来上传数据进行分析,或者将结果作为数据集或报告下载。本章将介绍在应用中进行文件上传和下载所需的用户界面和服务器组件。

1
library(shiny)

9.1 上传

我们先从文件上传开始讨论,向你展示基本的UI和server组件,然后展示它们如何在简单应用中协同工作。

9.1.1 用户界面 UI

支持文件上传的用户界面很简单:只需在用户界面中添加fileInput()即可。

1
2
3
ui <- fluidPage(
fileInput("upload", "Upload a file")
)

与其他大多数UI组件一样,这里只需要两个必需参数:idlabelwidthbuttonLabelplaceholder参数允许你以其他方式调整外观。我不会在这里讨论它们,但你可以在?fileInput中了解更多。

9.1.2 服务器 server

在server上处理fileInput()比处理其他输入稍微复杂一些。大多数输入返回简单的向量,但fileInput()返回一个包含四列的数据框:

  • name:用户计算机上的原始文件名。

  • size:文件大小(以字节为单位)。默认情况下,用户只能上传最多5MB的文件。你可以在启动Shiny之前设置shiny.maxRequestSize选项来增加文件大小限制。例如,要允许最多10MB,可以运行options(shiny.maxRequestSize = 10 * 1024^2)

  • type:文件的“MIME type”。这是文件类型的正式规范,通常根据文件扩展名得出,在Shiny应用中很少需要。

  • datapath:数据在server上上传到的路径。请将此路径视为临时路径:如果用户上传更多文件,此文件可能会被删除。数据始终保存到临时目录,并赋予临时名称。

作者认为理解这种数据结构最简单的方法是制作一个简单的应用。运行以下代码并上传几个文件,以了解Shiny提供的数据。在图9.1中,你可以看到我上传了几张小狗照片(来自第7.3节)后的结果。

1
2
3
4
5
6
7
ui <- fluidPage(
fileInput("upload", NULL, buttonLabel = "Upload...", multiple = TRUE),
tableOutput("files")
)
server <- function(input, output, session) {
output$files <- renderTable(input$upload)
}

图9.1 这个简单的应用可以让你看到Shiny为上传的文件提供了哪些数据。可以在 https://hadley.shinyapps.io/ms-upload 查看实时效果。

请注意,我使用了labelbuttonLabel参数来微调外观,以及multiple = TRUE来允许用户上传多个文件。

9.1.3 上传数据

如果用户正在上传数据集,你需要注意以下两个细节:

  • input$upload在页面加载时被初始化为NULL,因此你需要使用req(input$upload)来确保你的代码等待第一个文件上传。

  • accept参数允许你限制可能的输入。最简单的方法是提供一个文件扩展名的字符向量,如accept = ".csv"。但是,accept参数只是对浏览器的建议,并不总是强制执行,因此自行验证是个好习惯(例如第8.1节)。在R中获取文件扩展名最简单的方法是使用tools::file_ext(),但要注意它会删除扩展名前面的.

将这些想法结合起来,我们得到以下应用,你可以上传一个.csv.tsv文件,并查看前n行。你可以在 https://hadley.shinyapps.io/ms-upload-validate 查看它的运行情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ui <- fluidPage(
fileInput("upload", NULL, accept = c(".csv", ".tsv")),
numericInput("n", "Rows", value = 5, min = 1, step = 1),
tableOutput("head")
)

server <- function(input, output, session) {
data <- reactive({
req(input$upload)

ext <- tools::file_ext(input$upload$name)
switch(ext,
csv = vroom::vroom(input$upload$datapath, delim = ","),
tsv = vroom::vroom(input$upload$datapath, delim = "\t"),
validate("Invalid file; Please upload a .csv or .tsv file")
)
})

output$head <- renderTable({
head(data(), input$n)
})
}

请注意,由于multiple = FALSE(默认值),input$file将是一个单行数据框,而input$file$nameinput$file$datapath将是一个长度为1的字符向量。

9.2 下载

接下来,我们将讨论文件下载,向你展示基本的用户界面和服务器组件,然后演示如何使用它们来允许用户下载数据或报告。

9.2.1 基本操作

同样,用户界面非常简单:使用downloadButton(id)downloadLink(id)为用户提供一个可点击的下载文件选项。结果如图9.2所示。

1
2
3
4
ui <- fluidPage(
downloadButton("download1"),
downloadLink("download2")
)

图9.2 一个下载按钮和一个下载链接

你可以使用与actionButtons()相同的classicon参数来自定义它们的外观,如第2.2.7节所述。

与其他输出不同,downloadButton()不与渲染函数配对。相反,你使用downloadHandler(),它看起来像这样:

1
2
3
4
5
6
7
8
output$download <- downloadHandler(
filename = function() {
paste0(input$dataset, ".csv")
},
content = function(file) {
write.csv(data(), file)
}
)

downloadHandler()有两个参数,都是函数:

  • filename应该是一个没有参数的函数,返回一个文件名(字符串)。这个函数的工作是创建将在下载对话框中显示给用户的文件名。

  • content应该是一个带有一个参数file的函数,file是保存文件的路径。这个函数的工作是将文件保存在Shiny知道的位置,以便可以将其发送给用户。

这是一个不寻常的接口,但它允许Shiny控制文件应该保存的位置(以便可以将其放置在安全位置),同时你仍然可以控制文件的内容。

接下来,我们将把这些部分组合在一起,展示如何将数据文件或报告传输给用户。

9.2.2 下载数据

以下应用通过允许你下载datasets包中的任何数据集作为制表符分隔的文件,展示了数据下载的基础知识,如图9.3所示。我建议使用.tsv(制表符分隔值)而不是.csv(逗号分隔值),因为许多欧洲国家使用逗号来分隔数字的整数部分和小数部分(例如1,231.23)。这意味着它们不能使用逗号来分隔字段,而是在所谓的“c”sv文件中使用分号!通过使用制表符分隔的文件,你可以避免这种复杂性,因为它们在任何地方的工作方式都是相同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ui <- fluidPage(
selectInput("dataset", "Pick a dataset", ls("package:datasets")),
tableOutput("preview"),
downloadButton("download", "Download .tsv")
)

server <- function(input, output, session) {
data <- reactive({
out <- get(input$dataset, "package:datasets")
if (!is.data.frame(out)) {
validate(paste0("'", input$dataset, "' is not a data frame"))
}
out
})

output$preview <- renderTable({
head(data())
})

output$download <- downloadHandler(
filename = function() {
paste0(input$dataset, ".tsv")
},
content = function(file) {
vroom::vroom_write(data(), file)
}
)
}

图9.3 一个更丰富的应用,允许你选择内置的数据集并在下载前预览。可以在https://hadley.shinyapps.io/ms-download-data查看实时效果。

注意,这里使用了validate()来仅允许用户下载是数据框的数据集。更好的做法是对列表进行预过滤,但这样可以让您看到validate()的另一个应用。

9.2.3 下载报告

除了下载数据外,您可能还希望允许您的应用下载报告,该报告总结了Shiny应用中交互式探索的结果。这是一项需要大量工作的事情,因为您还需要以不同的格式显示相同的信息,但对于高风险的应用来说,这是非常有用的。

生成此类报告的一种强大方法是使用参数化的RMarkdown文档。参数化的RMarkdown文件在YAML元数据中有一个params字段:

1
2
3
4
5
6
7
title: My Document
output: html_document
params:
year: 2018
region: Europe
printcode: TRUE
data: file.csv

在文档中,您可以使用params$yearparams$region等来引用这些值。YAML元数据中的值是默认值;您通常通过在调用rmarkdown::render()时提供params参数来覆盖它们。这使得从相同的.Rmd生成许多不同的报告变得容易。

以下是一个简单的示例,改编自https://shiny.rstudio.com/articles/generating-reports.html,该文章更详细地描述了这种技术。关键思想是从downloadHander()content参数中调用rmarkdown::render()。如果您想生成其他输出格式,只需在.Rmd中更改输出格式,并确保更新扩展名(例如,更改为.pdf)。请在https://hadley.shinyapps.io/ms-download-rmd查看实时效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ui <- fluidPage(
sliderInput("n", "Number of points", 1, 100, 50),
downloadButton("report", "Generate report")
)

server <- function(input, output, session) {
output$report <- downloadHandler(
filename = "report.html",
content = function(file) {
params <- list(n = input$n)

id <- showNotification(
"Rendering report...",
duration = NULL,
closeButton = FALSE
)
on.exit(removeNotification(id), add = TRUE)

rmarkdown::render("report.Rmd",
output_file = file,
params = params,
envir = new.env(parent = globalenv())
)
}
)
}

渲染一个.Rmd文件通常需要几秒钟的时间,因此这是使用第8.2节中提到的通知的好地方。

还有一些其他值得了解的技巧:

  • RMarkdown在当前工作目录中运行,这在许多部署场景中可能会失败(例如,在shinyapps.io上)。您可以通过在应用启动时将报告复制到临时目录来解决此问题(即在server函数外部):

    1
    2
    report_path <- tempfile(fileext = ".Rmd")
    file.copy("report.Rmd", report_path, overwrite = TRUE)

    然后,在调用rmarkdown::render()时,将"report.Rmd"替换为report_path

    1
    2
    3
    4
    5
    rmarkdown::render(report_path, 
    output_file = file,
    params = params,
    envir = new.env(parent = globalenv())
    )
  • 默认情况下,RMarkdown会在当前进程中渲染报告,这意味着它将继承Shiny应用的许多设置(如加载的包、选项等)。为了获得更大的健壮性,我建议使用callr包在单独的R会话中运行render()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    render_report <- function(input, output, params) {
    rmarkdown::render(input,
    output_file = output,
    params = params,
    envir = new.env(parent = globalenv())
    )
    }

    server <- function(input, output) {
    output$report <- downloadHandler(
    filename = "report.html",
    content = function(file) {
    params <- list(n = input$slider)
    callr::r(
    render_report,
    list(input = report_path, output = file, params = params)
    )
    }
    )
    }

您可以在Mastering Shiny GitHub仓库中的rmarkdown-report/文件夹中看到所有这些组件组合在一起。

shinymeta包解决了一个相关的问题:有时您需要将Shiny应用的当前状态转换为可在未来重新运行的可重复报告。要了解更多信息,请查阅Joe Cheng在useR! 2019年主题演讲中介绍的“Shiny的神圣目标:交互性与可重复性”。

9.3 案例研究

最后,我们将通过一个简单的案例研究来结束本章。在这个案例中,我们将上传一个文件(用户可以指定分隔符),预览它,使用Sam Firke的janitor包进行一些可选的转换,然后让用户将其下载为.tsv文件。

为了更容易地理解如何使用这个应用,我使用了sidebarLayout()来将应用分为三个主要步骤:

  • 上传和解析文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ui_upload <- sidebarLayout(
    sidebarPanel(
    fileInput("file", "Data", buttonLabel = "Upload..."),
    textInput("delim", "Delimiter (leave blank to guess)", ""),
    numericInput("skip", "Rows to skip", 0, min = 0),
    numericInput("rows", "Rows to preview", 10, min = 1)
    ),
    mainPanel(
    h3("Raw data"),
    tableOutput("preview1")
    )
    )
  • 清理文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ui_clean <- sidebarLayout(
    sidebarPanel(
    checkboxInput("snake", "Rename columns to snake case?"),
    checkboxInput("constant", "Remove constant columns?"),
    checkboxInput("empty", "Remove empty cols?")
    ),
    mainPanel(
    h3("Cleaner data"),
    tableOutput("preview2")
    )
    )
  • 下载文件

    1
    2
    3
    ui_download <- fluidRow(
    column(width = 12, downloadButton("download", class = "btn-block"))
    )

    然后,这些文件被组合成一个单独的fluidPage()

    1
    2
    3
    4
    5
    ui <- fluidPage(
    ui_upload,
    ui_clean,
    ui_download
    )

    这种相同的组织方式使得理解应用程序变得更加容易。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    server <- function(input, output, session) {
    # Upload ---------------------------------------------------------
    raw <- reactive({
    req(input$file)
    delim <- if (input$delim == "") NULL else input$delim
    vroom::vroom(input$file$datapath, delim = delim, skip = input$skip)
    })
    output$preview1 <- renderTable(head(raw(), input$rows))

    # Clean ----------------------------------------------------------
    tidied <- reactive({
    out <- raw()
    if (input$snake) {
    names(out) <- janitor::make_clean_names(names(out))
    }
    if (input$empty) {
    out <- janitor::remove_empty(out, "cols")
    }
    if (input$constant) {
    out <- janitor::remove_constant(out)
    }

    out
    })
    output$preview2 <- renderTable(head(tidied(), input$rows))

    # Download -------------------------------------------------------
    output$download <- downloadHandler(
    filename = function() {
    paste0(tools::file_path_sans_ext(input$file$name), ".tsv")
    },
    content = function(file) {
    vroom::vroom_write(tidied(), file)
    }
    )
    }

    图9.4 一个允许用户上传文件、执行一些简单的清理操作,然后下载结果的应用程序。可访问 https://hadley.shinyapps.io/ms-case-study 查看实时效果。

9.4 练习

  1. 使用Thomas Lin Pedersen的ambient包生成Worley噪声,并下载其PNG图像。

  2. 创建一个应用程序,允许用户上传CSV文件,选择一个变量,然后对该变量执行t.test()。在用户上传CSV文件后,您需要使用updateSelectInput()来填充可用的变量。详情请参阅第10.1节。

  3. 创建一个应用程序,允许用户上传CSV文件,选择一个变量,绘制直方图,然后下载直方图。作为额外的挑战,允许用户从.png.pdf.svg输出格式中进行选择。

  4. 编写一个应用程序,允许用户使用Ryan Timpe的brickr包从任何.png文件创建乐高马赛克。完成基本功能后,添加控件以允许用户选择马赛克的大小(以砖块为单位),并选择使用“universal”“generic”颜色调色板。

  5. 第9.3节中的最后一个应用程序包含了一个大型的响应式表达式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    tidied <- reactive({
    out <- raw()
    if (input$snake) {
    names(out) <- janitor::make_clean_names(names(out))
    }
    if (input$empty) {
    out <- janitor::remove_empty(out, "cols")
    }
    if (input$constant) {
    out <- janitor::remove_constant(out)
    }

    out
    })

    将其分解为多个部分,以便当input$empty发生变化时,不会重新运行janitor::make_clean_names()

9.5 总结

在本章中,您学习了如何使用fileInput()downloadButton()来在用户之间传输文件。大多数挑战要么来自处理上传的文件,要么来自生成要下载的文件,因此我向您展示了如何处理一些常见的情况。如果我没有涵盖您在此处的特定挑战,那么您需要将您独特的创造力应用于问题中。

下一章将帮助您处理在处理用户提供的数据时遇到的一个常见挑战:您需要动态地调整用户界面以更好地适应数据。我将从一些易于理解且可以在许多情况下应用的简单技术开始,逐渐过渡到完全动态的用户界面。

加关注

关注公众号“生信之巅”。

生信之巅微信公众号 生信之巅小程序码

敬告:使用文中脚本请引用本文网址,请尊重本人的劳动成果,谢谢!Notice: When you use the scripts in this article, please cite the link of this webpage. Thank you!

上一篇:
Shiny从入门到入定——10-动态UI
下一篇:
Shiny从入门到入定——8-用户反馈