档案存取的Perl档案存取

如题所述

第1个回答  2016-06-04

档案系统在写程式时是非常重要的一个部份,尤其对于Perl的使用者来说,因为Perl能够处理大量而且复杂的资料,所以经常被拿来作为Unix作业系统的管理工具,尤其对于Unix-like系统管理员而言,在进行系统日志的管理时,存取档案,读取档案内容并加以分析就是最基本的部份。当然,你还可能进行目录的修改,档案权限的维护等等跟系统有密切关系的操作。 当你的Perl想要透过作业系统进行档案存取时,可以利用档案代号取得和档案间的系结,接下来的操作就是透过这个档案代号和实体的档案间进行沟通。也就是说,我们要进行档案操作时,可以先定义相对应实体档案的代号,以便我们用更简便的方式对档案进行存取。
而所谓的档案代号其实就是由使用者自行命名,并且用来跟实体档案进行连结的名称,他的命名规则还是依循Perl的命名规则,大家对于这个规则应该相当熟悉了,不过我们还是再次提醒一下:可以数字,字母及底线组成,但是不能以数字作为开始。而且一般来说,我们几乎都习惯以全部大写来作为档案代号,因为档案代号并不像其他变数,会使用某些符号作为识别,所以几乎约定成俗的全部大写习惯也是有存在的道理。
当然,你也可以依照自己的习惯来为档案代号命名(注一),这表示所谓的全部大写绝对不是一种铁律,就像Perl程式语言本身,希望以最少的限制来进行程式设计的工作。 对于档案的输出,输入而言,其实就跟平常时候,你利用Perl在进行其他的操作非常接近,有时候只是输出到不同的媒介上。所以Perl其实已经预定了几种档案代号,让你不需要每次写Perl的程式就必须去重新定义这些代号,很显然的,几乎大部份的程式都会需要这些档案代号。
这六个预设的档案代号分别是:STDIN,STDOUT,STDERR,DATA,ARGV,ARGOUT,看起来相当熟悉吧?没错,因为很多时候,我们其实就是靠这些预设的档案代号在进行程式的输出,输入。只是我们还没有了解这些其实就是档案代号。换个角度来看,其实即使我们都不知道他们是预设的档案代号,我们就能运作自如,那么对于档案代号的使用显然就不是太难。不过,我们还是要再来看看这六个Perl预设的档案代号。其中有些我们已经使用过了,我们就先对其中几个预设的档案代号来进行介绍:
STDIN:这也就是我们常看到的“标准输入装置”,当Perl开始执行时,它预设接受的外部资讯就是从这里而来。就像我们之前曾经看过的写法:
my $input = <STDIN>; # 从标准输入装置取得资料
print $input;
这时候,当我们从键盘输入时,Perl就可以正确的取得资讯,并且透过STDIN取得使用者用键盘打入的一行字串。因此他的运作方式就是以一个档案代号来进行。当然,你可以透过系统函式库的配合,让你的标准输入转为其他设备之后你就进行其他运用,不过这显然不是这里的主题,还是让我们言归正传。对于Perl来说,他从档案系统读入资料是以行为单位,因此即使是利用STDIN,Perl还是会等到使用者键入换行键时才会有所动作。
STDOUT:相对于标准输入,这就是所谓的标准输出,也就是在正常状况下,你希望Perl输出的结果就是透过STDOUT来进行输出的。而一般来说,我们所使用的就是荧幕输出。你可以看看这个程式里的写法:
my $output = 标准输出;
print $output\n;
print STDOUT $output\n;
没错,就像我们所预期的,Perl透过荧幕印出了两行一模一样的结果,也就是印了两行“标准输出”。原因非常简单,因为当我们使用print的指令时,Perl会使用STDOUT当作预设的档案代号,所以一般状况下,如果我们没有指定档案代号时,Perl就会自动输出到STDOUT。所以事实上,我们早就开始使用档案代号了,只是我们自己并没有发觉。或说,Perl原来的期望就是希望使用者都可以在最没有负担的状况下任意输出到荧幕,或从键盘输入,毕竟Perl程式设计师那么的怕麻烦,一般的键盘输入,荧幕输出又是使用的那么频繁,当然要让程式设计师以最简单的方式达成。而且非常显然,这个目的也算达到了。
STDERR:标准的错误串流,也就是程式错误的标准输出。正常而言,当程式发生错误时,程式可以发出错误讯息来通知使用者,这时候这些错误讯息也能透过档案代号处理,把这些讯息丢进错误讯息串流。不过这样说实在不太容易理解,那我们来玩个游戏吧:
my $output = 标准输出;
print $output\n;
print STDERR $output\n;
我们一开始定义了一个字串$output,一开始我们先直接从标准输出印出这个字串,接下来我们便要求Perl把这个字串送出到错误串流中。这样会发生甚么有趣的事呢?让我们来看看:
[hcchien@Apple]% perl
标准输出
标准输出
[hcchien@Apple]% perl s> error.txt
标准输出
第一次,我们直接执行了这支程式,而结果显然有点平淡无奇。于是我们第二次执行时,就在后面加上了>error.txt,对于熟悉Unix操作的人大概知道,这样的方式其实是把程式执行时的错误讯息导向档案error.txt了。所以STDOUT只输出了第一行的print结果,而系统也产生了另外的error.txt的档案,因为我们把标准错误串流送到了这个档案里,所以我们可以发现档案里正好有我们输出到标准错误串流的字串。这样的作法对于可能把Perl拿来进行系统管理的脚本程式时,就可以发挥很大的功能。因为我们也许希望某个程式可以帮我们进行一些日常的琐事,而在处理这些琐事的同时,如果发生甚么异常状况,可以把错误讯息存在某个档案中,这样一来我们就可以只检查这个日志档案。
ARGV:我们可以直接利用参数来读取某些档案的内容,使用者只需要在执行程式时,在程式后加上档案名称作为参数,然后在程式中我们就可以直接读到档案的内容了。还是用个例子比较容易理解:
my $input = <ARGV>;
print $input\n;
于是我们试着执行它,并且加上参数error.txt
[hcchien@Apple]% perl > error.txt
标准输出
没错,当我们用了刚刚得到的error.txt当参数时,程式里面直接使用预设档案代号ARGV来读取档案内容,所以当我们印出来时,就可以看到刚刚写入档案的内容了。不过由于Perl读档案的性质,其实我们只印出了档案内的第一行,不过这部份我们稍后会再提到,这里暂且略过不谈。
不过Perl的ARGV其实非常好用,让我们来看看使用阵列形式的@ARGV。也就是程式的参数,跟我们曾经提过的副常式参数有几分相似。它也是把取得的参数放入阵列中,然后在程式里,就可以直接叫用阵列,取出参数,就像这样:
my $input = shift @ARGV;
print $input\n;
我们用同样的方式执行,可以看到这样的结果:
[hcchien@Apple]% perl error.txt
error.txt
另外,我们也可以对ARGV进行一般档案代号的操作方式,不过这些将在稍后提到档案操作时再来讨论。 我们刚刚提到了一些Perl预设的档案代号,这些档案代号都是由Perl自动产生的。因此当我们开始执行Perl的程式时,就可以直接使用这些档案代号。可是除此之外,当我们希望自己来对某些档案进行存取时,就必须手动控制某些程序。所以现在应该来关心一下,当我们要手动进行这些档案的控管时,应该怎么做呢?
10.3.1 开档/关档
最基本的,我们要先开启一个档案,也就是我们必须将档案代号和我们想要存取的档案接上线。首先,我们可以使用open这个指令来开启档案代号,并且指定这个档案代号所对应的档案名称,所以我们使用的指令应该会会这样:
open FILE,file.txt;
open OUTPUT,<output.txt; # 从档案输出
open INPUT,>input.txt; # 输入到档案
open append,>>append.txt; # 附加在现有档案结尾
其实要开起一个档案代号非常的容易,至少从上面的例子来看,应该还算是非常的平易近人。那么我们只需要稍微的解释一些特殊的部份,大部份的人应该就可以轻松的开始使用档案代号了。
首先,最基本的语法也就是利用open这个指令来结合档案代号跟系统上实际的档案。所以我们看到了所有的叙述都是以open接下档案代号,接着是档案的名称。这样一来,我们就把档案代号跟档案名称连接起来,当然,前提是没有错误发生。不过不管如何,这看起来应该非常容易了。接下来,看看在档案名称前面有一些大,小于符号,这些又是甚么意思呢?这些符号主要在于对于档案操作需求不同而产生不同的形式。首先我们看到的是一个小于(<;)符号,这个符号代表我们会从这个档案输出资料,其实如果你对Unix系统有一点熟悉,你会发现这些表示方式跟在一般使用转向的方式接近。所以当你使用小于符号时,就像把档案的资料转向到档案代号中。如果你可以想像小于符号的方向性,那么大于符号也就是同样道理了。大于符号也就是把资料从档案代号中转入实际的档案系统里,也就是写入到某个档案中。而如果系统中没有这个档案,Perl会细心的帮你建立这个档案,然后你透过档案代号送出的资料就会由Perl帮你写入档案中。不过有一个部份必须要特别注意的地方,也就是如果你透过大于符号建立的档案系结,Perl会把你指定的档案视为全新的档案,就如我们所说的,如果你的系统中没有这个档案,Perl会先帮你建立一个新的档案。不过如果你的系统本来就已经存在同样的档名,那么Perl会把原来的档名清空,然后再把资料写入。当然,这样就遇到问题了,因为如果你的程式正在监视网站伺服器,而你希望只要伺服器有状况发生就把发生的状况写入日志档。这时候你大多会希望保留旧的日志,那么如果Perl每次都清空旧的日志内容就会让我们造成困扰。这时候我们总会希望Perl能把新的状况附加在原来的档案最后面的位置,那么我们就应该使用两个大于(>>;)的符号,这也就是>>跟>的不同之处。
既然你开启了一个档案代号,最好的方式就是在你使用完后要归回原处(从小妈妈就这么告诫我们)。因此如果你不再使用某个档案代号时,你最好养成关闭这些档案代号的习惯,对了,应该还要提醒的是“适时”关闭不需要的档案代号。虽然Perl会在程式结束时自动帮你关闭所有还开着的档案代号,不过有些时候,你如果没有在档案处理完之后就尽快处理的话,恐怕会有让系统资源的负担增加。
至于关闭档案代号的方式也是非常简单,你只要使用close这个关键字,然后告诉Perl你所要关闭的档案代号,这样就没问题了。因此你如果需要关闭档案代号,你只需要这么做:
close FILE;
没错,就是这么容易。不过却也相当重要,至少你应该考虑好你自己的系统资源管理。否则等到等到持续拖累系统资源时才要怪罪Perl时可就有失公允了。另外,Perl也会在你关闭档案代号时检查缓冲区是否还存有资料,如果有的话,Perl也会先把资料写入档案,然后关闭档案。另外,档案也可能因为你的开启而导致其他人无法对它正常的操作,因此尽可能在完成档案操作后马上关闭档案代号是重要的习惯。
10.3.2 意外处理
有些时候,当我们想要开启档案时却会发现一些状况。例如我们想要从某个已经存在的档案中读入某些资料,可是却发生档案不存在,或是权限不足,而无法读入的状况。我们先看看以下的例子:
#!/usr/local/bin/perl
use strict;
open FILE,<foo.txt;
while (<FILE>) {
print $_;
}
在这里,我们希望开启一个档案foo.txt,并且从档案中读取资料,接着再把档案内容逐行印出。不过非常可惜,我们的系统中并没有这个档案。不过Perl预设并不会提醒你这样的状况,而且如果你没有使用任何的警告或中断,Perl也能安稳的执行完这个程式,当然结果是“没有结果”。可是当我们在写程式,或是使用者在跟程式进行互动时,实在难保这些时候都不会甚么错误会发生,也许只要把档案名称打错,可是Perl却不会自动的警告你。于是我们应该考虑发出一些警告,让发生错误的人可以即时修正错误。当然,你可以使用warnings来让Perl对于人为的错误发生一些警告,不过我们还有另外一种方法可以让你更轻易的掌握错误发生的状况,也就是让程式“死去(die)”。
die函式就像他的字面意思,他可以让程式停止执行,也就是让程式“死去”。因此当我们希望程式在某些状况下应该停止执行时,我们就可以使用die函式来达成。而档案发生问题的状况则是die函式经常被使用的地方。因为很多时候我们一旦开启了某个档案,大多就会把操作内容围绕着这个被开启的档案,可是如果档案其实没有被正确的开启,就很容易产生一些难以预料的问题,因此我们可以在档案开启失败时就让程式停止执行。以刚刚的程式作为例子,我们就可以把开启档案的部份写成:
open File,foo.txt or die 开启档案失败: $!;
在这里,有几个地方需要解释的,首先自然就是die的用法。我们先尝试开启foo.txt这个档案,接着用了一个逻辑运算元'or',后面接着使用die这个叙述。根据我们对or运算符的了解,程式会先尝试开启档案foo.txt,如果成功开启,就会传回1,因此or后面的叙述就会被省略。相反的,如果开启档案失败,open叙述会传回0。如此一来,Perl就会去执行or后面的叙述,因此他就会die了,也就是只执行到这里为止。
利用die结束程式的执行时,我们会希望知道程式为甚么进入die的状况,因此我们便利用die印出目前的情况。这听起来就像程式说完遗言之后就不动了。而die的列印就跟我们一般使用print没甚么不同,因此我们可以加上可以提醒程式写作者或使用者的字串。不过在刚刚的例子,我们看到了一个不寻常的变数:$!。这是Perl预设的一个变数,他会储存系统产生出来的错误讯息。因为当我们透过Perl要进行档案的存取时,其实只是透过Perl和作业系统进行沟通,因此一但Perl对作业系统的要求产生失败的状况,他便会从作业系统得到相关的错误讯息,而这个讯息也会被存入$!这个变数中。
所以如果我们执行刚刚改过的那个程式,就可以得到像这样的结果:
[hcchien@Apple]% perl
开启档案失败: No such file or directory atline 5.
因为档案不存在的原因,导致这一支Perl程式无法继续执行而在执行完die之后就停止了。而且die这个指令也在我们的要求下,传达了系统的错误讯息给我们,问题发生在你要开启档案时却没有发现这个档案或资料夹。所以利用die这个指令,你就可以在程式无法正确开启档案时,就马上中断程式,以避免不可预知的问题产生。
既然提到die,我们就顺便来谈一下die的亲戚,warn吧!当你发生一些状况,可能导致程式发生无法正常运作时,你会希望使用die来强制中断程式的执行。可是有些时候,错误也许并没有这么严重,那么你就只需要发出一些警告,让执行者知道程式出了一点问题,让他们决定是否应该中断程式吧!我门把刚刚的程式改成这样:
#!/usr/local/bin/perl
use strict;
open FILE,<foo.txt or warn open failed: $!;
while (<FILE>) {
print $_;
}
print 程式在这里结束了\n;
你应该发现了,我们把die改成了warn,然后最后加了一行列印的指令,告诉我们程式的结尾在那里。接下来我们来试着执行这支修改过的程式,你会看到这样的结果:
[hcchien@Apple]% per
open failed: No such file or directory at line 5.
the end of the script
10.3.3 读出与写入
在我们可以正确的开启档案代号之后,接下来我们就可以开始存取档案中的资料,当然最主要的就是读取,以及写入档案。
透过档案代号来读取档案内容倒是不太有甚么困难。我们大多使用钻石符号(<>;)来进行档案内容的读取。所以我们可以像这样进行档案操作:
#!/usr/local/bin/perl -w
use strict;
open LOG,/var/log/messages; # 打开这个日志档
while (<LOG>) { # 利用钻石符号读入资料
print if (/sudo/); # 符合比对的资料就列印出来
}
看起来非常容易,不是吗?
我们先用刚刚了解的方式开启了一个档案代号,并且利用这个档案代号联系到档案/var/log/messages。在一些Unix系统中也许会看到这个档案,它会纪录一些使用者登入或是使用root权限的消息。而在这个档案中,如果有使用者利用sudo这个指令进行某些操作时也会被记录下来。因此我们就可以透过这个档案知道伺服器上有些甚么状况正在发生。
接下来我们透过钻石符号开始逐行读取日志档案中的资料,透过回圈while读取档案中的资料时,while会把所读到的资料内容放进Perl的预设变数$_中,一直到档案结束,传回EOF时,回圈便会结束。因此我们就将所读取的资料进行比对,以sudo这个关键字作为比对样式,把符合的结果印出来。
这样一来,只要系统中有人使用sudo进行系统操作时,我们就可以检查出来,而且印出来的结果会像是这样:
TTY=ttyp0 ; PWD=/var/log ; USER=root ; COMMAND=/bin/rm -rf httpd-error.log
TTY=ttyp0 ; PWD=/var/log ; USER=root ; COMMAND=/bin/rm -rf httpd-access.log
TTY=ttyp0 ; PWD=/var/log ; USER=root ; COMMAND=/bin/rm -rf 192.168.1.1_access_log
192.168.1.1_error_log
TTY=ttyp0 ; PWD=/usr/home ; USER=root ; COMMAND=/bin/rm -rf interchange/
TTY=ttyp0 ; PWD=/usr/home ; USER=root ; COMMAND=/bin/rm -rf gugod/
TTY=ttyp0 ; PWD=/usr/home ; USER=root ; COMMAND=/bin/rm -rf mysql/
TTY=ttyp0 ; PWD=/ ; USER=root ; COMMAND=/bin/rm kernel.old
TTY=ttyp0 ; PWD=/ ; USER=root ; COMMAND=/bin/rm -rf modules.old/
TTY=ttyp0 ; PWD=/ ; USER=root ; COMMAND=/bin/rm -rf opt/
如果你是负责管理一些Unix的伺服器,利用这样简单的方式,确实可以帮忙你完成不少工作。很显然,利用档案的操作,你还可以进行更多对日志档案的分析。例如你可以分析网站伺服器的各项资料,虽然其实已经有很多人用Perl帮你完成这样的工作了。(注二)
基本上,从档案内读取内容的方式就是这么容易,因此你可以简单的运用档案的内容进行所需要的工作。还记得我们在介绍open时的说明吗?我们有几个开启档案的方式包括了几种描述子,例如大于(>;),小于(<;),以及两个大于(>>;)。而且我们也都简单的描述过他们的差异,现在也许就是测试这些描述子的好时机,我们先来看看小于符号用于开档的时候,会有甚么影响。
我们之前也提过小于符号用在开档作为描述的话,是用来表示从档案内读取资料。那我们是不是就只能允许使用者读取资料呢?先来看看这个小小的程式吧:
open LOG,<log.txt or die $!;
while (<LOG>) {
print $_;
}
print LOG write to log or die $!;
假设我们已经有了log.txt这个档案,否则程式就会挂在中间,没办法继续执行。那么我们来看看执行结果吧:
file for log
Bad file descriptor at line 9,<LOG> line 1.
第一行就是原来log.txt里面的内容,我们可以很轻松的读出其中的资料,并且印出来,可是当我们要将资料写入时,却出现了错误讯息。没错,当初我们在开启这个档案时,只要求Perl给我们一个可以读出资料的档案,如今要求写入,果然就遭到拒绝。
看来一但我们使用了小于符号作为开启档案代号的描述子,那么我们就不能轻易的把资料写入所开启的档案中。想当然尔,Perl应该也不会让我们在开启一个利用大于符号指定为写入的档案中把资料读出吧?要想测试这样的结论,我们只需要把刚刚的程式修改一个字元,也就是把小于符号改成大于,那么就让我们来看看执行后的结果吧:
我们尝试着执行被我们修改了一个字元的程式,结果发生了甚么事呢?档案没有输出任何结果。好像很出乎意料?其实一点也不,而且正如Perl所要求我们的,我们使用了大于符号表明我们想要把资料写入档案log.txt,因此当我们想要从档案读取资料并且逐行印出结果时就无法成真。不过我们接下来去看看log.txt的内容。正如我们所预料的,程式已经正确的把字串write to log写到档案log.txt里面了。
既然使用大于符号跟小于符号都符合我们的期待,那么如果我们甚么描述子都没有使用,会是甚么样的情况呢?我们只需要使用刚刚的测试程式,并且把描述子全部取消,再来试试结果如何吧!
结果我们发现,Perl还是可以读出档案的内容,可是却无法写入。也就是跟我们使用小于符号时是一样的状况,这点其实对于经常必须使用档案的人来说其实是非常重要的。所以如果你有机会使用档案的存取时,可别忘了这一点。
另外,大于符号与两个大于的差别我们也曾经提过,这部份对于可能使用Perl来进行日常管理工作的人更是必须牢记。我们之前提过,一样是开启一个可以写入的档案,使用一个大于符号(>;)的时候,Perl会判断你是否已经有存在这个档名的档案,如果档案已经存在,那么Perl将会清空档案内容,把他视作一个新的档案来进行操作。如果在系统中档案并不存在,那么Perl就会跟系统要求开启一个新的档案。当然,在你使用两个大于符号的时候,Perl会把你要写入档案的内容以附加的方式存入。当然,如果你的系统中并没有这个档案,那么Perl也会先开启一个新档,并且把你所要求的内容写入档案中。这对于想要建立类似日志档的需求有着绝对的帮助,例如你可能会需要Perl来作为监控网路的状况,这时候你会需要每次有新状况时就把它记录下来,而且需要保留原来的纪录。那么如果你还是使用大于符号的话,你可就要小心原来的资料内容遗失了。
当然,我们知道开启档案时可以利用三种描述子去指定所要开启档案代号的状态,不过如果你甚么都没加的状况下,Perl又会作怎么样的处理呢?我们继续用刚刚的例子来进行实验吧。我们把开启档案的描述子拿掉,其他的部份一切照旧。所以你的程式就像这样:
open LOG,log.txt or die $!;
print LOG write to log\n or die $!;
接着我们发现,这样的结果就跟我们使用小于符号的效果是相同的,也就是Perl只会从档案中读出资料,却无法写入。
有了基本读写档案的能力之后,我们还必须了解该怎么样透过Perl去控制系统的档案以及资料夹。这样才能确实掌握系统的档案管理,尤其当你希望使用Perl来进行系统管理时,也就会更需要这样的能力,所以我们接下来就要讨论利用Perl对档案系统的操作。
注一:不过当你打算这么作的时候,也许要考虑这支程式未来只有你在维护,否则你这样的动作很可能会因为接下来维护的人需要花更多的时间来看懂程式而提高不少维护成本。
注二:其实跟这章主题不太有关,不过例如awstats就是这类型的工具。