外交部发言人华春莹称“中国的互联网是 自由、开放、有序 的”。尽管中国互联网相比世界其他地方的互联网也存在许多相似之处,但还有很大不同。
这是最好的时代 这是最坏的时代
于是他只好保持着沉默,默默地与同样面无表情的她擦肩而过,继续向前走去。他的身影逐渐融入同样安静的灰色人群之中,整个城市都显得寂静极了。——《寂静之城》
——谨以此文献礼十九大。
Shell解析处理XML方法汇总
前言
前几天干活的时候遇到一个需要解析处理xml文件的一个需求,当时考虑到逻辑比较复杂,因此用java慢慢搞了搞。不过这个需求经常会变,每次变化之后都要重新找到jar包的代码,改了之后还要替换原来的jar包,一来不方便修改,二来不方便统一保存代码,三来也不方便查看jar包的功能。
其实对于这种比较灵活的功能,最方便高效的做法是采用一些脚本语言,比如python,ruby等等,开发效率高,而且也能处理一些复杂逻辑。但是由于种种原因,工作中有的机器没有安装这些语言的解释器。因此不得已,研究了一波用shell脚本解析xml的方法。
说到底,shell还是不太适合处理复杂的逻辑,但是对于一些简单的查找替换等需求,用shell来搞还是挺方便的。
我这里主要采用了下面三个工具:
- xmllint
- xpath
- xml2
下面就分别总结下这三个工具的用法,方便以后查阅。
xmllint
简述
xmllint其实是由一个叫libxml2的c语言库函数实现的一个小工具,因此效率比较高,对不同系统的支持度也很好,功能也比较全。他一般属于libxml2-utils
这个软件包,因此类似与sudo apt install libxml2-utils
的命令就可以安装。
功能
xmllint至少支持下面几个常用功能:
- 支持xpath查询语句
- 支持类shell的交互式查询
- 支持xml格式验证
- 支持dtd,xsd对xml的校验
- 支持编码转换
- 支持xml格式化
- 支持去空格压缩
- 支持时间效率统计
其实我们比较常用的功能主要也就是三个–xpath查询、去空格和格式化、校验。
比如当前有sample.xml:
1 | <books> |
执行xpath查询
1 | myths@business:~$ xmllint --xpath "//book[@id=2]/name/text()" sample.xml |
去空格
1 | myths@business:~$ xmllint --noblanks sample.xml |
格式化
1 | myths@business:~$ xmllint --format sample.xml |
xsd校验
1 | myths@business:~$ cat sample.xsd |
注意校验结果信息是输出到stderr中的,工具默认会把原文件回显到stdout里,可以加–noout参数关闭stdout回显。
流传递
xmllint默认是传递文件名,如果我们希望用通过管道传递文件流的方式传递数据,我们可以这样弄:
1 | myths@business:~$ cat sample.xml |xmllint --format - |
奇怪报错”XPath set is empty”
这个问题在使用xmllint的xpath功能时候会经常遇到,其实原因主要是xml文件自带了名空间,比如maven的pom文件的开头:
1 | <?xml version="1.0" encoding="UTF-8"?> |
或者maven的setting文件的开头:
1 | <?xml version="1.0" encoding="UTF-8"?> |
他们都会有个xmlns的属性,表示该xml文件的名空间。如果对这种文件直接执行查找则会报错:
1 | myths@business:~/cucumber/code/device# xmllint --xpath "/project" pom.xml |
解决方案要么把文件的xmlns属性去掉,要么就只能采用下面这种变通的办法指定节点名:
1 | myths@business:~/cucumber/code/device# xmllint --xpath "/*[local-name()='project']" pom.xml |
xpath
简述
xpath工具其实是封装了的perl脚本,本身也只有两百来行,功能比较专一,就是提供xpath的查询功能。他一般属于libxml-xpath-perl
这个软件包,因此类似于sudo apt install libxml-xpath-perl
的命令就可以安装。像suse之类的系统还会直接自带。
功能
不同系统中安装的版本可能不同,不过基本功能是类似的:
1 | myths@business:~$ xpath -e '//book/name/text()' sample.xml |
默认会将查询呢结果输出到stdout中,将说明信息输出到stderr中。如果为了方便收集结果,可以将stderr重定向到/dev/null,或者加上-q参数:
1 | myths@business:~$ xpath -e '//book/name/text()' sample.xml 2>/dev/null |
对比xmllint
xpath相比xmllint的xpath功能有一点点区别很重要,如果xpath匹配了多个结果,那么xpath就会分行输出,而xmllint则会揉到一行:
1 | myths@business:~$ xmllint --xpath "//book/name/text()" sample.xml |
除此之外,xmllint工具相对比较稳定,在不同的系统内的使用参数基本固定。而xpath工具不是很标准,在不同系统内的默认版本之间甚至不互相兼容。比如在ubuntu14.04内默认用法是/usr/bin/xpath [options] -e query [-e query...] [filename...]
,而在suse12内的默认用法却是/usr/bin/xpath [filename] query
。这会很容易导致相同脚本对不同系统的不兼容性。
xml2
简述
xml2这个工具感觉知道的人并不多,不过其实他在某些场景里跟其他命令配合能起到奇效。这个工具的开发人员的博客似乎已经挂掉了,不过目测应该用C以及libxml2库写的一个小工具。一般是在xml2
软件包中,因此类似sudo apt install xml2
的命令就可以安装。
功能
这个工具包含六个命令:xml2,2xml,html2,2html,csv2,2csv,功能也非常unix,就是分别将xml,html,csv格式与一种他称之为“flat format”的格式进行转换。举个例子:
1 | myths@business:~$ cat sample.xml |xml2 |
这种自定义的格式非常简单而巧妙,有的表示新建节点(/books/book
),有的表示给节点赋值(/books/book/name=book1
),有的表示给节点的属性赋值(/books/book/@id=1
)。写法跟xpath很像但又不完全一样。而且相互对应的两个命令放在一起能做到幂等。
那么这种转化命令有什么用呢?其实我们经常会遇到一些创建xml文件的需求,但是直接按照xml格式动态生成就非常麻烦,这时候用flat format做个中转就非常方便了:
1 | #!/usr/bin/env bash |
上面这段代码就生成了与sample.xml一模一样的new_sample.xml.
云主机和 VPS 的区别
很多站长对于云主机、VPS、虚拟主机之间的区别都不知所以,很多解释这些概念的文章也经常出现各种错误,使得这些概念经常被混淆。所以我也写一篇阐述它们区别的文章。
如何写好一个开源项目的 README
README 是一个项目的门面。如果你想让更多人使用你的项目或者给你的贡献 PR、丢 star,你就应该写一个吸引人的 README。
Web 性能优化(6)——WebFont 字体优化
WebFont 的加载是一个令人头疼的事情。除了跨域问题、还有 FOIT、FOUT 等等。为了提供更好的用户体验,我寻找了一些高效加载 WebFont 的简单解决方法,并对它们分别进行测试。
Linux下定时任务配置深入理解
前言
关于定时任务的配置其实是一个老掉牙的问题了,为什么我又要总结一遍呢?我想大概有以下几点原因。首先,大多数文章都聚焦于cron语法,而比较忽视具体的操作步骤。其次,很多文章都介绍的比较凌乱,层次不是很清楚。而且,当我理清楚linux下定时任务配置的一套流程之后,深刻的觉得他的设计真的是很周到的。不过最重要的一点大概就是我非常不喜欢那种堆砌命令用法的文章,好像Linux就是他写的一样,东一块说明,西一块说明,谁都不知道这些说明是谁说的,从哪里来的,是不是以讹传讹,可信度有多少,是不是已经不被支持,等等。尤其是在当前这种版本飞速迭代的年代,对于一些重要配置只知其然不知其所以然是非常可怕的一件事。
其实关于定时任务配置这一块没有任何技术含量,重要的就是细心一点,理清配置文件之间的关系即可。
概述
在linux里配置定时任务主要是靠cron和crontab两个程序来控制。这两个命令各司其职而又互相联系,cron是执行定时任务的守护进程,负责配置的解析跟处理;而crontab则是方便用户进行直接配置的命令,相当于是方便用户直接进行管理的工具。
Cron
查阅Ubuntu14.04系统的cron的man文档,我们可以发现,cron其实是一个存放在/etc/init.d/
下的一个脚本,随着系统开机自动启动,可以由service命令调度控制开启和关闭。
Step1
首先,cron会搜索/var/spool/cron/crontabs
文件夹,这个文件夹下有多个以用户名命名的文件,每个文件就是属于各个用户的独立的cron配置文件。我们可以将定时任务项按照类似下面的规则配置在这些文件里:
1 | # For example, you can run a backup of all your user accounts |
其实就是基础的cron配置项加上需要执行的命令。需要注意的是与下面两个不同,这里的配置不需要指定用户名,而下面的配置是需要指定用户名的。
Step2
然后,cron会去搜索/etc/crontab
文件,并且解析里面的cron配置。比如在Debian里,默认的配置是这样的:
1 | SHELL=/bin/sh |
他首先定义了一些基本的环境变量,然后配置了四条任务。显然,这四条任务就是配置了定时调度 /etc/cron.hourly
,/etc/cron.daily
,/etc/cron.weekly
,/etc/cron.monthly
这四个文件夹,通过run-parts命令来递归调度文件夹下的所有可执行文件。
这个配置可能在不同的linux版本下写法不一样,但是最终的结果基本差不多,都是默认配置了定时调度文件夹的任务。比如在SUSE12中,这个配置就变成了这样:
1 | SHELL=/bin/sh |
看上去好像不一样,但其实这个run-crons
脚本做的事情也是定时调度那四个文件夹。
事实上,很多系统内置的定时脚本都是存放在这四个文件夹下进行自动调度的。
Step3
最后,cron会去执行/etc/cron.d/
这个文件夹下的东西,不过我们通常不建议在这里进行修改,虽然这个文件夹下的变化也会被监视,但是我们更习惯将这种不通用的定时任务配置在/etc/crontab/
里。
Crontab
查阅Ubuntu14.04系统的crontab的man文档,我们可以发现,crontab其实是方便用户来维护crontab配置文件的工具。
通过crontab -l
可以显示属于当前用户的/var/spool/cron/craontabs/
文件夹下的配置。通过crontab -e
可以安全的进行编辑,如果语法不对他会进行提示,保证安全。就像visudo
命令一样,本质就是”修改配置文件+语法检测”。
同时,crontab还提供了两个配置文件来控制用户的权限–/etc/cron.allow
跟/etc/cron.deny
。只有用户名在白名单里的用户才能使用crontab命令,用户名在黑名单里的用户是无法使用crontab命令的。显然,原则上这两个配置文件不能同时存在,如果同时存在,那么出于保守原则考虑,只有白名单有效,黑名单无效。如果这两个配置不存在,那么根据linux版本的不同,有的系统默认所有用户都有权限,有的系统默认只有root才有权限。
配置选择
以上大概就是最基本的配置文件了。看上去好像很繁琐,到处都有配置,其实我觉得设计者考虑的还是十分周到的。事实上这一系列的命令跟配置文件充分考虑了各种实际场景,并且给我们提供了很好的选择。
固定用户的定时任务
有时候,我们的服务器可能是多个用户在用,这时候如果所有人都把自己的定时任务配置在一个文件里显然不方便处理。这时候我们就可以使用crontab命令去修改位于/var/spool/cron/crontabs/
下的属于当前用户的配置文件。这样就能够非常方便的区分不同用户的配置,保护了数据的安全。
固定时间的定时任务
很多情况下,作为系统管理员,我们需求的任务模式大都是每小时触发,每日触发,每周触发,每月触发之类的。那么这时我们就可以不用配置cron项,只需要把脚本放在对应的/etc/cron.daily
,/etc/cron.hourly
之类的文件夹下即可,方便省事。而且事实上,很多系统自身需要的定时任务就是这么办的。这种方式也是我们最推荐的方式,因为我们只要把需要定时执行的脚本放在规定的路径下即可,无需配置cron,毕竟cron配置文件用起来还是比shell脚本麻烦很多。
固定程序的定时任务
有时候,某些处理特定任务的进程也希望能够创建定时任务,比如我们编写或者安装的第三方任务。这些任务不希望依附于某一个用户,而希望拥有独立的配置文件,方便修改和卸载等等。这时候我们就可以新建一个cron配置文件,放置于/etc/cron.d/
文件夹下,进行统一管理。像csf,lfd这类的进程就是这么做的,通过这样的配置保证服务定时重启。
配置脚本注意点
所谓的配置脚本其实也有两种,一种是cron配置文件,我们可以在这些文件的后面写一些简单的命令;还有一种是放在/etc/cron.daily
中的shell脚本,或者由cron配置文件调用的shell脚本,这些脚本写起来就更加灵活了。
cron配置文件
cron配置文件主要由两部分组成,一部分是环境变量的定义,另一部分就是符合cron语法的配置项。
环境变量
默认情况下,cron配置文件里是没有绝大多数的环境变量的,就连$PATH
跟$SHELL
这两个变量也都是用的最基础的版本。因此我们在写命令的时候要注意要么全部使用命令的绝对路径,要么就定义下重要的环境变量。。。不过一般来说还是写绝对路径方便。
输出日志
默认情况下cron命令的输出是会输出到邮件池里,然后发送给定时任务所属的用户。显然如果定时任务比较频繁就会给用户发送很多的”垃圾邮件”。因此我们一般都会将输出进行重定向,包括标准输出和错误输出,将其全部重定向到指定的文件夹内。
有时候我们可能希望查看定时任务到底有没有执行,这时候我们其实只需要查看下syslog的位置,通常是在/var/log/
下。他会输出类似下面的定时日志:
1 | Sep 6 23:41:52 pc crontab[8420]: (myths) BEGIN EDIT (myths) |
Shell脚本
写Shell脚本相比些cron配置文件就自由很多了,毕竟只需要关注业务逻辑即可。不过我们仍然需要关注环境变量和当前路径,因为这里的shell脚本中很多环境变量仍然是最基础的配置。通常我们会在脚本开头手动导入默认的配置:
1 | source /etc/profile |
为了安全起见我们仍然尽量采用绝对路径,或者cd进入想要的路径进行操作,避免不必要的问题。
字符集与字符编码的强化理解与操作实践
踩坑
最近在工作中遇到了一个说大不大说小不小的问题,就是当我解析一个xml文件的时候,抛出了一个”Invalid byte 2 of 2-byte UTF-8 sequence”的异常,这个异常会导致解析直接退出,显然不能容忍。查阅相关资料稍微定位了一下,大概知道是字符集的问题,仔细一看,xml文件中的确有中文字符,而且当我把这些中文字符删了之后的确又能解析成功。不过我还是不能理解这当中的缘由,不过由于时间原因,当时只是把中文字符删了就草草完工。现在回头想想这个坑还是不能留,顺便趁机补下字符集相关的知识。
字符集和字符编码
字符集
字符集的概念是一个非常容易让人混淆的概念,很多情况下我们都会把他跟字符编码当成是同一个概念,但是事实上这两个概念其实是完全不一样的。
所谓字符集,其实是对所有字符映射到唯一ID的一个映射表,或者叫hash表,比如我就可以定义一个字符集,这个字符集里只有四个字符—-“我”,”是”,”帅”,”哥”。那么我就可以把这四个分别映射为0,1,2,3,二者一一对应:
1 | 我-0 |
字符编码
但是字符集只是规定了字符与数字之间映射关系,并没有规定如何在二进制文件中进行表示(编码)。我可以定义很多中字符编码方法,比如我可以认为所有的字符都占两个bit位,这样当读取文件流的时候,我就可以两个bit两个bit的去读,并按照下面的规则进行解析:
1 | 00-我 |
看上去没问题,但是有人可能会说,这种编码不好,为啥呢,因为这样子每个字符都占用了2个bit,可能在某些情况下”我”这个字符出现的次数非常多,其他的字符出现的非常少,那么使用上面的编码方法可能就会浪费空间。我们可以用类似Huffman编码的策略修改一下编码方法:
1 | 0-我 |
这其实就是构造了一个二叉树,每一个内部节点就是0或1,每一个叶子节点就是一个字符。当我们解析的时候就顺着这棵树去找相应的字符就行了。
这种编码能保证当“我”出现次数很多的时候,文件的大小能够变小。当然我们需要注意每个字符的编码都不能是其他字符的前缀,否则就会出现解析混乱。
其实所谓字符集和字符编码的关系就是这么简单。只是由于历史原因导致当前的字符集和字符编码比较杂乱,没有绝对的统一,因此才会出现各种”乱码”现象。
Unicode字符集与UTF-8编码
为什么要单独拿Unicode字符集跟UTF-8编码来说是呢,一方便是因为这两个东西被用的最广,尤其是Java语言的原生支持;另一方面正是因为用到广,因此这两个东西被人误解的最多。
我在一开始了解这两个东西的时候也很蒙,有的文章说Unicode是一种编码,有的文章说Unicode不是编码而是字符集,有的文章说UTF-8是一种Unicode编码,有的文章说UTF-8不是Unicode编码。。。现在回想起来,其实他们说的都对,又都不全对。
Unicode是一种字符集
没错,Unicode当然是一种字符集,他又被称为”万国码”,能够表示很多很多的字符,具体的个数还在持续增加,目前根据WIKI上的说法,截至2017年6月已经增加到了13万个字符了。
所谓字符集,当然是想要多少有多少了,因此没有“Unicode能表示的最多字符数”这个概念。当需要增加新字符的时候,大不了把表格增加几行,然后对外发布个声明罢了。
Unicode有一个默认的编码叫UCS-2
这个概念是非常坑的,正式因为Unicode有一个默认的编码UCS-2(Universal Character Set),因此才导致了概念的混乱。我们可以在很多地方看见所谓“Unicode编码”这个概念,其实他们说的不是Unicode字符集,而是UCS-2编码。这种编码方式就像我之前举的第一个例子类似,是一种定长的编码方式,每一个字符都用两个字节来表示。这就导致了他最多只能表示2^16个字符。因此很多地方提到说”Unicode编码最多能表示65536个字符”,其实指的是UCS-2编码。
显然,这种编码方式并不具备较好的扩展性。我们前面提到Unicode已经有13万个字符了,显然UCS-2编码搞不定了。因此当前很多系统都不会默认用UCS-2编码,而是用扩展性更好的UTF-8编码,不过在windows中还是经常会用到Unicode(UCS-2)编码。
UTF-8是Unicode字符集上的编码
其实UTF-8跟UCS-2一样,都是Unicode字符集上的编码,不过UTF-8使用的方式更像我上面举的第二个例子。采用UTF-8编码的字符有可能占用1个字节,比如ACSII码,也可能占用2-3字节,比如中文,也有可能占用4个以上字节,比如中日韩的一些超大字符集里的文字。正是由于UTF-8采用的变长编码,因此他能够更有扩展性,被用的也最广。
具体的编码方式这里就不多说了,网上资料有很多。
Java的字符支持
支持方式
既然知道了字符集的相关知识,就有必要了解一下在具体的编程工作中的注意点了。我们知道Java是原生支持Unicode的,他默认采用的就是UTF-8编码来处理文件以及存储字节码。一个最具体的表现就是,在java中,我们可以将一个中文赋值给一个char,而在C中,这样的操作是会报warning,并且中文会乱码的。
我们知道Java有个InputStreamReader,他的作用就是将从文件读取的字节流转化为字符流。他读取InputStream中的字节流,并且对他进行字符解码。我们可以通过在这里指定编码方式从而对编码流程进行控制。比如这样:
1 | InputStream inputStream = new FileInputStream("/home/myths/examples.desktop"); |
通过InputStreamReader,所有的字节都被转化成了Unicode字符集保存在了内存里。
在默认情况下,Java采用的就是UTF-8的编码解码方式,当我们需要用指定的解码方式去解析文件的时候,我们就可以在这里进行指定。
Java支持的字符集
我们在指定字符集的时候需要注意,这些字符集一定要被Java支持,否则就会抛出UnsupportedEncodingException”。事实上对与这些字符集的名字,Java能够做到不区分大小写,忽略横线之类的辅助字符,不过我们最好还是写成标准形式。他的标准形式可以如下获取:
1 | Charset.availableCharsets().keySet().forEach(System.out::println); |
编码方式预测
很多情况下,当我们拿到一堆乱码的文件时,我们非常想知道这玩意的编码方式到底是啥。。。其实原则上来说,这种事情目前是无法精确办到的。一个最极端的例子就是纯ASCII码文件,绝大多数的编码方式都支持ASCII码单字节保存,那么给你一个纯ASCII码文件,你可以说他是ASCII码编码,也可以说他是UTF-8编码。不过windows的团队耍了一个小聪明,当我们用他的记事本去保存文件的时候,他会在文件开头加上三个字节的标记,告诉windows说这是啥编码方式。这三个字节就叫万恶的BOM。在windows下BOM这个东西还是很管用的,可是到了其他环境下,就会发现多出三个空白字符,很多命令都会解析失败,这就非常讨厌了。。。
事实上,虽然没有一个绝对准确的编码方式的预测方法,但是还是会有一些统计规律的,有了这些规律,我们就有了一些工具。
file命令
file命令是Linux自带的文件信息查看工具,我们可以用这个命令来简单查看文件的编码方式:
1 | myths@pc:~$ file -bi test.txt |
uchardet
uchardet是一个开源的工具,据说比file的更准。
1 | myths@pc:~$ uchardet test.txt |
chardet
chardet是一个python小脚本,调用的是python的函数,准确性也不错,而且还提供置信度供我们参考。
1 | myths@pc:~$ chardet test.txt |
说白了这些方法基本都是用概率统计的方法估计字符集的,所以非ascii的字符越多,判断的就会越准。
编码转换工具
有时候我们可能希望将文件的编码方式进行转换。需要注意的是,所谓的转换文件的编码,其实包括下面几个步骤:
- 读取二进制流,
- 按照旧的编码规则进行解码成统一的字符集
- 根据字符集,按照新的编码规则进行编码成新的二进制流
- 将二进制流写入文件
因此在进行编码格式转化的时候实际上就修改了文件本身,这一点需要注意。
转换编码最简单的方法其实可以通过iconv这个命令来进行处理:
1 | myths@pc:~$ iconv -f UTF-8 -t GBK sourcefile > outputfile |
通过-f指定旧的解码方式,通过-t指定新的编码方式,并将结果输出到新的文件中。
综合实践
下面做一个小实验。我们现在有如下的乱码数据,问这些数据是用什么编码的,他的正确编码方式应该是什么。
由于乱码的字符复制粘贴会影响二进制表示,因此我们通过指定二进制的方式来生成测试文件。
1 | echo -e "\xb5\xf3\xbc\xd2\xba\xc3\xa3\xac\xce\xd2\xca\xc7\xcb\xa7\xb8\xe7\xa3\xac\xbb\xb6\xd3\xad\xb4\xf3\xbc\xd2\xba\xcd\xce\xd2\xd7\xf6\xc5\xf3\xd3\xd1\xa1\xa3"> guess |
那么这个guess里装的到底是啥呢?
参考资料
linux-check-change-file-encoding
JAVA字符串与字符编码处理的终极解决
字符编码笔记:ASCII,Unicode和UTF-8
uchardet