使用cat/grep/awk/sort等工具简单分析Nginx日志

这两天需要对公司 Nginx 访问日志进行分析,实用到cat、grep、awk、sort、uniq、head等命令,简单记录一下。。单个来看,它们各有自己的用途,Linux的便捷之处,就是可以通过管道来组合各个不同用途的命令,能达到很大的威力。

统计访问ip,按访问次数排序:

1
2
// 查询访问最多的300个ip
cat access.log | awk '{print $1}' | sort -n | uniq -c | sort -rn | head -n 300

查找某个ip的访问记录

1
cat access.log | grep xxx.xxx.xxx.xxx | awk '{print $1 " " $4" " $6" "$7}'

查找某条URL的具体访问情况所有ip访问情况,访问数高到低排序:

1
cat access.log | grep /url | awk '{print $1}' | sort -n | uniq -c | sort -rn

统计访问最多的URL,并统计它们的访问次数:

1
cat access.log | awk '{print $7}' | sort -n | uniq -c | sort -rn | head -n 100

为什么说PHP不支持Unicode编码

经常看到有说法:PHP不支持Unicode,或者说PHP在底层不支持Unicode。虽然我知道PHP编码很蛋疼,各种字符串处理函数非常不规范,但也还能显示中文,一直没弄明白这个不支持Unicode是什么意思。花了一些时间来梳理这方面的信息。

先从一个例子来引入:
一个PHP脚本如下,假设文件的编码是UTF-8:

1
2
3
4
//文件编码UTF-8
echo strlen("中文"); // 6
echo substr("中文",0,1) // 乱码
echo substr("中文",0,3) // 中

很奇怪吧,从上面看,似乎把一个汉字当成了3个字符。这就要从PHP对于字符串的存储上说起了。

我总结了一下,如下:

  • PHP的字符串,是由字节(byte)组成的数组构成的。也就是说,类似于C语言 char a[3] = "abc" 这样,一个字符占据一个字节。
  • 除此之外,并没有存储文本的编码信息,也就是说PHP并不知道这些字符串的二进制数据,应该对应怎样的编码。
  • 再进一步,PHP会按照脚本文件的编码,来决定字符串的编码。就比如:$string = "中文";,如果脚本文件是UTF-8,就会把中文的UTF-8的编码:E4B8ADE69687给保存起来。
  • 再进一步,如前说所,PHP并不保存字符串的编码信息。所以即便中文保存为:E4B8ADE69687,在字符串原生函数看来,都只是一串二进制数。所以,PHP原生字符串函数只能操作单字节字符!就是把一个字节当做一个字符来处理!

如果想明白了上面几点,上面的代码例子就自然明白了:

1
2
3
4
5
//文件编码UTF-8
echo bin2hex("中文"); // 可以看到,"中文"对应的二进制就是:e4b8ade69687
echo strlen("中文"); // 所以按照单字节来统计长度,就是6
echo substr("中文",0,1) // 取0到1个字节,也就是e4,并不对应某个字符的编码,所以乱码
echo substr("中文",0,3) // 取0到3个字节,刚好把`中`的编码取出来

同理,如果把文件编码换成GBK或者别的,再实验也会得到类似的结果,只不过GBK一个汉字占2字节。

那么到现在,基本可以明白了PHP底层不支持unicode到底说的是什么了,总结如下:

PHP字符串不保存字符的编码信息,所以原生操作函数,并不知道二进制数据该如何对应文本,只能【假设】一个字符对应单个字节。这样在处理英文等ascii码时够用了,但对于中文等【多字节字符】,就会出错了。

而作为反面,我们可以看看所谓底层支持Unicode的语言的情况:

1
2
3
var string = "中文"
console.log(string.length); // 2
string.substr(0,1) // 中

可以看到,在JS中,能正确识别和处理多字节字符。也就是在存储时,把文本的编码信息也一并存储。(这里我猜测是保存的是文本的Unicode值,并不太确定,因为不了解JS的底层原理)

那么这里就有疑问了,PHP中如何才能正确处理多字节字符呢?答案就是mbstring扩展(具体可看:http://php.net/manual/zh/book.mbstring.php)。所谓mbstring,也就是:multi-byte string ,多字节字符串。

这套扩展中,有一系列与原生字符串函数对应的函数,能用来正确处理多字节字符的情况。如:strlen 对应 mb_strlen …… 这些对应函数中,基本和原生函数一致,只不过通常多了一个可选参数:编码。

举例如下:

1
2
3
4
5
// 脚本类型为UTF-8
echo strlen("中文"); // 6
echo mb_strlen("中文","UTF-8"); //2 使用mb_strlen ,并传入编码 utf-8, 就会把二进制E4B8ADE69687当做utf-8的处理能正确处理
echo mb_strlen("中文"); //2 如果不传编码UTF-8,则函数会自动确定编码,文档说:如果省略,则使用内部字符编码。所以这里也当做UTF-8来处理。
echo mb_strlen("中文","GBK"); //3,如果传入编码GBK,则:e4b8ade69687会被当做gbk来处理,一个gbk字符占2字节,所以为:3

微信小程序踩坑之:canvas

最近在公司做一个小程序的项目,要说小程序和传统前端开发很类似,而且和Vue很像,都是数据来驱动视图,没有DOM操作,概念上并不难接受(特别是有Vue/React等开发经验下)。

我所做的应用,有一个需求:需要画图表,也就是涉及到canvas这部分内容。实践下来踩坑不少,浪费了不少时间!记录一下。

History.pushState 在微信浏览器中的应用

由于微信浏览器限制了很多功能,比如apk文件下载、淘宝商品跳转等,所以在很多场景中,常常需要引导用户使用手机浏览器打开。

那这里就引出一个需求:在浏览器中,如何判断用户是独立访问,还是由【微信引导而打开】。这个需求很有意义,最简单的可以做一些数据统计,更进一步可以做一些某些特殊操作。

这个问题看似简单,实际要想解决并不容易。
首先想到的是,能否【加个参数】用以区分。

  • 用户在微信中打开网页后,在URL中加个参数,比如from=wechat 。这样倒是能实现,但实际上会造成页面刷新。即:用window.location.href 或者 window.location.search 来设置URL中的参数值,会造成刷新。页面已经打开了,再刷新一次,并不是好的解决办法。
  • 再考虑到,是否可以URL 加 hash,这样不会造成页面刷新了。如:window.location.hash = ‘wechat’ 。但经过实测,在微信中选择浏览器打开后,URL并不会带上hash值。这个办法也不行。

既然在前端加参数并不容易,那么考虑后端来识别是否可行。因为HTTP无状态,当前访问并不能知道上一次访问的情况。进而考虑设置cookie,实际上也不行,因为微信浏览器和手机浏览器是两个不同的环境,cookie并不能跨浏览器……

走到这里似乎没有新进展了,无意中想起可以使用History对象,测试了一番居然是可行的!核心就是利用:history.pushState() 这个函数。此函数作用是动态修改URL,而不引起页面重新请求。测试发现,当使用这个函数后,URL会改变,页面没有被刷新,而且微信中选择浏览器打开,打开的也是新的URL。

这样,如果用户在微信中访问,即可使用这个函数往URL中增加参数。在普通浏览器中,判断如果有微信设置的参数,即可知道曾在微信访问过了!

关于history.pushState()的具体解释:https://developer.mozilla.org/zh-CN/docs/Web/API/History/pushState

如何拦截jsonp的回调函数

最近在公司做的一个需求:在某个页面发送特定请求之后,拦截它的后续操作,以便自定义某些操作。听起来好像有点奇葩,但这类需求应该还是存在的,解决方案比较独特,值得记录一下。

美元符$和变量插值

众所众知,PHP中每一个变量,都需要以美元符$开头来表示,这是PHP诸多被诟病的点之一。

然而最近我领悟到美元符也有一点好处,就是:能方便的将一个变量嵌入到字符串中,也就是所谓变量插值

在PHP中,$a='hello';echo "$a world";即可打印出hello world。也就是当读取到$时,解释器知道这是一个变量,从而将变量替换成对应的值。正是有$的存在,使得字符串拼接变得相对自然。

其他语言则不一定有这样功能,将变量嵌入到字符串中,大致就有两种办法:

  • 字符串拼接。如:var a = 'hello'; console.log(a+'world')(js)
  • 要么就需要使用占位符。如:printf("%s,world","hello") (C语言)

相比之下,变量插值的方式更自然一些。

注:

  • 不止是PHP,其他一些带有$的语言也有变量插值的功能,例如Perl?我不太清楚
  • JS新标准es6,也提供了变量插值的功能,如:
1
2
var a = 'hello'; console.log(`${a} world`)
//hello world

Nginx如何查找文件

Nginx如何对应到磁盘文件上,看似简单(好像无非就是由URL映射到磁盘文件),但实际上还是有可值得琢磨的地方,记录一下。
主要是几个指令:

  • root
  • alias
  • try_files

如何用PHP实现反向代理

按道理请求转发并不是PHP所擅长的,至少有更好的可以做到的办法能做到,在服务器层面Nginx能很容易做到(http_proxy),在语言层面我感觉node.js会更好写。但有时候环境所限,就是有这种需求,在此记录一下。

cookie的secure属性引发的bug

https化的大潮中,大多网站不是一步到位的,基于各种原因,并不会强制https访问,大多同时存在过http/https均可访问的情况。

前段时间碰到过由此引起的一个cookie传递问题,值得记录一下:
问题的表现是:在用户在https访问的情况下登录了账号,这时候如果回到http版本,则并没有登录,而且重新登录也登不上去!

与登录相关的,首先想到的当然是cookie的问题,仔细调试才发现,在https访问的情况下,设置cookie,会多带了一个secure的属性,而http访问则不会带。

这个属性是一个布尔值,真或者假,为真时表示这个cookie值只在htts的情况下,才会随着请求一起发送给服务器。这个属性不太容易被重视到,因为一般来说设置cookie参数可能只会关注到域名、路径、过期时间等属性,而且这个secure属性,默认是false,也不会引发什么问题。

但如果网站同时允许https和http访问,并且在设置cookie时把访问协议考虑其中,例如通过是https或者443端口访问时,设置cookie是让secure为true。这样的出发点本来是好的,但是却引发一个问题:如果有用户先是通过http访问,并在一些交互过程中,给浏览器种下了cookie,此时secure为true,如果此时由于某种原因,用户通过https访问,则之前的设置的cookie则不会携带。这也就是为什么,在https环境下登录的用户,到了http,却自动退出了,其实并没有退出,只是secure设为了true,cookie没有传递。

需要小心,特记录一下。

如何跨域

跨域是个大问题

最近工作中经常需要在前端调用接口,则时常会碰到跨域的问题,积累一些思考,总结一下。另外也深刻感觉到,如果一个东西,不自己实践一下,仅看书是很难理解透彻的。