声明:本文为学习了"shell脚本学习指南"一书之后的学习手记.
p { margin-bottom: 0.21cm; }td p { margin-bottom: 0cm; }a:link { }
问题: 实现一个文本处理程序,找出 n 个出现最频繁的单词,将单词打印出来,并在输出结果的列表上加入它们出现的次数,按照由大到小排序。
下面给出一个给出完成这个任务在 *NIX 上可以进行的一些学习活动 .
从 *nix 中学习 *nix 。( *nix 代指各类 unix )(下面的方法最初来自 <<unix/linux编程实践教程>>)
方法: 1. 阅读联机帮助。
2. 搜索联机帮助
3. 阅读 .h 文件。
4 从参阅文件( SEE ALSO )得到启示
联机文档的使用如下 :
也就是 man 下 man 命令 . 结果如下 :
banxi1988@banxi1988-desktop:~$ man man
得到帮助信息如下图。
图 1-1 man man 命令信息。
使用 man 命令的时候,你应该还要知道 man 命令信息页的一些信息。如下:
由于手册页( man page )是用 less 程序来看的(可以方便地使屏幕上翻和下翻),所以在手册页( man page )里可以使用 less 的所有选项。手册页 man page 在很少的空间里提供了很多的信息。
在 less 中比较重要的功能键有:
[q] 退出;
[Enter] 一行行地下翻;
[Space] 一页页地下翻;
[b] 上翻一页;
[/] 后跟一个字符串和 [Enter] 来查找字符串;
[n] 发现上一次查找的下一个匹配。
man 命令中有一个参数很有用如下 :
-K, --global-apropos search for text in all pages
根据题目的功能要求 , 下面对一单词统计功能命令查找搜索如下 :
banxi1988@banxi1988-desktop:~$ man -k count word
原始搜索出来的信息是比较多的。但是我们上面介绍了手册页的各章节的命令的说明。
我们作为这些基本文本要求的命令,应该 是用户命令在手册页的第一章节。
所以重点考虑第一章节的,把第一章节的找出来比较相符的如上。
下面给出 man 手册页各章节的内容的一些规律 .
章节 | 描 述 |
1 | 用户命令的使用方法,可以使用的参数等 |
2 | 系统调用只有系统才能执行的函数 |
3 | 库调用大多是 libc 函数,如 qsort ( 3 ) |
4 | 设备和特殊文件 |
5 | 文件格式和约定,比如 /etc/passwd 及其他可读文件 |
6 | 游戏 |
7 | 宏命令包和约定 |
8 | 系统管理命令,多数只有 root 可以执行 |
9 | 内核程序 |
根据手册页的说明找出如下:
cksum (1) - checksum and count the bytes in a file
sum (1) - checksum and count the blocks in a file
wc (1) - print newline, word, and byte counts for each file
gnome-dictionary (1) - Look up words on dictionaries
因为看起来 wc 命令比较符合 . 所以再 help 一下 wc 命令的使用 . 如下 :
banxi1988@banxi1988-desktop:~$ wc --help
用法: wc [ 选项 ]... [ 文件 ]...
或: wc [ 选项 ]... --files0-from=F
输出每个指定文件的行数、单词计数和字节数,如果指定了
多于一个文件,继续给出所有相关数据的总计。如果没有指定
文件,或者文件为 "-" ,则从标准输入读取数据。
-c, --bytes 输出字节数统计
-m, --chars 输出字符数统计
-l, --lines 输出行数统计
--files0-from= 文件 从指定文件读取以 NUL 终止的名称,如果该文件被
指定为 "-" 则从标准输入读文件名
-L, --max-line-length 显示最长行的长度
-w, --words 显示单词计数
--help 显示此帮助信息并退出
--version 显示版本信息并退出
请向 bug-coreutils@gnu.org 报告 wc 的错误
GNU coreutils 项目主页: <http://www.gnu.org/software/coreutils/>
GNU 软件一般性帮助: <http://www.gnu.org/gethelp/>
请向 <http://translationproject.org/team/zh_CN.html> 报告 wc 的翻译错误
banxi1988@banxi1988-desktop:~$
虽然我们现在学习到了很多了,但是上面的只是统计出来了单词数。
但是我们要的是每个单词的单词数统计结果。
现在到了我们发挥想法的时候了,如果这是一个 C 语言的题,我们应该如何解决。
首先我们可能会有一个数组应该是结构数组。假设总共有 N 个不同的单词,分别统计每个单词的个数。
所以需要扫描 N 遍吧。然后到数组进行排序。将想要的单词级数据输出。
暂时不考虑其它的,因为过早优化是万恶之源。我们先过搜索下,有没有现成的工具能够做这些事情 。
首先一个问题是就是单词的匹配问题,在 C 语言我们可以使用 strcmp 。看下在 unix 系统下我们可以用什么。
banxi1988@banxi1988-desktop:~$ man -k match string words
(在结果中发现了很多第三章节的,可见 GNU/LINUX 的编程还是很方便的,有众多的功能可用的函数。)把匹配的并且是第章节的列出如下:
egrep (1) - print lines matching a pattern
fgrep (1) - print lines matching a pattern
git-grep (1) - Print lines matching a pattern
grep (1) - print lines matching a pattern
rgrep (1) - print lines matching a pattern
上面的东西都有一个共同的点就是都有 grep 。百度之,或者 Google 之。
Grep 的功能就知道了。
下面简单说明下: Unix 实用工具来源于 ed 中的一个全局命令。
g/re/p
其中 g 代表 global 。 Re 代表正则表达式。
下面我们来看下系统的帮助文档是怎么说明的!
banxi1988@banxi1988-desktop:~$ grep --help
用法 : grep [ 选项 ]... 模式 [ 文件 ]...
在每个文件中查找样式或标准输入。
PATTERN 默认的是一个基本的正则表达式 (BRE) 。
例: grep -i 'hello world' menu.h main.c
正则表达式的选择和解释:
-E, --extended-regexp PATTERN 是一个扩展的正则表达式 (ERE)
-F, --fixed-strings PATTERN 是一套新行分离修复字符串
-G, --basic-regexp PATTERN 是一个基本的正则表达式 (BRE)
-P, --perl-regexp PATTERN 是一个 Perl 正则表达式
-e, --regexp=PATTERN 使用 PATTERN 来匹配
-f, --file=FILE 从 FILE 来获得 PATTERN
-i, --ignore-case 忽略大小写
-w, --word-regexp 强制 PATTERN 仅匹配整个词
-x, --line-regexp 强制 PATTERN 仅匹配整行
-z, --null-data 结尾为 0 字节而不是新行符的数据行
杂项:
-s,--no-messages 不显示错误信息
-v,--invert-match 选择不匹配的行
-V,--version 打印版本信息并退出
--help 显示本帮助并退出
--mmap 如果可能,使用内存映象作为输入
输出控制:
-m, --max-count=NUM 在有 NUM 个匹配后停止
-b, --byte-offset 在输出行的同时打印字节位移
-n, --line-number 在输出行的同时打印行数
--line-buffered flush output on every line
-H, --with-filename print the filename for each match
-h, --no-filename suppress the prefixing filename on output
--label=LABEL print LABEL as filename for standard input
-o, --only-matching show only the part of a line matching PATTERN
-q, --quiet, --silent suppress all normal output
--binary-files=TYPE assume that binary files are TYPE;
TYPE is `binary', `text', or `without-match'
-a, --text 等同于 --binary-files=text
-I 等同于 --binary-files=without-match
-d, --directories=ACTION how to handle directories;
ACTION is `read', `recurse', or `skip'
-D, --devices=ACTION how to handle devices, FIFOs and sockets;
ACTION is `read' or `skip'
-R, -r, --recursive 等同于 --directories=recurse
--include=FILE_PATTERN 只搜索符合 FILE_PATTERN 型式的文件
--exclude=FILE_PATTERN 跳过名字为 FILE_PATTERN 的文件或目录
--exclude-from=FILE skip files matching any file pattern from FILE
--exclude-dir=PATTERN directories that match PATTERN will be skipped.
-L, --files-without-match print only names of FILEs containing no match
-l, --files-with-matches print only names of FILEs containing matches
-c, --count print only a count of matching lines per FILE
-T, --initial-tab make tabs line up (if needed)
-Z, --null FILE 名字后打印 0 字节
上下文控制 :
-B, --before-context=NUM 打印 NUM 行上文
-A, --after-context=NUM 打印 NUM 行下文
-C, --context=NUM 打印 NUM 行输出上下文
-NUM 与 --context=NUM 相同
--color[=WHEN],
--colour[=WHEN] 使用标记来高亮匹配的字符串;
WHEN 可取值为“ always” ,“ never” 或“ auto”
-U, --binary 不去掉 EOL(MSDOS) 处的 CR 字符
-u, --unix-byte-offsets 如果 CR 不在那里 (MSDOS) ,报告偏移值
‘egrep’ 与 ‘ grep -E’ 含义一致。‘ fgrep’ 与 ‘ grep -F’ 的含义一致。
直接调用‘ egrep’ 和‘ fgrep’ 的方式已经被废弃了。
没有 FILE 或者 FILE 是 - ,读取标准输入。如果少于两个 FILE ,
假设 -h 。如果任意行被选中,退出状态为 0 ;
如果出现任何错误并且 -q 未被给出,退出状态为 2 。
汇报错误到: bug-grep@gnu.org
GNU Grep 主页: <http://www.gnu.org/software/grep/>
GNU 软件一般性帮助: < http://www.gnu.org/gethelp/ >
上面的有几个选项正符合我们的意思。如下:
-i, --ignore-case 忽略大小写
-w, --word-regexp 强制 PATTERN 仅匹配整个词
-c, --count print only a count of matching lines per FILE
我们用我们的测试文本( testcat.txt )来测试一下。
banxi1988@banxi1988-desktop:~$ cat testcat.txt
this is a file to test cat command.
for the purpose is write by banxi1988
at 2010-01-16
banxi1988@banxi1988-desktop:~$
我们先来测试下,用 grep 来查找 is 这个单词吧。
如下
banxi1988@banxi1988-desktop:~$ grep is testcat.txt
th is is a file to test cat command.
for the purpose is write by banxi1988
banxi1988@banxi1988-desktop:~$
grep 将匹配的都变成了红色,而且匹配了单词的一部分。但是我们要匹配整个单词。
所以用上面说明的选项。
如下:
banxi1988@banxi1988-desktop:~$ grep -w -i is testcat.txt
this is a file to test cat command.
for the purpose is write by banxi1988
banxi1988@banxi1988-desktop:~$
现在好了,但是呢,没有打印出匹配的个数。只是表明在里面匹配有。
查看选项测试之如下:
在输出控制选项下找出下面这样一个。
-o, --only-matching show only the part of a line matching PATTERN
banxi1988@banxi1988-desktop:~$ grep -w -o -i is testcat.txt
is
is
banxi1988@banxi1988-desktop:~$
我们又进 了一步了。但是还得打出多少来呢。
再次测试之。
banxi1988@banxi1988-desktop:~$ grep -w -o -n -i 'is' testcat.txt
1:is
2:is
banxi1988@banxi1988-desktop:~$
上面加了选项 n ,表示输出行数。注意到没有我加了引号给单词。但结果是一样的,因为 grep 能够查找字符串,所以加上引号自己好看些。
输出控制的另外一个选项,测试之:
-c, --count print only a count of matching lines per FILE
banxi1988@banxi1988-desktop:~$ grep -w -o -c -i 'is' testcat.txt
2
banxi1988@banxi1988-desktop:~$
但是这个选项是打印出匹配的行数,但是我一行就有几个单词呢?会怎么样?
把 testcat.txt 改成下面的后测试 之。
banxi1988@banxi1988-desktop:~$ cat testcat.txt
this is a file to test cat command. and this is is a good day?
for the purpose is write by banxi1988
at 2010-01-16
banxi1988@banxi1988-desktop:~$ grep -w -o -c -i 'is' testcat.txt
2
banxi1988@banxi1988-desktop:~$
它还是打印出现行,所以不行啦。。
banxi1988@banxi1988-desktop:~$ grep -w -o -i is testcat.txt
is
is
banxi1988@banxi1988-desktop:~$
现在我们倒回去一点,我们对每一个不同的单词这样来一遍。再来计算它的行数,然后再比较,虽然这个方法比较笨但还是比较可行的。然后但是上面的是我们自己一个一个的来找单词的。然后听说 unix 的批处理很强大,所以我们还是再想想。再多加一层。
首先我们把应该让单词作为文件输出还不是我们自己查看文件之后再找出来,所以我们得让它自动查找出每个单词然后计算次数。但是又有一个问题,就是,单词的重复计数,因为单词在前面出现放进行到后面就又会出现,然后再计算。
所以我们要找出另外一种方案来,其中之一就是,将已经遇到过的单词把它从文件中删除。
makestrs (1) - makes string table C source and header(s)
replace (1) - a string-replacement utility
找到两个看似有用的列出如上。查阅之,结果发现都不是我们想要的。于是我们还是回头吧。
上面一开始就有说明查看 man 手册页时从 SEE ALSO 中找到灵感。现在我们就来试下吧。
man grep
的 see also 如下:
SEE ALSO
Regular Manual Pages
awk(1), cmp(1), diff(1), find(1), gzip(1), perl(1), sed(1), sort(1),
xargs(1), zgrep(1), mmap(2), read(2), pcre(3), pcrepattern(3),
terminfo(5), glob(7), regex(7).
然后再查阅之。首先一个是 awk 。
好跟我们的实验要求还有点联系,我们来 help 之。
由于页数的原因下面的过程不再重复 .
通过反复的 help man info 等帮助命令 , 得到解决方案 ( 测试文档也一起 ) 如下 :
#! /bin/sh
# 统计文章中的
# 用在在英文文章中单词频率程序
# 参数 1 文章的文件名 . 参数 2 显示排在前 N 位单词 .
# 程序名 .wf.sh
# 用法 :
#banxi1988@banxi1988-desktop:~$ chmod u+x wf.sh
#banxi1988@banxi1988-desktop:~$ ls -l wf.sh
#-rwxr--r-- 1 banxi1988 banxi1988 1433 2011-01-17 22:35 wf.sh
#banxi1988@banxi1988-desktop:~$ ./wf.sh testcat.txt 10
#
cat "$1" |
tr -s '[:punct:][:blank:]' '[/n*]' | # 将标点及空格替换为换行符 . -s 选项意思是 -s,
#--squeeze-repe 如果匹配于 SET1 的字符在输入序列中存在连续的重复,
# 在替换时会被统一缩为一个字符的长度
tr '[:upper:]' '[:lower:]' | # 将大写转换为小写 . 此时下面的 uniq 的 -i 选项可以不用 .
sort | # 使用默认的选项进行排序 .
uniq -c -i | # -c, --count 在每行前加上表示相应行目出现次数的前缀编号 -i 忽略大小写
sort -n -k 1 -r | # -n -n, --numeric-sort 根据字符串数值比较
# -k, --key= 位置 1[, 位置 2] 在位置 1 开始一个 key ,在位置 2 终止 ( 默认为行尾 )
# -r, --reverse 逆序输出排序结果
head -n "$2" # head -n, --lines=[-]N 显示每个文件的前 N 行内容;
# 结果测试如下 :
#banxi1988@banxi1988-desktop:~$ cat testcat.txt
# this is a file to test cat command. and this is is a good day?
#for the purpose is write by banxi1988
#at 2010-01-16
#banxi1988@banxi1988-desktop:~$ ./wf.sh testcat.txt 5
# 4 is
# 2 this
# 2 a
# 1 write
# 1 to
#banxi1988@banxi1988-desktop:~$ ./wf.sh testcat.txt 1000
# 4 is
# 2 this
# 2 a
# 1 write
# 1 to
# 1 the
# 1 test
# 1 purpose
# 1 good
# 1 for
# 1 file
# 1 day
# 1 command
# 1 cat
# 1 by
# 1 banxi1988
# 1 at
# 1 and
# 1 2010
# 1 16
# 1 01
# 1
#banxi1988@banxi1988-desktop:~$
下面 help 之上面的关键主角 .tr tr 的手册页有丰富的帮助信息 , 下面只给出它的 help 信息 .
banxi1988@banxi1988-desktop:~$ tr --help
用法: tr [ 选项 ]... SET1 [SET2]
从标准输入中替换、缩减和 / 或删除字符,并将结果写到标准输出。
-c, -C, --complement 首先补足 SET1
-d, --delete 删除匹配 SET1 的内容,并不作替换
-s, --squeeze-repeats 如果匹配于 SET1 的字符在输入序列中存在连续的
重复,在替换时会被统一缩为一个字符的长度
-t, --truncate-set1 先将 SET1 的长度截为和 SET2 相等
--help 显示此帮助信息并退出
--version 显示版本信息并退出
SET 是一组字符串,一般都可按照字面含义理解。解析序列如下:
/NNN 八进制值为 NNN 的字符 (1 至 3 个数位 )
// 反斜杠
/a 终端鸣响
/b 退格
/f 换页
/n 换行
/r 回车
/t 水平制表符
/v 垂直制表符
字符 1- 字符 2 从字符 1 到字符 2 的升序递增过程中经历的所有字符
[ 字符 *] 在 SET2 中适用,指定字符会被连续复制直到吻合设置 1 的长度
[ 字符 * 次数 ] 对字符执行指定次数的复制,若次数以 0 开头则被视为八进制数
[:alnum:] 所有的字母和数字
[:alpha:] 所有的字母
[:blank:] 所有呈水平排列的空白字符
[:cntrl:] 所有的控制字符
[:digit:] 所有的数字
[:graph:] 所有的可打印字符,不包括空格
[:lower:] 所有的小写字母
[:print:] 所有的可打印字符,包括空格
[:punct:] 所有的标点字符
[:space:] 所有呈水平或垂直排列的空白字符
[:upper:] 所有的大写字母
[:xdigit:] 所有的十六进制数
[= 字符 =] 所有和指定字符相等的字符
仅在 SET1 和 SET2 都给出,同时没有 -d 选项的时候才会进行替换。
仅在替换时才可能用到 -t 选项。如果需要 SET2 将被通过在末尾添加原来的末字符的方式
补充到同 SET1 等长。 SET2 中多余的字符将被省略。只有 [:lower:] 和 [:upper:]
以升序展开字符;在用于替换时的 SET2 中以成对表示大小写转换。 -s 作用于 SET1 ,既不
替换也不删除,否则在替换或展开后使用 SET2 缩减。