那些年我用awk时踩过的坑——awk使用注意事项

因为项目经历缘由,常用awk处理一些文本数据。甚至,我特地下载了一个windows上的awk:gawk.exe,这样在windows上也能享受awk处理数据的方便性,。c++

俗话说,“常在河边走,哪能不湿鞋”,使用awk过程当中碰上过很多坑,这里稍总结一下,但愿对你们有帮助。正则表达式

1 FS问题
看看这两个awk脚本:shell

cat demo_1.txt demo_2.txt
1|2|3|4|
1|@|2|@|3|@|4|@|
awk -F '|' '{print $2}' demo_1.txt; # 脚本1
awk -F '|@|' '{print $2}' demo_2.txt; # 脚本2

脚本原目的是达到的目的是分别按'|'和分隔'|@|',输出demo.txt第二列。但实际上,第一个脚本这样写没错,但第二个脚本倒是错的。express

为何呢?windows

由于竖线在正则表达式中是一个特殊字符,表示匹配竖线左右的字符组之一。若是想使用竖线自己,须要对用转义符。数组

但为何第一个命令也同样使用了竖线却没有问题呢?数据结构

这就涉及到awk在一个规定:函数

若是FS设置了不止一个字符做为字段分隔符,将做为一个正则表达式来解释,不然直接按该字符作为分隔符对每行进行分割。编码

因此第一个命令使用了竖线作分隔符没问题,第二个命令就出错了。spa

2 正则表达式与反斜杠号问题
继续上面的问题讨论,若是demo.txt是按"|@|"作为分隔符的,要输出demo.txt第二列,正确的答案应该是怎么写呢?
答案是:

awk -F '\\|@\\|' '{print $2}' demo.txt;

注意这里,FS的值是'\\|@\\|',而不是简单的'\|@\|'(这样写会报错,提示:awk: 警告: 转义序列“\|”被看成单纯的“|”)。为啥要这样写呢?

先来看一个试验:

echo|awk -F '|@|' '{print FS}' # 脚本1
echo|awk -F '\|@\|' '{print FS}' # 脚本2
echo|awk -F '\\|@\\|' '{print FS}' # 脚本3

能够看到第一和第二个脚本,FS值是同样的。缘由是awk先要解析用户输入的字符串,并将解析结果赋值给FS,而后再调用split类函数,把FS当成函数参数传进去。

而split须要再对FS进行一次解析,编译成正则表达式。awk解析字符串给FS变量赋值时会把'\|'认为是'|',从而致使传进split函数时,分隔符已。

所以,若是想让awk正确分割记录,须要使FS='\\|@\\|',这时awk会把\\解析成转义字符'\',这样竖线就能被当普通字符处理国。


3 关联数组访问问题

曾经碰上过这样一个场景:文件a.txt包含少许用户余额(userid|amt),约100行记录,文件b.txt包含了全部用户的余额(userid|amt),约有100万行记录。

如今要求关连a.txt和b.txt(使用userid),找出在a.txt与b.txt都存在的userid,并输出其中b.amt大于a.amt的记录。

当时我先写了如下脚本:

awk -F '|' 'BEGIN{ while(getline < "a.txt") { v_user_map[$1] = $2; } }
{
    v_amt_a = v_user_map[$1];
    if ((v_amt_a != "") && v_amt_a < $2) print $0;
}

看起来逻辑彷佛没有问题,因而开始跑。可是跑起来发现效率远比本身想象的低,并且发现程序运行过程消耗的内存愈来愈多。

这明显是有问题的,理论上应该是BEGIN那段语句会消耗一些内存,以后应该就不须要再消耗才对。

因为写过c++代码,里面也有相似关联数组的数据结构,我很快猜想并实验证实缘由:v_amt_a = v_user_map[$1]; 这一句。

虽然这里没有给v_user_map[$1]赋值,可是awk会默认赋值为空,致使v_user_map数组元素愈来愈多,占用内存空间愈来愈大,查找效率愈来愈低。

知道问题就好解决了,查了一下awk帮助手册,发现能够这样写:

awk -F '|' 'BEGIN{ while(getline < "a.txt") { v_user_map[$1] = $2; } }
{
    if ($1 in v_user_map) { if (v_user_map[$1] < $2) print $0; }
}

使用in操做符来判断元素是否在关联数组里面,这样就不会有默认赋值。

4 内存限制问题

若是awk是32位程序(可使用file命令判断),那么上面的脚本1,极可能跑着跑着就core了。由于默认状况下,32位的awk最多只能消耗256M内存。

若是申请内存超过这个数就会发生异常退出。

解决方法是使用64位程序,或者修改环境变量“export LDR_CNTRL=MAXDATA=0x80000000”。(AIX4.3以上有效)

5 getline返回值问题

注意楼上的getline用法,while(getline < "a.txt")循环读取文件直到结束。这样写实际上是不太规范的,有隐患。

曾经我觉得getline读到文件尾会把$0置空,后来实践发现实际不是这样的。geline在碰上文件尾时会返回0,但$0仍是保持最后一行的记录不变。因而就改为这种写法。

不过这种写法,有时也会碰上问题,缘由:getline返回值有三种状况:1 正常读取到一条记录 0 达到文件尾 -1 文件不存在或其它错误。

若是a.txt不存在,getline会返回-1,致使死循环。我之前曾经碰上过由于这个缘由致使程序挂死,因此特别提出来让你们注意。

建议你们使用函数前最好先看看帮助文档里面关于函数描述。

6 管道问题

先来看这个脚本:

ls -1rt
demo.txt
list.txt
echo -e "\n\n" | awk '{ while("ls -1rt" | getline) { print NR " : " $0 > "list.txt";}}'

猜猜看:脚本运行完后list.txt里面的内容是什么?


答案:

cat list.txt
1 : demo.txt
1 : list.txt

相信有很多朋友会以为诧异:

有些人会认为list.txt里面应该只有一行数据,就是ls -1rt命令输出内容的最后一行。

有些人会认为应该有6条数据才对,由于ls -1rt执行了三次。

有这种想法的人,多半是不知道awk一个规定: 默认状况下同一个文件或者管道只打开一次,若是须要重复打开,须要先close。

上面的脚本因为没有显式close文件和管道,list.txt和ls -1rt都只打开/执行了一次,因此输出结果如上。

再猜猜看:下面这个脚本运行完后list.txt里面的内容是什么?

echo -e "\n\n" | awk '{ while("ls -1rt" | getline) { print NR " : " $0 > "list.txt";} close("list.txt"); close("ls -1rt");}'

7 输出单引号问题

你们知道,awk脚本通常是用单引号括起来的,形如:awk '{ print "do something"; }' 。

所以,在awk中要使用单引号是比较麻烦的事情。网上找awk输出单引号通常能够找到如下方法:

echo | awk '{ print "'\''"; }' 

不少人所以就误会了,觉得awk脚本因为使用了单引号作为脚本开始结束标志,因此在awk脚本里面是不能直接使用单引号的。

其实这是误会了,看下面的脚本你就知道。

cat demo.awk
{ print "'"; }
echo | awk -f demo.awk
'


可见,awk脚本是能够直接使用单引号的,也不须要使用单引号把脚本括起来。 之因此在命令行须要用这么别扭的写法,是由于shell的关系:使用单引号括起来的内容,不会被shell当成特殊字符处理。

由于awk脚本里面常常须要$n来获取第几个字段的内容,而$在shell里面是有特殊意义的,表明变量开始。 若是不用单引号括起来,就会出问题。

'{ print "'\''"; }' 这段能够这样理解:脚本分三段

1'{ print "'2、 \'

3'"; }'

每段被shell解析后是这样的

1、 { print "

2'

3"; } ;

 

三段合起来就是传给awk的脚本内容:{ print "'"; }。理解了这个以后,在windows使用awk碰上如下问题,你就知道怎么解决了:

C:\Users\hch>awk '{print "";}'
awk: '{print
awk: ^ invalid char ''' in expression

8 自动隐式转换问题

在c语言里面,咱们习惯了整数相除,结果仍是整数。因此5/2结果是2,不是2.5。

而在awk里面,因为没有明确指定变量类型,因此在变量计算过程常常会发现隐式转换,整数相除结果多是小数。
举例:

echo | awk '{v_result = 5 / 2; print v_result}' 
2.5

若是咱们想要实现c语言的整数相除效果,要怎么办呢? 可使用int函数,以下:

echo | awk '{v_result = int(5 / 2); print v_result}' 
2

 

9 中文竖线问题

实际工做中,常常碰上文件中每行记录里面用竖线'|'作为分隔符的,如"a|b|c|d"。若是文件里面没有中文,这样作是没问题的。

但若是有中文,特别是gbk编码在中文时,这样作就容易出问题了。

gbk编码中,中文由两个字节组成,第一个字节取值范围是[128, 256),第二个字节取值范围是[0, 256)。

若是第二个字节值正好是'124',也就是'|'字符的asscii码,awk处理时就会误觉得这个字节是分隔符,从而致使分割字符串时出现错乱。

那有哪些中文是这样的呢? 能够用如下脚本输出gbk编码中包含竖线的特殊中文:(其它编码相似)

echo|awk '{for(i = 128; i < 256; i++) { printf("%c| ", i); } }' #终端编码要是GBK
€| 亅 倈 億 剕 厊 唡 噟 坾 墊 妡 媩 寍 峾 巪 弢 恷 憒 抾 搢 攟 晐 東 梶 榺 檤 殀 泑 渱 潀 瀨 焲 爘        ▅ ﹟ 獆 珅 瑋 瓅 畖 瘄 皘 眧 瞸 硘 磡 祙 秥 穦 竱 箌 簗 粅 紎 絴 緗 縷 纜 羭 聕 脇 膢 舼 苵 莬 葇 蓔 蕓 藎 蘾 蛗 蝲 蟶 衸 褆 襹 觸 詜 諀 謡 讄 貄 質 趞 踻 軀 輡 迀 遼 鄚 醸 鈢 銃 鋦 鍇 鎩 鐋 鑭 閨 陓 雦 靯 韡 顋 飢 饇 駖 騶 髚 魘 鮸 鰘 鱸 鴟 鵿 鷟 鹼 鼃 齶 

碰上这种状况暂时我没有发现太好的处理方法,建议使用比较长的分隔符,减小碰上问题的几率,如'|@|'。

若是分隔符不可变,那能够考虑使用iconv转换编码,处理完后再转换回来。

10 函数名与变量名冲突

awk内置了不少函数,若是不当心把变量名字取得跟这些函数名字同样,程序就会报错。提示很不清楚,就只是说错了,不说缘由,特别坑。
例如如下这个报错:

awk '{ if (NR == FNR) { sub[$1] = $2; } else { print sub[$1]; } }' subsid_amt.txt subsid.txt
awk: { if (NR == FNR) { sub[$1] = $2; } else { print sub[$1]; } }
awk: ^ syntax error

因为这个脚本是晚上加班到深夜时写的,当时头脑不清醒,看到报错蒙了很久:怎么看语法都是对的,可是运行却老是提示语法错了。
因此如今我写比较复杂的awk脚本,变量名都习惯前面加上v_后缀,这样能够减小名字冲突的几率。

 

暂时就总结了这些。若是你们也碰上过使用awk的问题,不妨一块儿发出来讨论一下吧:)

相关文章
相关标签/搜索