PS: 后来发现这方法实在太蠢了,还是 awk 靠谱,具体戳这里

接到一个日志回收记录的活,本来想着用 node 或者 python 快速搞定就好了, 结果线上机居然啥都没有,看来只能用 shell 了,正巧之前都没怎么写过太复杂的 shell, 刚好练下手。

读取文件

shell 读取文件有很多方法,可以用 read line 也可以用 cat,怎么顺手怎么来就好了。 由于提取日志须要做正则,为了避免做太多多余的匹配,使用的是 grep 来过滤掉不带有回收标记的日志,然后再用管道传给 sed 方法提取须要的字段。

touch $outputfile
grep 'statisticsLog\[' $inputfile | \
sed '正则过滤' \
>> $outputfile

过滤数据

过滤数据时使用的是 sed 命令,回到了熟悉的正则表达式,本以为一下子就搞定了, 但结果写了一下午才把这个正则写完。 sed 支持的正则跟 js 差别还是很大的,主要是两点

  1. 诡异的转义。 这个主要还是我太先入为主了,照着 js 那规则来写,怎么都不对, 后来还是老老实实去看 sed 手册 才搞明白哪里错了,原来 * + ? {n, m} 这些里面,其他的都要转义,唯独星号不转义才是特殊字符, 就不能统一一下吗
  2. 非贪婪匹配。 日志的格式大概是 key[value] key[value] 这样的一串,过滤的目标是只把须要的几个 key 提取出来。如果允许非贪婪匹配的话,还蛮简单的 key[.*?] 就裁出来了。 但不能用 *? 的话,就会变成 key[[^[]*] 这还没考虑数据中的方括号的转义呢。 写起来还是很蛋疼的

最后正则写出来差不多是这样的

sed 's/^.*\(key1\[[^\[]*\]\).*$/\1/g'

确实挺难看的

PS: 另外就是用 markdown 写东西的时候,* 号的转义也是特别的难受,规则十分诡异。 md 文件里面双星号是粗体,所以星号相当于特殊字符,如果平时使用的话是要带上斜杠转义的。 但写正则的时候就会很尴尬了,由于是写在代码块里面,所以所有转义都没用, 都会显示出来,但 vim 的语法插件还会会把它当成一个粗体/斜体的边界,到现在还没有找到很好的方法解决这个问题。

定时任务

linux 开发机用 crontab 还是很方便的,但就是要自己跑上线上机上去操作,总是感觉有点虚。 crontab 用起来方便是方便,就是线上机跑脚本,权限的问题太蛋疼,还是自己做 op 方便一些。 定时跑一份脚本,将线上日志过滤后备份起来。

merge 日志

在各个机器上生成备份日志之后,还要找一台开发机把所有日志 merge 起来,同样的, 也是使用 crontab 进行日志的收集。下载日志很简单,直接放一个有 http 服务的地方, 然后 webget 就好了,比较麻烦的是按时间 merge 日志的操作。 日志的每一行大概是这样

    time[1478247635187] key1[xx] key2[yy] key3[zz]

须要做的工作是切出 time 字段的值,再进行归并操作。但 shell 里面的数据结构非常简单, 基本上就是简单的变量还有数组。

如果将日志文件一口气读进来,内存是肯定装不下的,但如果每次只读一行, 进行归并,访问文件的次数又会非常多,非常的尴尬。

所以决定换个方案,直接把几个日志文件写到一起,然后再使用 awk 排序,这样就不用管太多的排序细节, 放着晚上跑跑排序就好了,只要把日志格式改一下,去掉字段名,用特殊符号分隔字段并不是很复杂。

PS: 不过最后在不懈努力下,总算在服务器上能用 python 了,可以直接拿到文件句柄, 归并数据也就不是很么大问题了。大概是这样

    import re
    import os

    // 文件列表
    fileNameList = [];
    fileList = [];

class LogFile:
    def __init__(self, fileName):
        '''打开文件'''
        self.fileObj = open(fileName)
        self.currentLine = (self.fileObj.readline()) if (self.fileObj) else None
        self.currentTime = re.search(r'begin_time\[(\d*?)\]', self.currentLine).group(1) \
            if self.currentLine else None
    def nextLine(self):
        '''读取文件下一行'''
        if self.currentLine:
            self.currentLine = self.fileObj.readline()
            self.currentTime = re.search(r'begin_time\[(\d*?)\]', self.currentLine).group(1) \
                if self.currentLine else None
def isDone():
    "是否已经读完所有文件"
    for logFile in fileList:
        if logFile.currentLine:
            return False
    return True

def getMinFile():
    "获取当前时间最小的文件"
    minFile = None
    for logFile in fileList:
        if (not logFile.currentTime):
            # 跳过已经读完的文件
            continue
        if (not minFile):
            minFile = logFile
        else:
            minFile = logFile \
                if logFile.currentTime < minFile.currentTime \
                else minFile
    return minFile

def mergelog(logTime, logPath):
    '''合并日志文件
    logTime 字符串,日志时间 eg. "2016110814"
    logPath 字符串,日志所在目录 eg. "./xxxxxx2016/11/08/"
    '''
    # 过滤得到指定时间的日志
    allLogFiles = os.listdir(logPath)
    for item in allLogFiles:
        if item.count(logTime) and not item.count('merged'):
            fileNameList.append(logPath + item)
    # 初始化文件对象
    for item in fileNameList:
        fileList.append(LogFile(item))
    print fileList
    # 输出合并后的日志
    outputFile = open(logPath + logTime + '.merged', 'w+')
    while (not isDone()):
        minFile = getMinFile()
        outputFile.write(minFile.currentLine)
        minFile.nextLine()
    outputFile.close()

summary

shell 虽然是很方便,但局限性还是太强了,基本上没什么好用的数据结构, 稍微复杂一点的工作还是高级一点的脚本语言会方便些。也可能是我 linux 还是用的太少了, 文件访问的时候不得要领吧。