强大的grep,sed和awk–用案例来讲解

准备工作:

先简单了解grep,sed和awk功能  

1) grep 显示匹配特定模式的内容

  • grep -v ‘boy’ test.txt 过滤掉test.txt文件的boy,显示其余内容

  • grep ‘boy’ test.txt 显示test.txt文件中,和boy匹配的内容

  • -E 同时过滤多个”a|b”

  • -i 不区分大小写

  • –color=auto 设置颜色

2)sed 取各种内容,以行为单位取内容

  • -n取消默认输出

  • p=print

  • d=delete 

3)awk 取列

  • -F 指定分割符 如对“I am a student” 以空格为分割符,其将被分为4列,awk里有参数可以去任意列

  • NF 表示当前行记录域或列的个数

  • NR 显示当前记录号或行号

  • $1第一列 $2第二列 $0整行 $NF 最后一列        

案例一:如何过滤出em1的ip地址

[zhaohuizhen@localhost Test]$ ifconfig em1
em1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.0.0.8 netmask 255.255.255.0 broadcast 10.0.0.254
inet6 fe80::b283:feff:fed9:6a9a prefixlen 64 scopeid 0x20<link>
ether b0:83:fe:d9:6a:9a txqueuelen 1000 (Ethernet)
RX packets 13908772 bytes 4072069839 (3.7 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 982482 bytes 86260856 (82.2 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device interrupt 40

步骤一

首先应该过滤出第二行inet 10.0.0.8 netmask 255.255.255.0 broadcast 10.0.0.254内容

方法一:grep命令

[zhaohuizhen@localhost Test]$ ifconfig em1 | grep 'inet '
 inet 10.0.0.8 netmask 255.255.255.0 broadcast 10.0.0.254

  
方法二:用sed命令

[zhaohuizhen@localhost Test]$ ifconfig em1 | sed -n '2p'
  inet 10.0.0.8 netmask 255.255.255.0 broadcast 10.0.0.254

方法三:用awk命令

[zhaohuizhen@localhost Test]$ ifconfig em1 | awk NR==2

  inet 10.0.0.8 netmask 255.255.255.0 broadcast 10.0.0.254

  
方法四:用head,tail命令

[zhaohuizhen@localhost Test]$ ifconfig em1 | head -2 | tail -1
  inet 10.0.0.8 netmask 255.255.255.0 broadcast 10.0.0.254

步骤二

过滤出第二行后,在过滤出ip地址

方法一:用cut命令

[zhaohuizhen@localhost Test]$ ifconfig em1 | sed -n '2p' | cut -c 14-25
   10.0.0.8

[zhaohuizhen@localhost Test]$ ifconfig em1 | grep 'inet ' | cut -d" " -f10
  10.0.0.8

方法二:用awk命令

[zhaohuizhen@localhost Test]$ ifconfig em1 | grep 'inet ' | awk -F '[ ]+' '{print $3}'
  10.0.0.8

用awk命令可以直接处理第二行,不用先将其过滤出来    

[zhaohuizhen@localhost Test]$ ifconfig em1 | awk -F '[ ]+' 'NR==2 {print $3}' 
  10.0.0.8

  
方法三:用sed命令

[zhaohuizhen@localhost Test]$ ifconfig em1 | sed -n '/inet /p' | sed 's#^.*et ##g' | sed 's# net.*$##g'
  10.0.0.8

此处用到了正则表达式(见http://www.cnblogs.com/ZGreMount/p/7656365.html),匹配的目标前面的字符串一般以^.开头,代表以任意字符开头,结尾写上要匹配的字符前面的几个字符,     如”^.addr “就匹配” inet addr “,而处理的目标后的内容则是开头写上要匹配字符后几个字符,加上以.$。如,“ Bcast:.$”就匹配“ Bcast:10.0.0.254 Mask:255.255.255.”

注:sed小括号分组功能

sed ‘s/********/……./标签’ #斜线可以被其它字符替换

前两条斜线中间部分内容********,可以使用正则表达式,后两条斜线中间内容…….不能使用正则表达式。

()是分组,在前面部分使用()括起来的内容,在后面部分可以使用1调用前面括号内内容。

如果有多个括号,那么依次是1,2,3,以此类推。

例如,直接取em1ip地址,不先过滤出第二行

[zhaohuizhen@localhost Test]$ ifconfig em1 | sed -n 's#^.*inet (.*) net.*$#1#gp'
  10.0.0.8

    
直接取出ip地址和子网掩码

[zhaohuizhen@localhost Test]$ ifconfig em1 | sed -n 's#^.*inet (.*) n.*k (.*) bro.*$#1 2#gp'
  10.0.0.8 255.255.255.0

案例二:输出文件a对应权限664

[zhaohuizhen@localhost Test]$ ll a 
   -rw-rw-r--. 1 zhaohuizhen zhaohuizhen 98 Oct 12 20:24 a

    
方法一:用awk命令

[zhaohuizhen@localhost Test]$ ll a | awk '{print $1}'|tr rwx- 4210|awk -F "" '{print $2+$3+$4 $5+$6+$7 $8+$9+$10}'
  664

    
解析:

1)ll a 长格式显示文件a    

[zhaohuizhen@localhost Test]$ ll a
  -rw-rw-r--. 1 zhaohuizhen zhaohuizhen 98 Oct 12 20:24 a

      
2)用awk命令,以空格为分隔符,取出第一列

[zhaohuizhen@localhost Test]$ ll a | awk '{print $1}'
  -rw-rw-r--.

      
3)用tr命令将rwx- 替换为4210

[zhaohuizhen@localhost Test]$ ll a | awk '{print $1}'|tr rwx- 4210
  0420420400.

      
4)用awk将上面的结果分割,然后相加得出结果

[zhaohuizhen@localhost Test]$ ll a | awk '{print $1}'|tr rwx- 4210|awk -F "" '{print $2+$3+$4 $5+$6+$7 $8+$9+$10}'
  664

方法二:用stat命令

[zhaohuizhen@localhost Test]$ stat a
File: ‘a’
Size: 98 Blocks: 8 IO Block: 4096 regular file
Device: fd02h/64770d Inode: 203491 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1002/zhaohuizhen) Gid: ( 1002/zhaohuizhen)
Context: unconfined_u:object_r:user_home_t:s0
Access: 2017-10-14 09:20:34.337529787 +0800
Modify: 2017-10-12 20:24:27.512609708 +0800
Change: 2017-10-12 20:24:27.536609708 +0800
Birth: -

    
1)命令stat a结果包含文件a对应权限644,可以用前面的方法直接过滤出来

[zhaohuizhen@localhost Test]$ stat a | awk -F '[(/]' 'NR==4 {print $2}' 
  0664

    
2)stat命令包含需要结果,考虑stat命令是否有参数可以直接获得我们需要的结果

[zhaohuizhen@localhost Test]$ stat -c %a a
  664

案例三:输出文件a内容,不带空行,文件a内容如下:

[zhaohuizhen@localhost Test]$ cat a
"hello,this is a test"
I am a studeng My QQ is 1534612574

computer

book

river
tree

man
computer

book
river
tree
man

  
方法一:grep命令

[zhaohuizhen@localhost Test]$ grep -v '^$' a
"hello,this is a test"
I am a studeng My QQ is 1534612574
computer
book
river
tree
man
computer
book
river
tree
man

    
注释:-v 即排除;^$,开头和结尾间没有任何东西,即空行

方法二:用sed命令

[zhaohuizhen@localhost Test]$ sed '/^$/d' a
"hello,this is a test"
I am a studeng My QQ is 1534612574
computer
book
river
tree
man
computer
book
river
tree
man

注释:^$代表空行,d即delete

方法三:用awk命令

[zhaohuizhen@localhost Test]$ awk /[^$]/ a
"hello,this is a test"
I am a studeng My QQ is 1534612574
computer
book
river
tree
man
computer
book
river
tree
man

    
注释:^$代表空行,放在[]中代表非,即不匹配空行。

awk(报告生成器),grep(文本过滤器),sed(流编辑器)使用入门

未分类

linux下的文本三剑客

grep

egrep,grep,fgrep 
文本查找的需要
grep:根据模式搜索文本,并将符合模式的文本行显示出来。
pattern:文本符和正则表达式的元字符组合而成的匹配条件

grep [option] "pattern" file 
grep root /etc/passwd

-i:忽略大小写 
--color:匹配的字符高亮显示  alias
alias  grep='grep --color'
-v:反向查找 
-o:只显示被模式匹配的字符串(不显示行)

globbing

*:任意长度的任意字符
?:任意单个字符
[]:任意一个字符
[^]:其中任意一个非

正则表达式:Regular ExPression,REGEXP

元字符:
.:匹配任意单个字符
[]:匹配指定范围内的任意字符
[^]:匹配指定范围内的任意单个字符
[:digit:][:lower:][:upper:] []

字符匹配次数:
*:表示匹配前面的字符任意次(0-inf)
   a*b 
   a.*b
.*:表示任意长度的,任意字符
工作在贪婪模式 
?:匹配其前面的字符一个或0次。
    部分匹配 
  a?b 
{m,n}:匹配其前的字符至少m,至多n次。
   {1,}
  {0,3}
  a{1,3}
  a.{1,3}

位置锚定:

^:锚定行首,此字符后面的任意内容必须出现在行首。
grep "^root" /etc/passwd 

$:锚定行尾,此字符前面的任意内容必须出现在行尾。

grep "bash$" /etc/passwd 
^$:空白行 
grep '^$' /etc/passwd

数字:

[0-9]:

grep "[[:space:]][[:digit:]]$" 

r555t 

锚定单词:

<或b:其后面的任意字符必须出现在行首
>或b:其前面的任意字符必须出现在行尾。

This is root.
The user is mroot
rooter is dogs name.
chroot is a command.
grep "root>" test.txt 
grep "<root" test.txt 
grep "<root>" test.txt  

分组:

()
(ab)* :ab一个整体 

  后向引用

He love his lover.
She like her liker.
He  love his liker.
She like her lover.

grep 'l..e*l..e*' text.txt 
grep "l..e.*1" text.txt
grep "(l..e)" 

1:调用第一个左括号以及与之对应的右括号之间的内容。
2:
3:

/etc/inittab 
grep '([0-90]).*1$'  /etc/inittab 

REGEXP:regular Expresssion

pattern:文本的过滤条件

正则表达式:
basic REGEXP : 基本正则表达式
Extent REGEXP :扩展正则表达式

基本正则表达式

.
[]
[^]

次数匹配:
*:
?:0或1次
{m,n}:至少m次,至多n次

.*:

锚定:
^:
$:
<,b: 
>,b:

()
1,2....

grep:使用基本的正则表达式定义的模式来过滤文本的命令:

-i:忽略大小写 
-v 
-o 
--color 

-E 支持扩展的正则表达式 
-A  # :显示匹配行及以后多少行也显示 
  after 
-B:显示匹配行以及前面的n行
   before 
-C:显示匹配行以及前后的n行
   contest 
grep -A 2 ""  file 


扩展正则表达式:
   贪婪模式

字符匹配:
.
[]
[^]

次数匹配:
*:
?:
+:匹配其前面的字符至少一次
{m,n}

位置锚定:
^
$
<
>

分组:
():分组
1,2,3.....

或者:
a|b  or 

C|cat: 
(C|c)at: 

grep --color -E '^[[:space:]]+' /boot/grub/grub.conf 

grep -E = egrep 

egrep --color '<([1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-5][0-9]|25[0-5])>' 

(<([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-5][0-9]|25[0-5])>.){3}'<([1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-5][0-9]|25[0-5])>.'

IPV4:
5类:
A B C D E 
A:1-127 
B:128-191 
C: 192--223 

<[1-9]|[1-9][0-9]|1[0-9]{2}|2[01][0-9]|22[0-30]>

sed(流编辑器)

sed基本用法:

sed:stream Editor 
行编辑器 
   文本编辑器 
   逐行处理文本 

全屏编辑器:vim 

内存空间:模式空间 
sed 模式空间 
匹配模式空间后,进行操作,将结果输出。仅对模式空间中的数据进行处理,而后,处理结束,将模式空间打印至屏幕;

默认sed不编辑原文件,仅对模式空间中的数据进行处理。

sed [option] [sed-scripts]

option:

-n:静默模式 
-i:直接修改原文件
-e scripts -e script:可以同时执行多个脚本。
-f /path/to/sed_scripts  命令和脚本保存在文件里调用。
  sed -f /path/to/scripts  file 
-r:表示使用扩展的正则表达式。
   只是进行操作,不显示默认模式空间的数据。

comamnd:

address:指定处理的行范围

sed 'addressCommand' file ... 
对符合地址范围进行操作。
Address: 
1.startline,endline 
 比如1,100
   $:最后一行
2./RegExp/ 
  /^root/
3./pattern1/,/pattern2/ 
  第一次被pattern匹配到的行开始,至第一次pattern2匹配到的行结束,这中间的所有行。
4.LineNumber 
   指定行 
5.startline,+N 
 从startline开始,向后的N行。

Command:
 d:删除符合条件的行。
     sed '3,$d' /etc/fstab
     sed '/oot/d' /etc/fstab 
注意:模式匹配,要使用 // 
    sed '1d' file 
p:显示符合条件的行 
 sed '/^//d' /etc/fstab 
 sed '/^//p' /etc/fstab 
   会显示两次
    先显示P匹配,再显示所有模式空间的数据。
a string:在指定的行后面追加新行,内容为"string"
sed '/^//a # hello world' /etc/fstab 
添加两行:
sed '/^//a #hello world n #hi' /etc/fstab 

i sting:在指定行的前面添加新行,内容为string。

r file:将指定的文件的内容添加在指定行后面。
  sed '2r /etc/issue'   /etc/fstab 
  sed '$r /etc/issue' /etc/fstab 

w file:将地址指定的范围的内容另存至另一文件中。
 sed '/oot/w /tmp/oot.txt' /etc/fstab 

s/pattern/string/:查找并替换 
     sed  's/oot/OOT/'  /etc/fstab 
sed 's/^//#/' /etc/fstab 
sed 's///#/'/etc/fstab 仅替换每一行第一次被模式匹配的串。
  加修饰符 
   g:全局替换 
   i:忽略大小写 
 sed 's///#/g'/etc/fstab

 s///:s###
 s@@@

sed 's#+##' 

后向引用

l..e:like----->liker 
     love----->lover 

sed 's#l..e#&r#' file
&:表示模式匹配的引用 

sed 's#l..e#1r#' file 

like---->Like
love---->Love 
sed 's#l(..e)#L1#g' file 


history |sed 's#[[:space:]]##g'
history | sed 's#^[[:space:]]##g'

sed ''dirname

例子????

 1.删除/etc/grub.conf文件中行首的空白符;
 sed  's/^[[:space:]]+//g' /etc/grub.conf 
 2.替换/etc/inittab文件中"id:3:initdefault:"一行中的3
 sed 's#id:3:init#id:5:initd#'
 sed 's@(id:)[0-9](:initdefault:)@152@g' /etc/inittab 
 3.删除/etc/inittab文件中的空白行。
  sed '/^$/d' /etc/inittab
4.删除/etc/inittab文件中开头的#号
sed 's/^#//'  
5.删除莫文件中开头的#号以及空白行。
sed 's/^[[:space:]]+//g' 
6.删除某文件中以空白字符后面跟#类的行中开头的空白字符以及#
sed -r 's/^[[:space:]]+#//g' 
7.取出一个文件路径的目录名称
echo '/etc/rc.d'|sed -r 's@^(/.*/)[^/]+/?@1@g'

awk(报告生成器)

grep :文本过滤器
sed:流编辑器 


grep option pattern file 
sed addresscommmand file 
sed 'comand/pattern/' file 

awk(报告生成器)

根据定义好的格式,显示出来。
nawk 
gawk
gnu awk 

awk option 'script' file file2 
awk [option] 'pattern {action}' file file2 

print 
printf 自定义显示格式


awk一次抽取一行,然后对每一行进行切割分片,每一片可以使用变量进行引用。
$0:表示引用一整行
$1:第一个切片
$2:第二个切片 

awk '{print $1}' text.txt 
awk '{print $1,$2}' text.txt

选项:

-F  指定分隔符
awk -F ''

awk 'BEGIN{OPS="#"}{print $1,$2}' test.txt
BEGIN{OPS=""} 输出分隔符

输出特定字符
awk '{print $1,"hello",$2,$3,$4,$5}' file 

awk 'BEGIN{print "line onenline twonline tree"}'

print的格式:
print item1,item2...

awk -F: 输入分隔符 
OFS="#"   输出分隔符

awk变量

awk内置变量
FS: filed separator,读取文本时,所用字段分隔符
RS:recordsepartor,输入文本信息所使用的换行符。
OFS:OUT filed separator 
ORS:Output ROw separator 

awk -F:
OFS="#"
FS=":"

awk内置变量之数据变量

NR: the number of input record ,awk命令所处理的记录,如果有多个文件,这个数据是所有处理的行数。
FNR:当前文件所处理的行是本文件第多少行。
NF:当前所处理的行有多少个字段。


awk '{print NF}' file 
awk '{print $NF}' file 
awk '{print NR}' file 

-v 定义变量

awk -v test="hello awk" '{print test}' 
awk -v test="hell awk" 'BEGIN{print test}'


awk  'BEGIN{test='hello awk',print test}'

printf 格式化显示

printf  format,item1,item2...

awk 'BEGIN{printf %c,}'
注意:printf不换行  

%d 
%e 
%f 
%g 

修饰符
-:左对齐 
%nd:显示宽度 
awk '{printf %-10s%-10sn,$1,$2}' file

awk的操作符

算术操作符

字符串操作符

布尔表达式

x < y 
x <= y 
x > y 
x != y 
x ~ y 匹配 
x !~ y 

表达式间的逻辑关系符

&& 
||

条件表达式

select?if-true-exp:if-false-exp 
a>b?a=1:b=2 

awk模式

1.正则表达式 /pattern/
2.表达式 
3.REGEXP 指定匹配范围 
4.BEGIN/END 
5Empty  


awk -F : '/^r/ {print $1}' /etc/passwd 
awk -F : '$3>=500{printf $1,$3}' /etc/passwd 
awk -F: '$3+1>=500{print $1,$3}' /etc/passwd

awk -F: '$7~"bash$"{print $1,$7}' /etc/passwd 
进行匹配测试
awk -F: '$7!~"bash$"{print $1,$7}' /etc/passwd 

awk -F: '/^r/,/^m/{print $1,$7}' /etc/passwd 

awk -F: '$3==0,$7~"bash"{print $1,$3,$7}' /etc/passwd 

awk -F '{printf "%-10s%-10s%-20sn",$1,$2,$3}' /etc/passwd 

BEGIN ,END 

awk -F: '$3==0,$7~"nologin"BEGIN{print "Username       ID    shell"}{printf "%-10s%-10s%-20sn"$1,$3,$7} END{print "ending"}' /etc/passwd 

action

1.ExPression 
2.control statements 
3.compound statements 
4.INput statment 
5 output statements 

控制语句

if-else

if(condition) {then-body} else {[else-body]}
eg:
awk -F:

while

while (condition){statement1;statement2;...}
循环每一字段 
length([string])

awk -F: '{i=1; while (1<=NF) if {(length($i)>4) {print $i}; i++}}'

df -hP |awk '{if($4 >=) Print $0}'


do while 
do{statement1,statement2,...} while(condition)

for 
for( ; ; ){statement1;statement2....}

awk -F: '{for(i=1:i<=NF;i++){if(length($i)>=4){print $i}}}'  /etc/passwd 

case 
switch (exprssion) {case value or /regexp/:statement1,statement2,...default:statement,....}

break和continue 
contine是遍历字段的 

next 
提前结束对本行文本的处理,并接着处理下一行,

数组

数组下表是从1开始的
awk[mon]=1 
awk[tus]=2 


for (var in arrary){statement,....}

awk -F: '{shell[$NF]++}END {for(A in shell) {print A,shell[A]}}' /etc/passwd 

nestat -tan 

netstat -tan |awk '/^tcp/{STATE[$NF]++}END{for (S in STATE){print S,STATE[S]}}'

awk '{count[$1]++}END{for ip in count}{printf "%-20s:%dn",ip,count[ip]}}'  access_log

Linux Awk使用案例总结 nginx日志统计

知识点:

  • 数组

数组是用来存储一系列值的变量,可通过索引来访问数组的值。
Awk中数组称为关联数组,因为它的下标(索引)可以是数字也可以是字符串。
下标通常称为键,数组元素的键和值存储在Awk程序内部的一个表中,该表采用散列算法,因此数组元素是随机排序。

数组格式:array[index]=value

一、Nginx日志分析

日志格式

'$remote_addr - $remote_user [$time_local] "$request" $status $request_body  $body_bytes_sent "$http_referer"  "$http_user_agent" "$http_x_forwarded_for"

日志记录:

183.251.21.109 - - [16/Sep/2017:09:43:36 +0800] "POST /article/getKeywords HTTP/1.1" 200 str=Linux+Awk%E4%BD%BF%E7%94%A8%E6%A1%88%E4%BE%8B%E6%80%BB%E7%BB%93+nginx%E6%97%A5%E5%BF%97%E7%BB%9F%E8%AE%A1  185 "http://xxxx" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36" "-"

1、统计日志中访问最多的10个IP

思路:对第一列进行去重,并输出出现的次数

方法1:

awk '{a[$1]++}END{for(i in a)print a[i],i|"sort -k1 -nr|head -n10"}' access.log

方法2:

awk '{print $1}' access.log |sort |uniq -c |sort -k1 -nr |head -n10

说明:a[$1]++ 创建数组a,以第一列作为下标,使用运算符++作为数组元素,元素初始值为0。处理一个IP时,下标是IP,元素加1,处理第二个IP时,下标是IP,元素加1,如果这个IP已经存在,则元素再加1,也就是这个IP出现了两次,元素结果是2,以此类推。因此可以实现去重,统计出现次数。

2、统计日志中访问大于100次的IP

方法1:

awk '{a[$1]++}END{for(i in a){if(a[i]>100)print i,a[i]}}' access.log

方法2:

awk '{a[$1]++;if(a[$1]>100){b[$1]++}}END{for(i in b){print i,a[i]}}' access.log

说明:方法1是将结果保存a数组后,输出时判断符合要求的IP。方法2是将结果保存a数组时,并判断符合要求的IP放到b数组,最后打印b数组的IP。

3、统计2016年4月9日一天内访问最多的10个IP

思路:先过滤出这个时间段的日志,然后去重,统计出现次数

方法1:

awk '$4>="[9/Apr/2016:00:00:01" && $4<="[9/Apr/2016:23:59:59" {a[$1]++}END{for(i in a)print a[i],i|"sort -k1 -nr|head -n10"}' access.log

方法2:

sed -n '/[9/Apr/2016:00:00:01/,/[9/Apr/2016:23:59:59/p' access.log |sort |uniq -c |sort -k1 -nr |head -n10  #前提开始时间与结束时间日志中必须存在

4、统计当前时间前一分钟的访问数

思路:先获取当前时间前一分钟对应日志格式的时间,再匹配统计

date=$(date -d '-1 minute' +%d/%b/%Y:%H:%M);awk -vdate=$date '$0~date{c++}END{print c}' access.log
date=$(date -d '-1 minute' +%d/%b/%Y:%H:%M);awk -vdate=$date '$4>="["date":00" && $4<="["date":59"{c++}END{print c}' access.log
grep -c $(date -d '-1 minute' +%d/%b/%Y:%H:%M) access.log

说明:date +%d/%b/%Y:%H:%M –> 09/Apr/2016:01:55

5、统计访问最多的前10个页面($request)

awk '{a[$7]++}END{for(i in a)print a[i],i|"sort -k1 -nr|head -n10"}' access.log

6、统计每个URL访问内容的总大小($body_bytes_sent)

awk '{a[$7]++;size[$7]+=$10}END{for(i in a)print a[i],size[i],i}' access.log

7、统计每个IP访问状态码数量($status)

awk '{a[$1" "$9]++}END{for(i in a)print i,a[i]}' access.log

8、统计访问状态码为404的IP及出现次数

awk '{if($9~/404/)a[$1" "$9]++}END{for(i in a)print i,a[i]}' access.log

AWK分析nginx访问日志

如题,记录一次 awk 应用。通过分析 nginx 访问日志,得出 pv、uv等数据。

背景

前段时间被要求通过分析 nginx 的访问日志得出项目的 pv、uv 和来源设备等占比。主要是该平台下面有多个 h5 子项目,每个子项目又是一个简单的 SPA 应用,所以就没有考虑到第三方统计平台。不过这个完成了好久,现在才写,主要还是个人懒癌发作。。

AWK

首先介绍下 AWK 吧。

AWK 是一种 linux/unix 下的编程语言,强大的文本语言处理工具。它仅仅需要几行代码就能够完成复杂的文本处理工作。AWK 模式如下:

awk 'BEGIN{ commands } pattern{ commands } END{ commands }'

网上找到一张对应的流程图如下:

未分类

BEGIN语句块在awk开始从输入流中读取行之前被执行,这是一个可选的语句块,比如变量初始化、打印输出表格的表头等语句通常可以写在BEGIN语句块中。

END语句块在awk从输入流中读取完所有的行之后即被执行,比如打印所有行的分析结果这类信息汇总都是在END语句块中完成,它也是一个可选语句块。

pattern语句块中的通用命令是最重要的部分,它也是可选的。如果没有提供pattern语句块,则默认执行 { print },即打印每一个读取到的行,awk读取的每一行都会执行该语句块。

基本语法

AWK 的基本语法可参考: http://man.linuxde.net/awk

处理 nginx 日志

默认的 nginx 日志输出少了些我们需要的部分,所以我们需要先处理 nginx.conf 配置,打出我们需要的日志。这里先说下信息的统计思路吧。PV 很简单,每条 html 的访问记录算一条 PV,每个时间段的 PV 通过 nginx 日志里面的访问时间来判断,每天的 PV 通过将 nginx 日志切割,即每天的访问日志放到每天对应的文件里来判断。而 UV 则通过 cookie 来鉴别,即首先通过 javascript 在客户端种一个唯一的 cookie,每次访问记录中没有携带 cookie 的访问算一次 UV,客户端 cookie 的有效期到每天的0点。而来源及访问设备等可以通过浏览器携带的信息来判断。

nginx 配置

我们采取的部分 nginx.conf 配置如下:

http {
    include       mime.types;
    default_type  application/octet-stream;
    // 定义一种 project_log 格式的日志模式
    log_format project_log '$remote_addr - $time_local "$request" $status $sent_http_content_type UV: "$guid" $http_user_agent';
    sendfile        on;
    keepalive_timeout  65;
    client_max_body_size 2m;
    fastcgi_intercept_errors on;
    server {
    listen 443;
    server_name localhost;
    ssl on;
        ssl_certificate /test/server.crt;
    ssl_certificate_key /test/server.key;
    // 通过 cookie 正则匹配,如果有 _qingguoing 形式的 cookie 记录,就打到 log 中,默认是空
    if ($http_cookie ~* "_qingguoing=([A-Z0-9.]*)" ) {
        set $guid $1;
    }
    if ( $request_method !~ GET|POST|HEAD|DELETE|PUT ) {
            return 403;
        }
    location /qingguoing/ {
            index index.html index.htm;
            error_page 404 ./404.html;
            // 日志输出到 logs/qingguoing.log 文件
            access_log logs/qingguoing.log project_log;
            if (!-e $request_filename){
                return 404;
            }
        }
        error_page  404              /404.html;
    }
}

nginx 日志切割脚本

#nginx 日志切割脚本
#!/bin/bash
# 设置日志文件存放目录
logs_path="/var/www/logs/";
#设置pid文件
pid_path="/var/www/logs/nginx.pid";
#重命名日志文件
mv ${logs_path}qingguoing.log ${logs_path}logs_daily/qingguoing_$(date -d "yesterday" +"%Y%m%d").log
#向ngin主进程发信号重新打开日志
kill -USR1 `cat ${pid_path}`

AWK 处理脚本

BEGIN {
}
{
    # 过滤错误请求,只处理 html 200 请求,因为 html 没设缓存
    if ($8 != 200) next;
    if ($9 == "text/html") {
        # 来源匹配
        fromIndex = index($6, "from=");
        endIndex = index($6, "&");
        if (fromIndex != 0 && endIndex == 0) {
            # 增加 from= 的长度
            origin = substr($6, fromIndex + 5);
        } else {
            origin = substr($6, fromIndex + 5, endIndex);
        }
        # pathname 匹配
        pathLength = split($6, path, "/");
        # uv标记
        uv = 0;
        project = path[3];
        # 404.html
        if (pathLength > 4 || project == "404.html") next;
        if (origin != "platform") {
            # 日期 时间
            split($3, datetime, ":");
            if (timeArr[project] && timeArr[project] != datetime[2]) {
                # 新的一条记录,数据存到 mongodb 中并清空
                system("mongo 'yshow' --eval 'var project=""project"", date=""dateArr[project]"", time=""timeArr[project]"", pv=""pvArr[project]"", uv=""uvArr[project]"", pvFromOther=""pvFromOtherArr[project]"", pvFromTimeline=""pvFromTimelineArr[project]"", pvFromGroupmsg=""pvFromGroupmsgArr[project]"", pvFromSinglemsg=""pvFromSinglemsgArr[project]"",  uvFromOther=""uvFromOtherArr[project]"", uvFromTimeline=""uvFromTimelineArr[project]"", uvFromGroupmsg=""uvFromGroupmsgArr[project]"", uvFromSinglemsg=""uvFromSinglemsgArr[project]"", pvIos=""pvIosArr[project]"", uvIos=""uvIosArr[project]"", pvAdr=""pvAdrArr[project]"", uvAdr=""uvAdrArr[project]"", pvDeviceOther=""pvDeviceOtherArr[project]"", uvDeviceOther=""uvDeviceOtherArr[project]""' ./logs_mongo_insert.js");
                delete dateArr[project];
                delete timeArr[project];
                delete pvArr[project];
                delete uvArr[project];
                delete pvFromOtherArr[project];
                delete pvFromTimelineArr[project];
                delete pvFromGroupmsgArr[project];
                delete pvFromSinglemsgArr[project];
                delete uvFromOtherArr[project];
                delete uvFromTimelineArr[project];
                delete uvFromGroupmsgArr[project];
                delete uvFromSinglemsgArr[project];
                delete pvIosArr[project];
                delete uvIosArr[project];
                delete pvAdrArr[project];
                delete uvAdrArr[project];
                delete pvDeviceOtherArr[project];
                delete uvDeviceOtherArr[project];
            }
            # save
            timeArr[project] = datetime[2];
            dateArr[project] = datetime[1];
            pvArr[project]++;
            if ($11 == """") {
                uvArr[project]++;
                uv = 1;
            }
            ios = index($0, "iPhone");
            adr = index($0, "Android");
            # 设备
            if (ios > 0) {
                if (uv) uvIosArr[project]++;
                pvIosArr[project]++;
            } else if (adr > 0) {
                if (uv) uvAdrArr[project]++;
                pvAdrArr[project]++;
            } else {
                if (uv) uvDeviceOtherArr[project]++;
                pvDeviceOtherArr[project]++;
            }
            # 其他
            if (fromIndex == 0 ) {
                if (uv) uvFromOtherArr[project]++;
                pvFromOtherArr[project]++;
            }
            # 微信朋友圈
            if (origin == "timeline") {
                if (uv) uvFromTimelineArr[project]++;
                pvFromTimelineArr[project]++;
            }
            # 微信群
            if (origin == "groupmessage") {
                if (uv) uvFromGroupmsgArr[project]++;
                pvFromGroupmsgArr[project]++;
            }
            # 好友分享
            if (origin == "singlemessage") {
                if (uv) uvFromSinglemsgArr[project]++;
                pvFromSinglemsgArr[project]++;
            }
        }
    };
}
END {
    for (project in pvArr) {
        system("mongo 'yshow' --eval 'var project=""project"", date=""dateArr[project]"", time=""timeArr[project]"", pv=""pvArr[project]"", uv=""uvArr[project]"", pvFromOther=""pvFromOtherArr[project]"", pvFromTimeline=""pvFromTimelineArr[project]"", pvFromGroupmsg=""pvFromGroupmsgArr[project]"", pvFromSinglemsg=""pvFromSinglemsgArr[project]"",  uvFromOther=""uvFromOtherArr[project]"", uvFromTimeline=""uvFromTimelineArr[project]"", uvFromGroupmsg=""uvFromGroupmsgArr[project]"", uvFromSinglemsg=""uvFromSinglemsgArr[project]"", pvIos=""pvIosArr[project]"", uvIos=""uvIosArr[project]"", pvAdr=""pvAdrArr[project]"", uvAdr=""uvAdrArr[project]"", pvDeviceOther=""pvDeviceOtherArr[project]"", uvDeviceOther=""uvDeviceOtherArr[project]""' ./logs_mongo_insert.js");
    }
}

至此基本逻辑已写完。只需要在服务器里设置定时任务就行,例如每天的0点切割nginx日志,0点30开始处理前一天的日志存储统计信息。所以当前该思路唯一也是最大的缺陷就是没办法获取实时访问记录。后续如果有新思路会及时补充。

至此基本逻辑已写完。只需要在服务器里设置定时任务就行,例如每天的0点切割nginx日志,0点30开始处理前一天的日志存储统计信息。所以当前该思路唯一也是最大的缺陷就是没办法获取实时访问记录。

最后

前面也说了,最近才写这篇文章的主要原因是懒癌,导火索是前两天想通过 AWK 处理一个 hosts 文件到 charles 的 DNS 记录里,居然忘了 AWK 该怎么写了,果然好记性不如烂笔头。

本文代码地址:https://github.com/qingguoing/test/tree/master/awk/nginx

hosts 文件转 charles DNS 记录文件:https://github.com/qingguoing/test/tree/master/awk/DNS

awk 自带函数用法

一、split 初始化和类型强制

awk的内建函数split允许你把一个字符串分隔为单词并存储在数组中。你可以自己定义域分隔符或者使用现在FS(域分隔符)的值。
格式:

   split (string, array, field separator)
   split (string, array)  -->如果第三个参数没有提供,awk就默认使用当前FS值。

例子:

例1:替换分隔符

time="12:34:56"
out=`echo $time | awk '{split($0,a,":");print a[1],a[2],a[3]}'`
echo $out

例2:计算指定范围内的和(计算每个人1月份的工资之和)

[root@test ~]# cat test.txt
Tom    2012-12-11      car     53000
John   2013-01-13      bike    41000
vivi    2013-01-18      car     42800
Tom    2013-01-20      car     32500
John   2013-01-28      bike    63500
[root@test ~]# awk '{split($2,a,"-");if(a[2]==01){b[$1]+=$4}}END{for(i in b)print i,b[i]}' test.txt  
vivi         42800
John   104500
Tom    32500

二、substr 截取字符串

返回从起始位置起,指定长度之子字符串;若未指定长度,则返回从起始位置到字符串末尾的子字符串。
格式:

  substr(s,p) 返回字符串s中从p开始的后缀部分
  substr(s,p,n) 返回字符串s中从p开始长度为n的后缀部分

例子:

[root@test ~]# echo "123" | awk '{print substr($0,1,1)}'
1

解释:

awk -F ',' '{print substr($3,6)}'    --->  表示是从第3个字段里的第6个字符开始,一直到设定的分隔符","结束.
substr($3,10,8)  --->  表示是从第3个字段里的第10个字符开始,截取8个字符结束.
substr($3,6)     --->  表示是从第3个字段里的第6个字符开始,一直到结尾

三、length 字符串长度

length函数返回没有参数的字符串的长度。length函数返回整个记录中的字符数。

[root@test ~]# echo "123" | awk '{print length}'
3

四、gsub函数

gsub函数则使得在所有正则表达式被匹配的时候都发生替换。gsub(regular expression, subsitution string, target string);简称 gsub(r,s,t)。

举例:把一个文件里面所有包含 abc 的行里面的 abc 替换成 def,然后输出第一列和第三列

awk '$0 ~ /abc/ {gsub("abc", "def", $0); print $1, $3}' abc.txt

使用sed或awk获取除最后两个字段之外的字段

字符串示例为:

/Users/yfan/Downloads/dsc20170801_jar/releases/com/netfinworks/ufs/ufs-client/ufs-client-2.0.0.jar

需求:如果使用斜杠/作为分隔符的话,可能每个字符串的字段数不同,这时需要取 除了最后两个字段外的其他字段字符串,所得结果应为如下形式:

/Users/yfan/Downloads/dsc20170801_jar/releases/com/netfinworks/ufs/ufs-client/

解决方法:

1、使用sed

[root@www ~]# b="/Users/yfan/Downloads/dsc20170801_jar/releases/com/netfinworks/ufs/ufs-client/2.0.0/ufs-client-2.0.0.jar"
[root@www ~]# a=`echo "/Users/yfan/Downloads/dsc20170801_jar/releases/com/netfinworks/ufs/ufs-client/2.0.0/ufs-client-2.0.0.jar" |awk -F'/' '{print $(NF-1)"/"$NF}'`
[root@www ~]# echo $b|sed -n "s#$a##gp"
/Users/yfan/Downloads/dsc20170801_jar/releases/com/netfinworks/ufs/ufs-client/

2、使用awk

[root@www ~]# b="/Users/yfan/Downloads/dsc20170801_jar/releases/com/netfinworks/ufs/ufs-client/2.0.0/ufs-client-2.0.0.jar"
[root@www ~]# echo $b |awk -F'/' '{gsub($(NF-1)"/"$NF,"");print}'
/Users/yfan/Downloads/dsc20170801_jar/releases/com/netfinworks/ufs/ufs-client/

使用awk来统计访问站点的响应状态码

使用awk来通知nginx的日志来实现对某站点响应状态码的统计

代码:

awk 'BEGIN{
        print "状态码t比例"
    }
    {
        if ($11 ~ /http://blog.longway.pw/) {
            count+=1;
            item[$9]+=1
        }
    }
    END{
        for(i in item) 
            print i,"t",item[i]/count * 100
    }' /usr/local/nginx/logs/access.log | sort -nk2

awk中引用变量的两种方式

结合编辑数据文件的shell脚本学习awk传参方式,该脚本功能:

  • 取VIDEOUSR_11082017_0102_ONLINE_STASTIC.dat文件中第87个字段的低8位;

  • 将每行数据的第3列和第87列对调;

  • 修改后的数据生成到最新时间戳文件中。

1、常规用法

#!/usr/bin/bash

if [[ $# -eq 2 ]];then
  srcfile="$1" #文件名
  bit=$2 #低bit位
  filetimetmp=${srcfile#*_}
  filetime=${filetimetmp%%_*} #截取文件中时间戳,日+月+年
  realtime=`date +"%d%m%Y"` #生成最新时间戳,日+月+年
  let databit="2**$bit" #低bit位对应的值,通过取余截取第低bit位数值
  if [[ -f $srcfile ]];then
    newfile=${srcfile//$filetime/$realtime} #生成最新时间戳对应的文件
    if [[ -f $newfile && $newfilew =~ ".dat" ]];then
       rm -rf $newfile
    fi
       cat $srcfile|sed 's/ /#/'|awk -v mod=$databit -F ',' '{predata=$87%mod;$87=$3;$3=predata;print $0}'|sed 's/ /,/g'|sed 's/#/ /' >>$newfile
       ##其中sed 's/ /#/'和sed 's/#/ /'是对数据中带有空格时间(2017-08-11 17:30:21)做保护(编辑前空格替换为#看,编辑完后#替换为空格)
  fi
fi

调用方式:

sh datatransfomate.sh VIDEOUSR_11082017_0102_ONLINE_STASTIC.dat 8

awk传参:awk -v mod=$databit,之后可以在‘{}’中使用mod变量。

但字段值不能通过变量方式进行修改,即$87和$3如何使用外部变量进行传参。

2、${}变量传参

#!/usr/bin/bash

if [[ $# -eq 4 ]];then
  srcfile="$1" #文件名
  exchghead=$2 #对应第3个字段
  exchgtail=$3 #对应第87个字段
  bit=$4 #低bit位
  filetimetmp=${srcfile#*_}
  filetime=${filetimetmp%%_*} #截取文件中时间戳,日+月+年
  realtime=`date +"%d%m%Y"` #生成最新时间戳,日+月+年
  let databit="2**$bit" #低bit位对应的值,通过取余截取第低bit位数值
  if [[ -f $srcfile ]];then
     newfile=${srcfile//$filetime/$realtime} #生成最新时间戳对应的文件

    if [[ -f $newfile && $newfilew =~ ".dat" ]];then
       rm -rf $newfile
    fi
       cat $srcfile|sed 's/ /#/'|awk -v mod=$databit -F ',' "{predata=$${exchgtail}%mod;$${exchgtail}=$${exchghead};$${exchghead}=predata;print $0}"|sed 's/ /,/g'|sed 's/#/ /' >>$newfile
       ##其中sed 's/ /#/'和sed 's/#/ /'是对数据中带有空格时间(2017-08-11 17:30:21)做保护(编辑前空格替换为#看,编辑完后#替换为空格)
  fi
fi

调用方式:

sh datatransfomate.sh VIDEOUSR_11082017_0102_ONLINE_STASTIC.dat 3 87 8

awk传参:由于使用了“”,shell会对“”中$进行索引替换,因此需要对部分$进行转译。

awk中“”经过shell处理后会转换:

awk -v mod=$databit -F ',' "{predata=$${exchgtail}%mod;$${exchgtail}=$${exchghead};$${exchghead}=predata;print $0}"

                                                    ==》awk -v mod=$databit -F ',' ‘{predata=$87%mod;$87=$3;$3=predata;print $0}’

3、使用总结

  • 常规使用 -v 即可传参,awk -v innerpara=$outerpara -F ‘,’ ‘{print innerpara,$0}’

  • 通过变量方式传参,awk -v -F ‘,’ “{print $${outerpara},$0}” ##注意使用转译符即可

  • 以上两种传参方式可以同时使用

使用awk求指定列的最大值最小值

需求:一文件内容如下,求第一列的最大值和最小值

1 1220
2 1221
3 1222
3 1223
4 1224
5 1225
12 1226
12 1227
12 1228
12 1229

12 1230

命令如下

求最小值:

sed '/^$/d' test.txt|awk 'NR==1{min=$1;next}{min=min<$1?min:$1}END{print min}' 

求最大值:

sed '/^$/d' test.txt|awk 'NR==1{max=$1;next}{max=max>$1?max:$1}END{print max}' 

利用sed删除空行,利用awk筛选出最大值和最小值。

AWK 两个文件字段合并处理实例

一、概念解析

1、awk命令概念

$0 表示一个文本中的一行记录

$1...N 表示一行中的第 1...N 字段

FNR     The input record number in the current input file.  #已读入当前文件的记录数

NR      The total number of input records seen so far.      #已读入的总记录数

next    Stop processing the current input record. The next input record is

       read and processing starts over with the first pattern in the AWK

       program. If the end of the input data is reached, the END block(s),

       if any, are executed.

2、处理两个文本文件

执行处理顺序:

首先,对file1执行“NR==FNR{…}”第一个循环,建立哈希数组;第二步,执行“NR>FNR{…}”第二个循环,打印输出命令结果。

  • 一种是
awk 'NR==FNR{...}NR>FNR{...}' file1 file2 或 awk 'NR==FNR{...}NR!=FNR{...}' file1 file2
  • 另一种是
awk 'NR==FNR{...;next}{...}' file1 file2

二、处理两个文件实例

1、处理实例一

file1:

文件内容:

sina.com 52.5

sohu.com 42.5

baidu.com 35

file 2:

文件内容:

www.news.sina.com sina.com 80

www.over.sohu.com baidu.com 20

www.fa.baidu.com sohu.com 50

www.open.sina.com sina.com 60

www.sport.sohu.com sohu.com 70

www.xxx.sohu.com sohu.com 30

www.abc.sina.com sina.com 10

www.fa.baidu.com baidu.com 50

www.open.sina.com sina.com 60

www.over.sohu.com sohu.com 20


awk 'NR==FNR{a[$1]=$2;next}{print $0,a[$2]}' f1 f2

命令结果:

www.news.sina.com sina.com 80 52.5

www.over.sohu.com baidu.com 20 35

www.fa.baidu.com sohu.com 50 42.5

www.open.sina.com sina.com 60 52.5

www.sport.sohu.com sohu.com 70 42.5

www.xxx.sohu.com sohu.com 30 42.5

www.abc.sina.com sina.com 10 52.5

www.fa.baidu.com baidu.com 50 35

www.open.sina.com sina.com 60 52.5

www.over.sohu.com sohu.com 20 42.5

2、处理实例二

需要处理的同名字段可以在两个文件中行号不同的行,无需行号排序相对应,

命令结果的行顺序依据第二个文件中同名字段顺序输出。

f1

文件内容:

10020036 beijing

10050259 lanzhou

10045682 hefei

20130495 guangzhou

20981345 shenzhen

20984748 chengdu

20891376 changsha

f2

文件内容:

guangzhou 4.5

hefei 2.6

beijing 1.3

shenzhen 8.5

changsha 0.8

chengdu 2.0

lanzhou 2.4


awk 'NR==FNR{a[$2]=$1}NR>FNR{print a[$1],$0}' f1 f2

命令输出:

20130495 guangzhou 4.5

10045682 hefei 2.6

10020036 beijing 1.3

20981345 shenzhen 8.5

20891376 changsha 0.8

20984748 chengdu 2.0

10050259 lanzhou 2.4