碎碎念

2006, March 29

awk随机读取smiley

Filed under: awk
  • awk随机读取文件内容
    • 背景知识
    • 开始动工
    • 改良脚本
    • 最终方案
    • 基于这个例子的一些扩展应用
    • 下一篇

awk随机读取文件内容

这篇文章要求读者对正则表达式有基本的了解,同时对awk的编程模型有个概念——就是awk的工作流程。有任何批评建议都可以联系我,我的电子信箱是“hq00e @ 126 点 com”。

记得在Unix系统中有一个“smiley”命令用来随机读取表情符号。当你在shell提示符下输入`smiley‘就会得到如下的输出:

     $ smiley
     :,-D            User is laughing so much that they're crying

不过并不是每个系统都能找到这个命令。现在我们要写一个与smiley功能一样的脚本来演示GNU awk1的用法——你会发现用gawk完成这种任务竟是如此轻松!

什么是smiley faces?smiley faces就是表情符号如我们经常见的`: )‘就是一个smiley face。当然类似的符号还有很多,比如:`:-(‘或`8-O‘。你可以到这里找到更多smiley。将你自已收集的表情符号——没有的话就从前的提供的网址中复制吧,然后保存为文件“mysmiley”。我们将写一个gawk脚本来随机的读取mysmiley文件中的某一个表情符号及其对应的解释。现在你的“mysmiley”应该是像这样,一个表情符号后可能带有多于一行的解释。不同的表情符号间用一空行分隔。

:^)             From a person with a large nose
		Messages teasing people about their noses
		3/4 view of person with elf-type sharp nose
		Broken nose
		Happy
	
:-@{#@}           From a person with braces
	
@:-)            From a person with wavy hair
		User is wearing a turban
		Smiley wearing a turban
		Indian (East/Asian)
		Elvis
		A turban
	
O:-)            Angel
……

背景知识

gawk读入数据是以记录为单位,一次读入一条。哪怎样算是一条记录呢?gawk用了一个内置的变量来设定记录与记录之间的分隔符,这个变量是RS——注意大小写。这个变量的默认值是一新行符,即如果没有指定RS的值gawk将以行为单位一次读入一行。而每一条记录又可以分为多个栏位。同样的gawk有一个内置的变量来设置栏位的分隔符——FS。这个变量的默认值是空格/制表符。

与RS和FS相关的还有两个变量分别是NR和NF分别表示当前记录在文件中的序号和当前记录的栏数2。NR是从1开始计算的。举例来说,假设某文件的第二行内容如下:

     aa bb cc	dd

则在没有改动RS和FS的情况下,当gawk读入该行时NR的值为2,NF的值为4。可以在gawk中用“$1”、“$2”、“$3”、“$4”来分别引用“aa”、“bb”、“cc”、“dd”。而用“$0”来引用“`aa bb cc dd‘”。更具体的说明见相关的手册页。

gawk提供了一个rand()函数来产生随机数序列和一个srand()函数来产生随机序列种子。记得在gawk脚本的BEGIN段使用srand()函数,这样每次使用rand()函数产生的随机数序列才会不相同。如果不明白什么是随机序列种子的话,可以先记住在rand()函数前使用srand()会让随机数“更像”随机数。

开始动工

首先要确定mysmiley文件中的条目之间是如何分隔的。我们可以看到这些表情符号后面及下面紧邻的行都属于同一条目。这一条目的范围一直到遇到空行,而空行下是另一表情符号。可以看到条目与条目之间是用空行分隔的,所以我们可以将RS3设为空行:` RS="\n\n" ‘ 。然后设置一个随机数来随机读取某条记录,下面的命令将产生一个0到400之间的随机整数:`int(400*rand())‘。剩下的就相当简单了,我们只要结合起来:

     gawk 'BEGIN{RS="\n\n";srand();recno=int(400*rand())}recno==NR{print $0}' mysmiley

……是不是比你想像中要容易呢?不过还没结束呢。对于上面的脚本还要再补充一点:我们已经产生了一个0-400之间4的随机数并存放在变量recno中。我们要将第recno条记录显示出来,即当当前的记录数NR与recno相等时打印记录:`recno==NR{print $0}‘使用print时,没有提供参数默认是打印$0,所以上面的命令可以省略为:`recno==NR{print}

技巧:有一个特殊的情况是当RS又被设定为空字串:`RS=""‘时,记录的分隔符成为了连续的一个或以上的多个空行。不过这一技巧对于FS并不适用。另外在gawk中当条件为真时如果没有提供处理语句,默认是打印当前记录。所以recno==NR也与recno==NR{print $0}是一样的。现在我们的命令可以写成:

     gawk 'BEGIN{RS="";srand();recno=int(400*rand())}recno==NR' mysmiley

我们可以随机的读取0-400条记录了。但还有几个问题:

  • 首先,没有第0条记录。
  • 其次,mysmiley文件中可能有不只或不到400条记录。

我们再看看怎么解决这些问题?

改良脚本

理想的随机数的范围应在1到总的记录数之间。看来我们得先得到总的记录数。通过shell有多种方法可以做到这一点,但我们还是用gawk:

     gawk -vRS="" 'END{print NR}' mysmiley

这条命令打印最一一条记录的序号。也就是mysmiley文件中的记录数。下面是完整的shell脚本:

     #!usr/bin/sh
     totalRecords = `gawk -vRS="" 'END{print NR}' mysmiley`
     gawk -vtr="$totalRecords" '
      BEGIN{
        RS=""
        srand()
        recno=int(tr*rand()+1)
       }
      recno==NR' mysmiley

不过我们还是寻求更纯粹的gawk方案。在上面的方法中我们通过在shell使用一条gawk命令来得到mysmiley的记录数,有没有办法不使用shell而直接在gawk得到mysmiley文件的记录数并显示相应的记录呢?照原来的思路看来是不行的,因为当我们得到记录总数——最后一条记录的NR时程序已经执行快完了5。解决的办法是将每一条记录以记录的序号为下标存在数组中,然后在END段中(此时我们已得到了总的记录数),产生一个随机数。以这个随机数做为数组的下标读取数组。但我们将不示范这种方法,读者可以自已试一试这种使用数组的方法。这里要使用的是另一种方法。

最终方案

gawk一次读入一条记录所以所以得等到读入最后一条记录时才能得到总的记录数。而栏位数是一读入一条记录就得到了总的栏位数即NF。如果我们将mysmiley整个文件视作一条记录6而将里面的每一条目视为一栏,那我们就可以通过NF得到总的条目数并随机地显示某栏(即条目)的内容。显然我们只需要重新地设定RS和FS就完成了一半了。

为了将整个文件当成一条记录,可以将RS设置为文件中不存在的字串。如果这个mysmiley文件的内容是都是英文的我们可以设置RS="你好吗?",或是随便什么中文句子,来“强迫”gawk一次读入整个文件。但比较常用的是使用“\0”7,来达到这一目的。至于FS通过观察我们可以看到上面的mysmiley文件中不同的条目间有一空行,所以我们可以用“\n\n”分隔栏,但因空行中可能有空白字行如制表符或空格符所以我们将之改为“\n[ \t]*\n”。下面是成品:

     #!usr/bin/sh
     gawk 'BEGIN{
            RS="\0"
            FS="\n[ \t]*\n"
            srand()
           }
          { print $int(NF*rand()+1) }' mysmiley

最后再解释一下“$int(NF*rand()+1)”。先是NF*rand(),将栏位数乘以一个0到1之间的随机数,得到了一个大于等于0小于NF的随机数。再加上“1”我们得到了一个大于等于1小于NF+1的随机数。用int()函数对这个数取整,我们得到了大于等于1小于等于NF的随机数。我们在前面加上“$”号得到相应栏位的值。假设“int(NF*rand()+1)”得到的值是“3”,那“$int(NF*rand()+1)”就等同于“$3”。而“print $3”将把第三栏的内容,即mysmiley文件中的第三条表情符号及相应的解释,显示出来。

现在我们知道脚本运行的原理了。将脚本保存为“readsmiley”,并与mysmiley文件放到同一目录中。为readsmiley增加执行权限,就可以运行了:

     ./readsmiley

如果你用的是Windows的话,先确保gawk在搜索路径中然后将脚本改成单行的形式:

     gawk "BEGIN{RS=\"\0\";FS=\"\n[ \t]*\n\";srand()}{print $int(NF*rand()+1)}" mysmiley

保存为readsmiley.bat并与mysmiley文件放在同一目录中。运行readsmiley.bat就行了。如果你用双击的方式运行这个批处理文件你可能会发现一个黑色窗口一闪而过。让窗口留片刻的方法是在这人批处理文件的末尾加上`pause‘命令8

基于这个例子的一些扩展应用

这个例子主要演示了FS和NS的用法。我们完全可以将之扩展到其他方面的应用。比如随机显示通讯录中的某条记录;显示某条笑话;随机地显示英文单词及它的中文解释等等。通常情况下我们只要相应地修改FS和NS就能应用于这些方面了。

我们以Best of Vim Tips为例,看如何改脚本来适应新的需要。Vim Tips文件中有许多有用的Vim使用技巧,通过修改脚本我们让它每次运行时显示一条技巧。先看一下该文件的结构:

" Combining g// with normal mode commands
:g/|/norm 2f|r*                      : replace 2nd | with a star
"send output of previous global command to a new window
:nmap <F3>  :redir @@a<CR>:g//<CR>:redir END<CR>:new<CR>:put! a<CR><CR>
----------------------------------------
" Global combined with substitute (power editing)
:'a,'bg/fred/s/joe/susan/gic :  can use memory to extend matching
:g/fred/,/joe/s/fred/joe/gic :  non-line based (ultra)
----------------------------------------
" Find fred before beginning search for joe
:/fred/;/joe/-2,/sid/+3s/sally/alley/gIC
----------------------------------------
" create a new file for each line of file eg 1.txt,2.txt,3,txt etc
:g/^/exe ".w ".line(".").".txt"
----------------------------------------
……

可以见到每条tips都是以“" ”开始的,将FS改为“\n\"”就行了:

     gawk 'BEGIN{
            RS="\0"
            FS="\n\""
            srand()
           }
           {print "\"" $int(NF*rand()+1)}' vimtips

现在我们只要运行这个脚本就可以随机显示一条Vim提示了。你可以在系统中设置让这条命令在登录时自动运行,或是做成shell的提示符。

你还以用同样的技巧做许多的事。当然,还能怎么用就看你的创意了。

下一篇

使用过smiley命令的人应该知道,这条命令还有带参数的用法。如输入`smiley ':-)'‘会得到对表情符号`:-)‘的相关解释。我会在下一篇中说一说在gawk中使用参数的方法。同时我们还将一起写一个能查字典的脚本。下一篇再见……

PS:在以后的文章中,内容会逐渐涉及gawk的各个方面。我们近期可能会讲到的有getline、gawk中的管道、gensub、一些内置变量如FILENAME等。有兴趣的读者不防先看一看这方面的有关资料。另外包括awk、sed在内的许多Unxi工具的使用都相当依赖于用户对正则表达式的掌握程度,所以要使用awk或其他Unix工具(许多Unix工具也有Windows版本),一定要有意识地学习和掌握正则表达式的用法。


Footnotes

[1] 虽然是用gawk但这里面的一些方法并非只有gawk能使用。但因为可能使用到一些GNU扩展,而我不想时刻地注意着哪些是GNU扩展哪些不是。所以下面一概以Gawk代替awk——即使有些并没有用到GNU扩展。
[2] 准确地说NR表示的是在已读入的所有记录中的序号。对应的还有一个FNR表示的是当前记录在当前文件中的序号。如果只有一个输入文件则这两个变量的值是一样的。在本文中将只考虑一个输入文件的情况。
[3] RS是记录分隔符,设置RS可以让gawk知道我们是以什么为标准来分隔不同的记录的
[4] 不包括400
[5] 这与gawk的运行模式有关,gawk逐条读入记录并且读入一条记录时上面的一条记录就被舍弃。当gawk读到第五条记录时你没办法让它再对第三条记录进行操作。除非你将先前读入的记录存在数组或变量中
[6] 注意:将整篇个文件当成一条记录读入,会占用较多的内存。所以如果系统资源紧张或者读入的文件太大的话则不适合用这种方式。
[7] 在一些其他版本的awk中可能没法使用这一方法。另外字过c语言的应该都会知道“\0”就是用来表示字串结束的数字0。
[8] 如果你用的是Win2000以上版本,你也可以使用管道让随机读取的条目显示在消息框中。方法是:`gawk "BEGIN{RS=\"\0\";FS=\"\n[ \t]*\n\";srand()}{print $int(NF*rand()+1)}" mysmiley | msg %username%‘。

Comments »

The URI to TrackBack this entry is: http://blah.blogsome.com/2006/03/29/awk-smiley/trackback/

No comments yet.

RSS feed for comments on this post.

Leave a comment

Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>



请输入验证码。

Get free blog up and running in minutes with Blogsome
Theme designed by Jay of onefinejay.com