Skip to content

Chapter-07 Unix Data Tools

约 5582 个字 252 行代码 预计阅读时间 23 分钟

本节我们将详细了解如何在Unix shell中利用命令行工具进行生物信息学研究。

Inspecting and Manipulating Text Data with Unix Tools

在生物信息学中,很多数据都是以表格的形式存储的——tabular plain-text files,各个数据之间以制表符分割,其的优势是可以被很多Unix shell命令,Python和R识别。

Tabular Plain-Text Data Formats

表格形式的纯文本数据通常单行独立,列间分割,每列之间隔开的分隔符可分为制表符、逗号和可变空格。在生物信息学中常见制表符分隔。

  • 常见表格数据格式以\t制表符分隔。
  • CSV以逗号,分隔数据。
  • 有些表格数据格式的文件采用空格来分割数据,但是这不被提倡。

How does file sperate lines?

值得我们注意的是,在Linux中以单个换行符\n分割各行,而在Windows中则遵循DOS风格的\r\n换行。特别的,对于CSV而言,它通常也遵循\r\n的换行风格。

本节我们主要涉及BED格式和GTF格式的文件,这两个格式都是典型的制表符分隔的文件。

Inspecting Data with Head and Tail

一个生信数据文件往往很长,如果我们无脑cat,你的终端马上会陷入一团糟,更好的方法是使用head命令来查看文件前几行:

 head Mus_musculus.GRCm38.75_chr1.bed
1       3054233 3054733
1       3054233 3054733
1       3054233 3054733
1       3102016 3102125
1       3102016 3102125
1       3102016 3102125
1       3205901 3671498
1       3205901 3216344
1       3213609 3216344
1       3205901 3207317

head提供了-n参数,可以控制查看的行数,默认是10:

 head -n 3 Mus_musculus.GRCm38.75_chr1.bed
1       3054233 3054733
1       3054233 3054733
1       3054233 3054733

head -n 3是一个很甜点的命令,它恰好可以让我一窥文件的列数,是否有行标题和列标题以及数据大致是啥样等等。

head相反,tail可以打印文件最后几行的内容:

 tail -n 3 Mus_musculus.GRCm38.75_chr1.bed
1       195240910       195241007
1       195240910       195241007
1       195240910       195241007

head略有不同的是,tail-n可以指定为+2等,其含义是“从第2行开始输出”,通过这个命令,我们可以很方便的截断文件可能存在的标题行。

 seq 3 > data.txt
 cat data.txt
1
2
3
 tail -n +2 data.txt
2
3

headtail结合起来,我们可以快速检查文件的开头和结尾:

 (head -n 2; tail -n 2) < Mus_musculus.GRCm38.75_chr1.bed | column -t
1  3054233    3054733
1  3054233    3054733
1  195240910  195241007
1  195240910  195241007

这个命令可能太长,我们可以在shell的配置文件里增设一个指令:

# 在你的shell配置文件里加入下面这段命令
inspect() { (head -n 2; tail -n 2) < "$1" | column -t }

这样inspect就等同于上面那个命令的作用了!

 inspect Mus_musculus.GRCm38.75_chr1.bed
1  3054233    3054733
1  3054233    3054733
1  195240910  195241007
1  195240910  195241007

head很适合查看管道符中传递的数据,假如我们想要跟踪我们grep命令产生的数据:

grep 'gene_id "ENSMUSG00000025907"' Mus_musculus.GRCm38.75_chr1.gtf | head -n 1 | column -t
1  protein_coding  gene  6206197  6276648  .  +  .  gene_id  "ENSMUSG00000025907";  gene_name  "Rb1cc1";  gene_source  "ensembl_havana";  gene_biotype  "protein_coding";

SIGPIPE

在以上的命令中,grep不会筛选所有的文本,因为整个命令中,head在打印一行后就停止了,对于管道符而言,当它所辖的一个命令已经停止后,会发出一个SIGPIPE信号——类似于ctrl+c,使得命令中的所有进程停止。

在以下这个命令中,所有程序都会在head打印出三行内容后停止(除非grep提前终止了)。

grep "some_string" data.txt | program1 | program2 | head -n 3

less

less的哲学是“少即是多”,它是一个终端分页器,可以让你轻松浏览一个长文本文件,以下是常用的快捷键,你可以随时在less界面里按h来查看:

快捷键 操作
空格键 下一页
b 上一页
g 第一行
G 最后一行
j 向下(一次一行)
k 向上(一次一行)
/ 向下搜索(向前)字符串
? 向上搜索(向后)字符串
n 重复上次向下搜索(向前)
N 重复上次向上搜索(向后)

less还是一个很好的调试工具,用来查看管道之间的输出,并且less在文本充满屏幕的时候会拒绝输入,这会使得管道被堵塞,从而无需担心我们在实时调试时候无端耗费性能,因为less迫使整个命令暂停了。

你可以随时退出less,只需要按q

Plain-Text Data Summary Information with wc, ls, and awk

我们有时候不仅需要检查文件的内容(头部或尾部内容),还需要检查一些其他的文本信息,比如字数,行数或列数。对此,我们可以使用wc命令:

 wc Mus_musculus.GRCm38.75_chr1.bed
  81226  243678 1698545 Mus_musculus.GRCm38.75_chr1.bed

默认情况下,wc会输出指定文件的行数-l、字数-w和字符数-m,一般我们只关心行数,所以我们可以指定参数-l

 wc -l Mus_musculus.GRCm38.75_chr1.bed
81226 Mus_musculus.GRCm38.75_chr1.bed

wc可以很好的处理多文件:

 wc Mus_musculus.GRCm38.75_chr1.bed Mus_musculus.GRCm38.75_chr1.gtf
   81226   243678  1698545 Mus_musculus.GRCm38.75_chr1.bed
   81231  2385570 26607149 Mus_musculus.GRCm38.75_chr1.gtf
  162457  2629248 28305694 total

也许你注意到了,同一组数据文件的不同格式使用wc统计出的行数并不一致,让我们用head查看一下:

 head -n 5 Mus_musculus.GRCm38.75_chr1.gtf
#!genome-build GRCm38.p2
#!genome-version GRCm38
#!genome-date 2012-01
#!genome-build-accession NCBI:GCA_000001635.4
#!genebuild-last-updated 2013-09

五行差异就来源于此,需要注意的是,很多数据文件都有这样的注释,在统计行数的时候需要关照一下。

Warning

虽然wc可以快速的统计出文件的行数,但前提是文件的结构是排列良好的。如果我们仅有三行数据,但是文件中意外地在结尾多出了两个空格,那么wc -l的结果就5了。

当然,我们可以通过grep避免这种情况,如果你有一个文件results.txt,想要统计其中的空行数,你可以:

grep -c "[^ \\n\\t]" results.txt

当然,我们也很想知道文件的大小,使用ls -l即可:

 ls -l Mus_musculus.GRCm38.75_chr1.bed
-rw-r--r-- 1 yangshu233 yangshu233 1698545 May 28 16:20 Mus_musculus.GRCm38.75_chr1.bed

ls -l以字节数在第四列打印出文件大小,使用ls -lh更像人话一点:

 ls -lh Mus_musculus.GRCm38.75_chr1.bed
-rw-r--r-- 1 yangshu233 yangshu233 1.7M May 28 16:20 Mus_musculus.GRCm38.75_chr1.bed

当然,表格的列数也是我们十分关心的事情,Unix shell提供了awk命令,它是一个功能强大的小编程语言,但这里我们只简单演示一下如何用它查看文件列数:

 awk -F "\t" '{print NF; exit}' Mus_musculus.GRCm38.75_chr1.bed
3

其中NFawk内置的一个变量,其记录了当前文件的字段数量。默认情况下,awk以空格,制表符视为字段分隔符,这里我们通过参数-F规定分隔符为\t

特别地,有些文件,比如Mus_musculus.GRCm38.75_chr1.gtf在开头是有几行注释的,这会使得awk命令无法正常工作,因为他返回的是第一行注释的字段数量,所以我们要手动筛选一下:

# 使用tail命令
 tail -n +6 Mus_musculus.GRCm38.75_chr1.gtf | awk -F "\t" '{print NF; exit}'
9
# 使用grep命令
 grep -v "^#" Mus_musculus.GRCm38.75_chr1.gtf | awk -F "\t" '{print NF; exit}'
9

通常,使用grep命令更加健壮,因为它对不同注释数量的文件表现都很良好。你可以在shell的配置文件中加入如下语句:

ckcolumn() { grep -v "^#" $1 | awk -F "\t" '{print NF; exit}'}

再使用source重新加载一下配置,你就可以使用ckcolumn来检查一个表格文件的列数了!

Working with Column Data with cut and Columns

有时候我们只想要表格文件中的某一列信息,这时候可以用cut命令:

 cut -f 2 Mus_musculus.GRCm38.75_chr1.bed | head -n 3
3054233
3054233
3054233

我们通过参数-f指定cut提取文件中的第二列数据,当然,它还可以被指定为列数范围2-5或者特定列数2,4,5等等。

Note

cut命令无法对列重新排列,即使你指定-f3,2,1,其仍然遵循1,2,3的顺序输出。

假如我们想要提取GTF文件中的基因组范围(染色体,基因起始与结束为止),可以采用以下命令:

 grep -v "^#" Mus_musculus.GRCm38.75_chr1.gtf | cut -f1,4,5 | head -n 3
1       3054233 3054733
1       3054233 3054733
1       3054233 3054733

类似于awkcut命令可以通过-d参数指定分隔符,比如处理CSV文件时:

 cut -d, -f 1,2 Mus_musculus.GRCm38.75_chr1_bed.csv | head -n 3
1,3054233
1,3054233
1,3054233

Formatting Tabular Data with column

对于表格本身来说,其采用制表符来区分列元素,或者说标签的方式方便了计算机处理,但是有时候不适合人来阅读。原因是因为可变的数据长度破坏了良好的表格对齐方式,这对计算机来说没什么,但是对于以视觉为依靠的人类来说,下面这份数据看起来就一团糟:

 grep -v "^#" Mus_musculus.GRCm38.75_chr1.gtf | cut -f1-8 | head -n3
1       pseudogene      gene    3054233 3054733 .       +       .
1       unprocessed_pseudogene  transcript      3054233 3054733 .       +       .
1       unprocessed_pseudogene  exon    3054233 3054733 .       +       .

Unix当然考虑到了这种情况,我们可以通过column -t命令来重新对齐我们的表格:

 grep -v "^#" Mus_musculus.GRCm38.75_chr1.gtf | cut -f1-8 | head -n3 | column -t
1  pseudogene              gene        3054233  3054733  .  +  .
1  unprocessed_pseudogene  transcript  3054233  3054733  .  +  .
1  unprocessed_pseudogene  exon        3054233  3054733  .  +  .

其中-t参数代表着输入数据为表格类型。最终我们得到的结果显然更容易被阅读了!

cut类似的是,column默认分隔符为\t,如果我们想要处理类似于CSV这样的文件,可以用-s参数另外指定分隔符。

 column -s"," -t Mus_musculus.GRCm38.75_chr1_bed.csv | head -n 3
1  3054233    3054733
1  3054233    3054733
1  3054233    3054733

Make Data for Computer

很多时候,我们强调代码的可读性,是因为代码主要是给人类阅读的,操作代码的主要是人。但如果面向数据,则其主要的操作者是计算机,也就是说,数据的格式是要为计算机服务的。很多数据格式对于人类不好辨认,却容易被计算机处理,比如使用\t而不是可变长度的空格来分割列标签。

更重要的是,column这一命令行工具传递着这样的信息:“请为计算机组织数据格式”,虽然数据的原始格式对你来说可能阅读困难,但是column可以帮你转化,这促使人类更多将数据转化为计算机好处理的,而无需顾虑自己是否容易阅读。

所以,Program for human, data for computer.

The All-Powerful Grep

在前面的内容中,我们有简单接触过grep这个命令,但是那些只是浮于表面的知识,实际上grep是一个及其强大,而且快速的指令,它的诞生就是为了实现这么一个功能:快速找到匹配要求的文件行

让我们简单回顾一下grep的用法:grep [OPTION]... PATTERNS [FILE]...,其接受一些参数,需要pattern来匹配目标行,以及接受传入一个文本文件。

以下这个例子向我们展示了如何在文件Mus_muscu‐lus.GRCm38.75_chr1_genes.txt中找到基因"Olfr418-ps1":

 grep "Olfr418-ps1" Mus_musculus.GRCm38.75_chr1_genes.txt
ENSMUSG00000049605      Olfr418-ps1

包围pattern的引号不是必需的,但是其确实是一个好习惯,其避免了patterngrep错误地解读。

grep会返回所有能匹配上的行,即使这些行可能只是部分匹配:

 grep "Olfr" Mus_musculus.GRCm38.75_chr1_genes.txt | head -n 5
ENSMUSG00000067064      Olfr1416
ENSMUSG00000057464      Olfr1415
ENSMUSG00000042849      Olfr1414
ENSMUSG00000058904      Olfr1413
ENSMUSG00000046300      Olfr1412

参数--color=always非常好用,它会启用你终端的配色方案,并且将匹配到的字符用显眼的颜色标注出来,很多时候这个参数会被默认启用。

Warning

值得注意的是,Unix的这些工具有不同版本的实现,分别是以MacOS为代表的BSD utils和以Linux为代表的GNU coreutils。后者目前仍在活跃开发的阶段中,并且有很多很有意思的扩展功能,这些往往是前者所不具备的。所以这里很建议使用GNU coreutils的工具,更新更好。

前面我们有说,grep提供了-v这么一个参数,被称作反向模式,它很迅速帮我们排除了数据文件中的注释行。当然,这种反向排除的方法的应用场景还有很多,比如我们想知道所有包含"Olfr"但是不包含"Olfr1413"的基因:

 grep "Olfr" Mus_musculus.GRCm38.75_chr1_genes.txt | grep -v "Olfr1413"

请打住!需要强调的是,这种匹配方式往往也会过滤掉诸如"Olfr1413a"等基因,虽然它们符合我们的匹配规则,但并不是我们要排除的基因。为了避免这种情况的发生,我们可以指定-w参数选项,使得grep进行完全匹配——完全匹配被空格包围的单词。我们可以通过一个简单的例子来看看它是如何工作的:

 cat example.txt
bio
bioinfo
bioinformatics
computational biology
 grep -v bioinfo example.txt
bio
computational biology
 grep -v -w bioinfo example.txt
bio
bioinformatics
computational biology

使用-w是一种将我们的匹配变得更加精准,或者说严格的方式。通常情况下,我们的模式应该尽可能的严格,以避免意外的匹配结果。

不知道你有没有发现,之前的例子中,grep返回的结果虽然精确,却不能提供足够的上下文信息给到我们。而这点Unix早已想到,grep提供了以下三个参数来返回更多信息:

  • -B:before,返回匹配行与前文信息
  • -A:after,返回匹配行与后文信息
  • -C:context before and after,返回匹配行的前后文信息

以上三个参数均支持传入一个数字,以决定要打印多少行前后文信息。

 grep -B1 "AGATCGG" contaminated.fastq | head -n 6
@DJB775P1:248:D0MDGACXX:7:1202:12362:49613
CACTGCTTGCTCTGCGTTGATACCACTGCTTACTCTGCGTTGATACCACTGCTTACTCTGCGTTGATACCACTGCTTAGATCGGAAGAGCACACGTCTGAA
--
@DJB775P1:248:D0MDGACXX:7:1202:12782:49716
TTACTCTGCGTTGATACCACTGCTTACTCTGCGTTGATACCACTGCTTACTCTGCGTTGATACCACTGCTTACTCTGCGTTGATACCACTGCTTAGATCGG
--
 grep -A2 "AGATCGG" contaminated.fastq | head -n 6
CACTGCTTGCTCTGCGTTGATACCACTGCTTACTCTGCGTTGATACCACTGCTTACTCTGCGTTGATACCACTGCTTAGATCGGAAGAGCACACGTCTGAA
+
CCCFFFFFHHHHHGHGFHIJJ9HHIIJJJJJIJJJJIIJJJIIJIJJJJJJJJJIIJJJJJJHIHHHGHFFFFFFCEEEEEDBD?DDDDDDBDDDABDDCA
--
TTACTCTGCGTTGATACCACTGCTTACTCTGCGTTGATACCACTGCTTACTCTGCGTTGATACCACTGCTTACTCTGCGTTGATACCACTGCTTAGATCGG
+

ERE

在前文中我们一直使用的都是被称为POSIX Basic Reugular Expressions (BRE)的正则语法,其不如诸如Python等语言中提供的正则库强大,特别是在多值匹配的问题上(如果你足够熟悉它们的语法的话)。

对此,grep提供了对POSIX Extended Regular Expressions (ERE)的支持,它支持使用管道符|进行多值匹配:

 grep -E "(Olfr1413|Olfr1411)" Mus_musculus.GRCm38.75_chr1_genes.txt
ENSMUSG00000058904      Olfr1413
ENSMUSG00000062497      Olfr1411

当然,在这么短的篇幅里肯定无法涵盖BRE和ERE的精髓,仅作一个说明而已。

grep提供参数-c,可以统计匹配模式的行数:

 grep -c -P "\tOlfr" Mus_musculus.GRCm38.75_chr1_genes.txt
27

Note

在书中使用的命令是grep -c "\tOlfr" Mus_musculus.GRCm38.75_chr1_genes.txt,但在grep默认的BRE语法中,其对正则表达语法的支持是很薄弱的,比如在这里\t会被解释为t而不是制表符。类似于-E启用ERE语法支持,-P可以启用PCRE——Perl兼容正则表达式(最强的支持引擎)的语法支持。

借助wc -l也可以实现:

 grep -P "\tOlfr" Mus_musculus.GRCm38.75_chr1_genes.txt | wc -l
27

在表格样式的文件中,grep对匹配行数的计算同样有用,比如它可以让我们知道有某一特定属性的样本有哪些,像是基因类型为snRNA的:

 grep -c 'gene_biotype "snRNA"' Mus_musculus.GRCm38.75_chr1.gtf
315

有时候我们想要更多的上下文信息,有时候只希望得到准确匹配的部分,grep说没问题,请使用-o参数:

 grep -o "Olfr.*" Mus_musculus.GRCm38.75_chr1_genes.txt| head -n 3
Olfr1416
Olfr1415
Olfr1414

或者我们想知道所有的基因id:

 grep -o -E 'gene_id "\w+"' Mus_musculus.GRCm38.75_chr1.gtf | head -n 5
gene_id "ENSMUSG00000090025"
gene_id "ENSMUSG00000090025"
gene_id "ENSMUSG00000090025"
gene_id "ENSMUSG00000064842"
gene_id "ENSMUSG00000064842"

Decoding Plain-Text Data: hexdump

我们拿到的大多数数据文件都是ASCII编码的(想要了解更多ASCII的信息?在终端里使用man ASCII即可!),在大多数情况下编码和我们要解决的问题没啥关系,但是在某些特殊情况中,比如存在一个非ASCII字符时候,我们需要从基本的原始编码数据上查看文件,这需要我们掌握一些Unix提供的工具。

首先是如何查看文件的编码类型。Unix提供了file命令,其可以根据文件内容自动推断出文件的编码类型。

 file Mus_musculus.GRCm38.75_chr1_genes.txt
Mus_musculus.GRCm38.75_chr1_genes.txt: ASCII text

同时,UTF-8也是一个常见的文件编码格式,它是ASCII的一个拓展,支持更多特殊字符:

 file utf8.txt
utf8.txt: Unicode text, UTF-8 text

由于UTF-8本身属于ASCII的拓展,如果我们将文件中所有的非ASCII字符全部删去,file会返回ASCII编码。

对于大多数你拿到的数据文件来说,其都是标准的ASCII编码文件,比如从NCBI下载下来的数据。但是对于一些他人手动整理的数据文件,请务必小心,因为其中还很可能混入一些不正确的字符编码!有时候这种情况及其隐蔽,作者可能都不知情。

比如有一个improper.fa文件,表面上它是一个很正常的FASTA文件:

 cat improper.fa
>good-sequence
AGCTAGCTACTAGCAGCTACTACGAGCATCTACGGCGCGATCTACG
>bad-sequence
GATCAGGCGACATCGAGCTATCACTACGAGCGAGΑGATCAGCTATT

让我们使用hexdump这个命令行工具来一探究竟:

 hexdump -c improper.fa
0000000   >   g   o   o   d   -   s   e   q   u   e   n   c   e  \n   A
0000010   G   C   T   A   G   C   T   A   C   T   A   G   C   A   G   C
0000020   T   A   C   T   A   C   G   A   G   C   A   T   C   T   A   C
0000030   G   G   C   G   C   G   A   T   C   T   A   C   G  \n   >   b
0000040   a   d   -   s   e   q   u   e   n   c   e  \n   G   A   T   C
0000050   A   G   G   C   G   A   C   A   T   C   G   A   G   C   T   A
0000060   T   C   A   C   T   A   C   G   A   G   C   G   A   G 316 221
0000070   G   A   T   C   A   G   C   T   A   T   T  \n
000007c

可以发现,在"CGAGCGAG"后跟着两个不是对劲的非ASCII字符,但是当我们将其当做纯ASCII文本打印在终端上,两个非ASCII字符会被忽略,从而看起来没啥问题。

使用file命令也能发现不对劲:

 file improper.fa
improper.fa: Unicode text, UTF-8 text

当然,另一种查看非ASCII字符的方法,是使用grep命令,不过其比较复杂:

 LC_CTYPE=C grep --color='auto' -P "[\x80-\xFF]" improper.fa
GATCAGGCGACATCGAGCTATCACTACGAGCGAG��GATCAGCTATT

我强烈建议将其设置一个别名,因为它既快速也好用。

总的来说,filehexdumpgrep都为我们检查文件编码问题提供了方便,还是那句话:不要太信任你的数据

od

hexdump是一个随着BSD Unix流行起来的命令行工具,而本身不属于POSIX标准,也就是说,有些类Unix可能不含有它。

而POSIX标准里支持的查看文件原始编码信息的,是od (Octal Dump),顾名思义,其的重心在八进制输出上。

od的跨平台性更好,因为它是POSIX标准的一部分,其本身也支持丰富的格式输出,在很多时候其是不逊色于hexdump的。但是hexdump通常被认为更适合复杂的二进制分析任务,或者逆向工程。

Storing Plain-Text Data with Sort

可以理解的是,排序后的数据无论对于计算机还是对于人类,都是一个更良好的数据结构,比起随机排序的数据来说。

特别是在生信分析中,面对超大量的样本,排序良好的数据对某些命令来说是一种优化,意味着它们的运行效率更高,甚至可以覆盖掉预先排序的时间损耗。

在本节中,我们主要讨论sort命令的使用。类似于cutsort主要面向表格形式文件的列操作,如果不指定任何参数,sort会简单以行为单位,按照字母顺序排列:

 cat example.bed
chr1    26      39
chr1    32      47
chr3    11      28
chr1    40      49
chr3    16      27
chr1    9       28
chr2    35      54
chr1    10      19
 sort example.bed
chr1    10      19
chr1    26      39
chr1    32      47
chr1    40      49
chr1    9       28
chr2    35      54
chr3    11      28
chr3    16      27

Tip

默认情况下,sort以空白符号(如制表符或空格)作为字段分隔符,如果你在处理的文件使用其他分隔符(例如CSV),可以通过-t指定(e.g., -t ",")。

当然,仅以字母顺序进行排列无法满足我们的实际需求.对于sort,我们希望它可以:

  • 按照指定的列进行排序
  • 能够指定特定的值类型(比如数值类型)

显然,这些需求对于sort来说不过是顺手的事,让我们用它按照染色体编号起始位置来对example.bed排序:

 sort -k1,1 -k2,2n example.bed
chr1    9       28
chr1    10      19
chr1    26      39
chr1    32      47
chr1    40      49
chr2    35      54
chr3    11      28
chr3    16      27

此处我们使用参数-k来指定用来排序的列,该参数需要分别指定一个起始列和一个结束列,比如-k1,1以第一列为排序依据列。同一个sort可以有多个排序依据列,按照先后顺序被依次满足(默认下是升序)。

Reverse Sorting

默认情况下sort采用升序(无论是字典序还是数值排序)排序,我们可以指定-r参数来反转排序,也就是使用降序排序。

 sort -r -k1,1 -k2,2n example.bed
chr3    11      28
chr3    16      27
chr2    35      54
chr1    9       28
chr1    10      19
chr1    26      39
chr1    32      47
chr1    40      49

直接指定-r参数是对整体的排序进行反转,但是如果你只想在某一列上的排序反转,只需要在对应的-k参数后加上r即可。

 sort -k1,1 -k2,2nr example.bed
chr1    40      49
chr1    32      47
chr1    26      39
chr1    10      19
chr1    9       28
chr2    35      54
chr3    16      27
chr3    11      28

特别地,对于-k2,2n中n的含义为数值类型,默认下sort以字典序排序,指定n可以让其以数值类型进行排序。当然,如果你希望sort 完全以数值类型排序,可以指定-n参数,而无需额外在-k中指明。

Warning

或许你在写Python,或者别的地方了解到过,对于两个拥有完全相同排序键的对象,排序算法是无法将其区分的。很多时候,排序算法会试图通过某种方法来指定两者先后,其结果通常是不符合预期的。

而对于类似于Python中sorted函数来说,它面对两个排序键完全相同的对象时候,会选择遵循其原顺序。有这种特性的排序算法被称作是稳定的(stable)。

对于sort来说,如果不想要它随意改变两个“相同行”的原顺序,你可以指定-s参数使其稳定。

当然,sort是一种消耗大量资源的命令,在执行其之前,先检查一遍目标文件是否已排序是一个更为经济的选择。对于sort来说,指定参数-c使其启用检查模式,若目标文件已排序,则退出代码为0,否则为1。

 sort -k1,1 -k2,2n -c example.bed
sort: example.bed:4: disorder: chr1     40      49
 echo $?
1
 sort -k1,1 -k2,2n -c example_sorted.bed
 echo $?
0

此外,sort还有很多十分实用的参数值得了解,但首先要说明的是,这些参数仅在 GUN sort 中被支持。

首先是-V,使用这个参数可以启用更加智能的对于字符串中数字的识别与排序。

 cat example2.bed
chr2    15      19
chr22   32      46
chr10   31      47
chr1    34      49
chr11   6       16
chr2    17      22
chr2    27      46
chr10   30      42
 sort -k1,1 -k2,2n example2.bed
chr1    34      49
chr10   30      42
chr10   31      47
chr11   6       16
chr2    15      19
chr2    17      22
chr2    27      46
chr22   32      46

在上面这个例子中,sort命令似乎做的很完美,它按照我们给定的列对文本文件做了排序操作,但是请注意第一列,对于sort来说,chr11在ASCII编码的角度下显然是小于chr2的(因为在第四位的比较上1小于2),但是这显然不符合人的一般认知,我们都会把chr11chr2中的112当做数字来看待。

如果我们启用-V参数,sort可以更加智能的识别字符串中的数字。

 sort -k1,1V -k2,2n example2.bed
chr1    34      49
chr2    15      19
chr2    17      22
chr2    27      46
chr10   30      42
chr10   31      47
chr11   6       16
chr22   32      46

sort有一个独到的优点使得其对大文件的排序处理也得心应手——其采用归并排序。这种算法的一大亮点在于:当文件体积超出内存容量时,它会将已排序的中间结果暂存至磁盘进行处理,这使得有限的内存可以处理尽可能大的文本文件。

Merge Sort

归并排序是一种“分而治之”的排序算法,其将待排序的对象分为子问题,再将子问题作为父问题做进一步分解,直至问题可以直接被解决(比如两个数字之间的比较),然后一步步将无数子问题的结果合并为各自父问题的结果,最终推回原本的问题的结果。

这种算法天生适合递归方式,因为其具有可分解为相似的子问题的特质。这种算法同样也利好于问题的简单化,这意味着我们不必“一口气吃成胖子”,如果问题(比如未排序的文件)太大,我们可以选取其的子问题来解决(子问题太大,那就继续分解),最后逐步将已经解决的问题拼起来。

但需注意的是,对磁盘的读写速度远远慢于对内存的读写速度(数量级上的差异),如果sort一次性可以支配的内存过小,频繁的IO操作将会是工具的性能瓶颈,幸运的是,-S允许我们设置sort的内存缓冲区大小。

 sort -k1,1 -k4,4n -S4G Mus_musculus.GRCm38.75_chr1_random.gtf

-S参数很聪明,它能理解K、M和G等字节单位,同样支持使用百分比(比如-S50%代表使用50%的内存)。

同样的,如果允许sort调用更多CPU核心并行处理(毕竟有很多个子问题需要处理),同样可以显著提升文件排序的速度,这可以通过-parallel实现。

 sort -k1,1 -k4,4n --parallel 4 Mus_musculus.GRCm38.75_chr1_random.gtf

Note