第13章 批量处理文件

    这就是 R。没有做不到,只有想不到。

— Simon Blomberg, April 2005

相信你和我一样,电脑里保存了大量的文件吧。比如数据,有时候需要从多个文件里提取有用的信息,手动查找和摘选十分耗时耗力;比如照片,动辄成百上千张,虽然有些软件可以自动整理,但毕竟难以随心所欲。

我们前边学了编程的结构和字符串处理,如果把这些结合起来,再学几个文件操作函数,就可以高效地对电脑文件为所欲为了。

13.1 批量整理照片文件

事实上,我们从第2章起就已经悄悄开始接触文件操作函数了。当时我们用dir.create()创建了个文件夹,还用file.choose()来选取文件。后者有几个双胞胎姐妹:choose.files()函数可以用来选取多个文件,而choose.dir()可以用来选取文件夹。请复习一下这几个函数,热热身。

下面,我们运行下面的代码,会从我们服务器下载一个压缩文件,并解压缩到指定文件夹。打开这个文件夹,你会看到3个图片文件。我们假定这是我们手机里拍的照片,我们的目标是修改这3个文件的名称为拍摄的时刻。一旦能实现这个目标,那么对成千上万个文件重新命名都将不费吹灰之力。

download.file(url = "http://dapengde.com/r4rookies/figren.zip", 
              destfile = "c:/r4r/figren.zip")
unzip(zipfile = "c:/r4r/figren.zip", exdir = "c:/r4r")

首先,我们使用dir()函数,可以获取文件夹里的文件列表。如果将参数指定为完整文件名,那么得到的是文件的完整路径。

fotodir <- 'c:/r4r/figren'
fotofilefull <- dir(fotodir, full.names = TRUE)
fotofile <- dir(fotodir)

然后,使用file.info()函数,可以获取文件的完整信息:

fotoinfo <- file.info(fotofilefull)
fotoinfo
##                            size isdir mode
## c:/r4r/figren/IMG_689.jpg  1716 FALSE  666
## c:/r4r/figren/IMG_690.jpg 56321 FALSE  666
## c:/r4r/figren/IMG_691.jpg   307 FALSE  666
##                                         mtime
## c:/r4r/figren/IMG_689.jpg 2017-06-29 22:02:44
## c:/r4r/figren/IMG_690.jpg 2017-06-29 22:02:44
## c:/r4r/figren/IMG_691.jpg 2017-06-29 22:02:44
##                                         ctime
## c:/r4r/figren/IMG_689.jpg 2017-06-29 22:02:44
## c:/r4r/figren/IMG_690.jpg 2017-06-29 22:02:44
## c:/r4r/figren/IMG_691.jpg 2017-06-29 22:02:44
##                                         atime exe
## c:/r4r/figren/IMG_689.jpg 2017-06-29 22:02:44  no
## c:/r4r/figren/IMG_690.jpg 2017-06-29 22:02:44  no
## c:/r4r/figren/IMG_691.jpg 2017-06-29 22:02:44  no

其中的mtime列,是我们需要的文件创建时刻,小时分秒之间使用冒号分隔。由于操作系统不允许文件名中含有冒号,我们利用前面学过的时刻格式处理函数,稍微调整一下格式,设定新的文件名:

fototime <- format(fotoinfo$mtime, '%Y-%m-%d-%H%M%S')
newname <- paste(
  fotodir, '/', fototime, '_', fotofile, sep = '')

最后,我们用文件重命名函数file.rename()函数,就可以对文件批量重命名了:

file.rename(fotofilefull, newname)

如果有成千上万张照片需要这样整理,R是极佳的选择。

思考 13.1 除了照片文件,平时还有什么其他文件需要类似的方法整理吗?

我们还可以将照片按年份或月份归类整理到文件夹里。该怎么操作呢?下面就会讲到。

Example 13.1 将你自己的一批照片的文件名里插入日期信息,并按默认排序在文件名里增加从1开始的编号。

13.2 从网页批量下载和整理图片

我所在单位有个网站,有个页面展示着研究组野外观测照片缩略图,并按站点分了类36。新来到这里时,我想把本组的工作了解一番,在看图的时候,需要逐个点开才能看大图上的细节,将来需要某张图时,找起来很不方便,我就萌生了把图片全部下载到本地并按站点分类保存的想法。但是这几百张图,一一点开下载也太累了。如何批量下载网页上的图片呢?

方案有很多,列举如下:

  • 可以通过浏览器的’保存网页全部内容’来实现,本地生成一个文件夹,包含了网页上的图片。这个我试了,但只保存下来了缩略图,没有大图。

  • 可以安装迅雷、快车之类的软件,但是我不想装。有些软件臃肿庞大也就算了,关键是不知道他们在背后悄悄做了些什么。另外,他们无法解决图片分类保存的问题。

  • 傲游、360等浏览器有批量下载功能,或者firefox+BatchDownload 插件也行,但我不想装,并且他们也无法解决图片分类的问题。

  • chrome浏览器有个fatkun插件,专门用来批量下载图片,能下载大图,是我最满意的方案了,但也并非完美。下载的图片文件名要么是原始文件名,要么只能简单编号。这样一来,所有观测站点的图片都混在了一起,这仍然不是我想要的。我希望下载到本地的图片能自动按观测站分类保存。

其实,查看一下网页的源代码(chrome浏览器里按快捷键ctrl+u),发现每张图片所属站点的信息,包含在了图片的链接里。比如Neustift观测站某图的链接是:

http://...gallery/neustift/img_8260_59_58_....jpg

这个链接里是含有站名信息的(neustift)。这就好办了,可以自己动手用R代码实现。

首先,我们把网页的源代码读进R,跟学习字符函数时读取千字文文件的方法一样:

urlink <- 'http://www.biomet.co.at/pictures/'
aa <- readLines(urlink, encoding='UTF-8') # 读取网页

然后,我们找到有图片链接的行,也就是含有下面字符串的行:

src="http://www.biomet.co.at/wp/wp-content/gallery

获取这些行的方法是我们学过的grep()函数:

linkformat <- 
  'src="http://www.biomet.co.at/wp/wp-content/gallery'
bb <- aa[grep(linkformat, aa)]
思考 13.2 有没有别的更方便的办法,来获取一个网页上的超级链接呢?

接着,我们用循环函数,对得到的每一行进行处理,把图片的链接提取出来,并且去掉重复的图片链接:

for (i in 1:length(bb)) 
  bb[i] <- substring(
    bb[i], 
    regexpr("http", bb[i])[1], 
    regexpr(".jpg\"", bb[i])[1]+3) # 获取链接
bb <- unique(bb)
length(bb)
writeLines(bb, 'c:/r4r/links.txt')

每个链接里,从第47个字符开始就是观测站名了。为了简便,我们截取站名的前4个字母,并以此为名称,用dir.create()函数新建一批文件夹:

stname <- substring(bb, 47, 50)
stname <- stname[-which(stname == '')]
for (i in unique(stname)) 
  dir.create(paste('c:/r4r/', i, sep = ''))

快看看新的空文件夹是不是已经在那里了?

一切准备妥当了。下面,我们就用download.file()将图片下载保存到对应的文件夹里。由于文件多,可以预料,整个过程可能会比较耗时,所以我们用第5.6节的方法,在循环中添加了一个print()函数来提醒我们下载进度,并在循环结束后用winDialog()函数来弹出一个任务完成的提示框。

for (i in 1:length(bb)) {
  download.file(
    url = bb[i], 
    destfile = paste(
      'c:/r4r/', stname[i],'/', stname[i], i, '.jpg', 
      sep = ""), 
    method = 'curl', quiet = TRUE) 
  print(paste(i, 'of', length(bb), 'downloaded.'))
}
winDialog(type = c("ok"), message = '下载完毕!')

13.3 从大量文件里提取汇总信息

有时候,我们需要从大量的数据文件中,提取感兴趣的条目并汇总到一起进行分析。这里我们举个例子。

我国有数千个气象观测站。每隔一段时间,各气象站就把当地的气象要素观测数据上传到国家一级的服务器,在信息中心汇总成一个文件,每个观测站的数据占一行,这个汇总的文件就有几千行。如果要从中获取某个观测站气象要素的时间序列,就需要从每个这样的文件里找到来自该观测站的那一行,附加上时刻信息,合并为该观测站的一个数据文件进行后续分析。

这样的文件可以从我们的服务器下载。为了方便,我们把原来的几千行删减为几十行,并且仅给出6个文件供示范。

我们先把这样文件的一个压缩包下载到本地电脑,并解压缩为文件夹。

download.file(url = "http://dapengde.com/r4rookies/obs.zip", 
              destfile = "c:/r4r/obs.zip")
unzip(zipfile = "c:/r4r/obs.zip", exdir = "c:/r4r")

请用记事本打开任意一个文件。可以看出,每个文件的前两行是文件头。从第三行起是个数据表,第一列是观测站的编号,从第二列起是各种观测要素。时刻信息既包含在文件头里,也包含在文件头里。现在,假定我们要从这6个文件里提取编号为54527观测站的数据。

像前面的例子那样,我们先获取文件列表,把文件名作为时刻信息存储。

stn <- 54527
obsdir <- 'c:/r4r/obs'
obsfilefull <- dir(obsdir, full.names = TRUE)
obstime <- as.numeric(dir(obsdir))

然后,我们先准备好一个名为output的空变量,用来存放输出结果。我们使用循环语句,逐个读取每个文件,从中找到目标行,并将其作为新行追加到output后面,让这个新行的行名称为时刻信息。

output <- NULL
for (k in 1:length(obsfilefull))
{
  input <- read.table(obsfilefull[k], header = FALSE, 
                      skip = 2, sep="")
  output_new <- input[which(input[, 1] == stn),]
   if (nrow(output_new) != 0) 
     rownames(output_new) <- obstime[k]
  output <- rbind(output, output_new)
}
output$time <- rownames(output)

得到的output数据框,就是目标观测站的时间序列。

小贴士 13.1 常用文件操作函数

名称 作用
file.show()file.info() 查看文件内容或信息
file.exists() 检查文件是否存在
file.create()dir.create() 新建文件或目录
file.copy() 文件复制
file.remove() 文件删除(注意!直接删除!不进入回收站!)
file.rename() 文件重命名
download.file() 下载文件
unzip() 解压缩文件
dir() 查看目录下文件清单
file.choose(), choose.files() 选取文件或目录
choose.dir() 选取文件夹
Example 13.2 从上述气象观测站的数据中,筛选出所有编号以50开头的观测站,并提取这些观测站的第4列气象要素,连同时刻信息一起合并在一个文件里。时刻信息从文件头获取。

Example 13.3 将练习13.2得到的数据,对每个观测站该项气象要素做时间序列图,并做出每个观测站该气象要素的箱式图(boxplot)。

13.4 课外活动:打通任督二脉

如果你熟悉WIndows操作系统,那么应该知道cmd,也就是命令行。同时按下键盘的windows键和r字母键,在弹出的小窗口里输入cmd,回车,出现的那个简陋的黑色小窗口就是cmd。在开始菜单里搜索cmd也能搜到。别看它又黑又丑,它的强大会让你惊叹。如果R和cmd强强联手,你的电脑就被打通了任督二脉,离练成绝世神功已经不远了。下面我们举几个例子。

让我们先打开任脉。请在cmd小黑窗里输入:

notepad

并回车,记事本就被打开了。这就是用cmd打开记事本的指令。

如果不使用cmd小黑窗,在R里也可以进行等同的操作,只需使用shell()函数:

shell('notepad')

shell()就是R用来呼唤cmd的方式。R可以呼唤电脑里已经安装的软件,例如网页浏览器:

shell('start iexplore http://xuer.pzhao.net')

或者打开qq:

shell('cmd /c "D:/Program Files/Tencent/QQProtect.exe"')

当然,你得把上面这条代码里QQProtect.exe的完整路径改成你自己电脑上的路径才行。

如果一段R代码在办公室处理大量数据尚未完成,而我又着急下班,那么我可以事先在R代码的最后加上一条打开qq的指令,就可以回家了。当我在手机上看到办公室电脑的qq上线了,就意味着R已经把数据处理完了。如果配合AutoHotKey这样的软件,我甚至可以让办公室的qq自动发一条“搞定!”的信息给自己的手机37

cmd支持的命令非常丰富,可以完成很多原本复杂的工作。R调用cmd,如虎添翼。如果感兴趣,可以去学一下批处理脚本,把cmd要执行的多条命令写在.bat文件里,让R来调用,那必将是一片繁华。

现在,R呼唤cmd的任脉已经打通了,下面我们打通督脉——让cmd呼唤R。

假定你有一批打算要自动运行的R代码,保存在文件c:/r4r/timer.r,并且假定你的R安装路径是D:/Program Files/R/bin/R.exe,那么,请在cmd小黑窗里运行:

"D:/Program Files/R/bin/R.exe" CMD BATCH --vanilla --slave 
  "c:/r4r/timer.r"

这条命令的含义是由cmd呼唤R来运行timer.r里的代码。一声召唤后,timer.r里的代码全部自动运行,该画的图自动画,该写出的数据自动写,根本不用打开RStudio。

这有什么用呢?说说我是怎么用的吧。

有段时间,我参加了连续一个多月的野外科研观测,需要把这一个月的天气状况记录在案,每个小时是阴是晴,是雨是雪。天气状况其实在很多天气网站上都有,但是只能实时浏览,网页每天更新,并且不能下载最新一个月的历史记录。这种事当然可以安排个人每天手动把信息摘抄整理下来,但是我有R在手啊!于是,我写了一段R代码,可以获取天气网站当天显示的天气现象并保存到一个文件里,并做出需要的图表。然后,我又写了一句cmd代码保存在脚本文件.bat里,用来呼唤上述R代码文件。最后,在windows的“计划任务”里设置每天运行一次.bat文件。好了,一切每天都自动完成了。

我们在本章学了用R整理照片、下载图片和处理大量数据文件;在下一章,我们还将学会批量制作Word文档和PPT幻灯片。如果这些任务需要定期大量重复操作,那么,cmd+R+计划任务的组合,将节省大量的人工劳动。

这只是我作为一个菜鸟的想法。反正,任督二脉已经打通。在这个神奇世界里,发挥你的想象力,想怎么折腾就怎么折腾咯!能不能成为绝顶高手,就看你自己咯!


  1. 因斯布鲁克大学生态气象研究组:http://www.biomet.co.at/pictures/

  2. 事实上,R有专门的扩展包,可以自动发送电子邮件,比调用qq更爽快。