我逐渐意识到,Unix 的命令行工具可以解决一切与文字处理有关的问题。我来讲一个我遇到过的问题,以及怎样用 Unix 命令行工具解决的。
问题
我在做我的硕士论文研究。像许多统计学家一样,我需要进行许多模拟。首先我利用一些数字的种子(以保证可复现性)来模拟了一些数据,然后在这些数据上用一个算法来估算一些东西。每次运行模拟时我都会创建一些文件,大概像下面这样:
dataset-directory/0001_data.csvdataset-directory/0001_A.csv
但有时候运行会失败。这并不是太严重,模拟失败了我只需要运行另一个模拟就好了。比如,0001 这个数据我成功地运行了算法 A。因此我想在 0001 数据上使用算法 B。但我真正需要的是跟踪哪些命令失败了。
在许多数据上运行算法 A 之后,我得到了一大堆文件,像这样:
dataset-directory/0001_data.csvdataset-directory/0001_A.csvdataset-directory/0002_data.csvdataset-directory/0002_A.csvdataset-directory/0003_data.csvdataset-directory/0003_A.csvdataset-directory/0004_data.csvdataset-directory/0005_data.csvdataset-directory/0005_A.csvdataset-directory/0006_data.csvdataset-directory/0006_A.csvdataset-directory/0007_data.csvdataset-directory/0007_A.csvdataset-directory/0008_data.csvdataset-directory/0009_data.csvdataset-directory/0009_A.csv...dataset-directory/0499_A.csv
通过观察得知算法 A 在数据 0004 和 0008 上的结果不存在。那么,怎样才能列出所有 A 没有成功的数据呢?
我当然可以手工来做,但那样很容易出错,而且很麻烦。要是能写程序完成就最好了!
解决方案
简单地来说,那些没有成功的就是从0001到0500的数字中去掉那些成功了的数字。而得到数字列表的最好命令就是 seq:
$> seq 1012345678910
(如果只给出一个数字,那么默认序列从1开始。当然,seq 2 10也会按照你期待的那样工作。)
现在,如果我能获得成功运行的列表,就可以通过 seq 命令生成所有可能的数字,再交叉检查两个列表来获得想要的结果了!
许多命令行工具只完成一项工作。例如,cut 可以获得每行指定位置上的字符:
$> cat textLorem ipsumdolor sit amet$> cat text | cut -c 2-5oremolor
注意这里使用了所谓的“管道运算符” | 。我说过,许多工具只做一件事,而且把那件事做到最好。很方便的是这些工具可以组合使用。使用管道,左边命令的结果就会变成右边命令的输入。注意这些命令会将输入当做一系列行来处理,通常来说这非常方便。
我们可以把成功的文件名单列表利用管道输入至 grep 命令,该命令可以使用正则表达式来查找。由于所有文件都以同样长度的四个数字开始,我们可以利用正则表达式 dddd 来匹配这四个连续的数字,然后将文件末尾的 A 算法加到正则表达式中。要获得文件列表,每行一个文件,只需执行 ls。(尽管直接调用 ls 时并不会显示成每行一个文件,但放在管道中时输出就会变成这样。)
$> ls dataset-directory | grep 'dddd_A.csv'0009_A.csv0001_A.csv0002_A.csv0005_A.csv0007_A.csv0003_A.csv0006_A.csv...
出于某些原因,在使用 grep 之后这里的顺序混乱了。我们可以使用 sort 命令来改正。而且由于我们只关心数字,可以使用 cut -c 1-4 来取出数字部分。
$> ls dataset-directory | grep 'dddd_A.csv' | sort | cut -c 1-40001000200030005000600070009...0499
这些数字跟 seq 命令的结果不太一样,因为它们前面补了零。我们可以使用一段 Python 脚本将它们转成整数。
# parse.pyimport sysfor line in sys.stdin: i = int(line) print(i)
现在将结果用管道导入这个脚本就能得到我们需要的数字:
$> ls dataset-directory | grep 'dddd_A.csv' | cut -c 1-4 | python3 parse.py1235679...499
接近目标了!现在需要找出怎样才能交叉检查这两个数字的列表。幸运的是,有个命令叫做comm,它能检查两个输入流中的公共字符。为了将上面这个命令的输出作为输入使用,我们可以将其求值后重定向,只需使用<(...)即可实现:
$> comm <(ls dataset-directory | grep 'dddd_A.csv' | cut -c 1-4 | python3 parse.py) <(seq 500) 1 2 3 4 5 6 7 8 9 10... 500
这个输出不是太好理解。阅读 comm 的手册(运行 man comm)可以发现,comm 会“产生三列的输出,分别是:只存在于 file1 的行,只存在于 file2 的行,以及存在于两个文件的行”。要去掉第一列(其实它是空的,因为没有任何数字只存在于文件列表中),可以在调用 comm 时加入参数-1。我们也不关心同时存在于两个输入中的数字,因此还要传递标志-3。
$> comm -1 -3 <(ls dataset-directory | grep 'dddd_A.csv' | cut -c 1-4 | python3 parse.py) <(seq 500)48...500
搞定!
全部0条评论
快来发表一下你的评论吧 !