异常分析与字符串匹配笔记

环境

android 系统

语言

java

场景

采集android平板在发生内核级崩溃时记录的异常日志文件。经过了解在发生内核级崩溃时系统会把异常信息记录在”/proc/last_log”文件中,该文件是恒大小(512K)的。那么我们要做的就是在每次开机启动并且联网时查询该日志是否记录了异常,如果否则不处理;如果是则通过网络将这个文件上传到服务器。本文主要关注分析日志文件这一块。
需求:有一个异常日志文件,需要分析出该文件中是否记录了某种异常,该异常有一个标志字符串“EXCEPTION _ABC”。即分析出一个文件中是否包含一个字符串“EXCEPTION _ABC”。

分析

把任务拆开了,就显得简单了,分治法也算是程序员的基本功。分到日志分析这一步,看起来倒是不难,无非是把文件内容独到内存中,再匹配是否有目标字符串,因为目前的需求还不需要仔细分析出是什么异常,只需要返回是否有异常就行,所以匹配起来应该还是挺轻松。可是到真正要实现的时候才发现问题还有不少,比如android下使用的是java,所以不少事情都不需要亲力亲为,直接调API搞定就行,那么问题就出来了,读文件的方式不少,字符串匹配的方法也不少,那怎么用才能保证省时省力呢?再比如,现在的环境很单一,固定路径下固定大小的文件拿来做一个字符串匹配,那么在实现的时候是否要考虑下通用性呢,就是说换到其他场景这个文件字符串匹配可以直接复用。

实现

一步一步来,首先是文件的读取策略。是按行读还是按块读,按行就是每次处理一行,也就是说下一步——字符串匹配这部分就是有多少行就要处理多少次,我想当然的认为这可能会耗费不少时间。但是按行读有个好处就是可以精确定位到行,如果后续还需要拓展一下要提取出错误码什么的,处理起来就方便了;按块读,唔,这个说法貌似不是很正确,就是读几次,或者说每次读N K大小的数据,一块一块去匹配。比如一次处理10K,那512K的文件处理个几十次,似乎效率能高一些,当然内存充足的话一次把文件都读进去也不是不行,这东西就是个时间与空间的互换,然后找个最合适的点。
然后是字符串的匹配方法,开始想到了两种,一种是直接用String类的contain方法;一种用正则表达式来匹配。后来在做分块处理文件时,读出的文件信息是byte数组,就又抄了段KMP算法的实现来匹配。总结一下,再抄抄代码,弄出三种实现来。
三种实现方法实现如下:

第一种,按行读取,String.contain 方法匹配:

private boolean analysisContain(String strFilePath) {
    final String tag = "tagContain";
    boolean isException = false;
    String path = strFilePath;
    // 打开文件
    File file = new File(path);
    // 如果path是传递过来的参数,可以做一个非目录的判断
    if (file.isDirectory()) {
        Log.d(tag, "The File doesn't not exist.");
    } else {
        String set = "GBK";// "UTF-16BE";// charsetDetector(file);
        try {
            InputStream instream = new FileInputStream(file);
            if (instream != null) {
                InputStreamReader inputreader = new InputStreamReader(instream, set);
                BufferedReader buffreader = new BufferedReader(inputreader);
                String line;
                int lineCount = 0;
                // 分行读取
                while ((line = buffreader.readLine()) != null) {
                      lineCount++;
                      if (line.contains(EXCEPTION_TAG)) {
                           Log.v(tag, lineCount + ":" + line);
                           isException = true;
                           break;
                      }
                }
                Log.d(tag,  "line count:" + lineCount);
                instream.close();
            }
        } catch (java.io.FileNotFoundException e) {
              Log.d(tag, "The File doesn't not exist.");
         } catch (IOException e) {
            Log.d(tag, e.getMessage());
        }
    }
    return isException;
}

第二种,按行读取,正则匹配:

private boolean analysisRegex(String strFilePath) {
    final String tag = "tagRegex";
    String regEx = EXCEPTION_TAG;
    Pattern p = Pattern.compile(regEx);
    boolean isException = false;
    String path = strFilePath;
    // 打开文件
    File file = new File(path);
    // 如果path是传递过来的参数,可以做一个非目录的判断
    if (file.isDirectory()) {
        Log.d(tag, "The File doesn't not exist.");
    } else {
        String set = "GBK";// "UTF-16BE";// charsetDetector(file);
        try {
            InputStream instream = new FileInputStream(file);
            if (instream != null) {
                InputStreamReader inputreader = new InputStreamReader(instream, set);
                BufferedReader buffreader = new BufferedReader(inputreader);
                String line;
                int lineCount = 0;
                // 分行读取
                while ((line = buffreader.readLine()) != null) {
                    lineCount++;
                    Matcher m = p.matcher(line);
                    if (m.find()) {
                        Log.v("tag", lineCount + ":" + line);
                        isException = true;
                        break;
                    }
                }
                Log.d(tag,  "line count:" + lineCount);
                instream.close();
            }
        } catch (java.io.FileNotFoundException e) {
            Log.d(tag, "The File doesn't not exist.");
        } catch (IOException e) {
            Log.d(tag, e.getMessage());
        }
    }
    return isException;
}

第三种,每次读10K,KMP匹配:

private boolean analysisRandomAccess(String strFilePath) {
    final int byteCountOnce = 10 * 1024;
    final byte[] beginTag = EXCEPTION_TAG.getBytes();
    final byte[] endTag = "PREEMPT SMP".getBytes();
    boolean isException = false;

    RandomAccessFile randomFile = null;
    try {
        // 打开一个随机访问文件流,按只读方式
        randomFile = new RandomAccessFile(strFilePath, "r");
        byte[] bytes = new byte[byteCountOnce];
        int readCount = 0;
        while (randomFile.read(bytes) != -1) {
            readCount ++;
            //String matched = searchKMP(bytes, beginTag, endTag);
            //if (matched != null) {
            if(search(bytes, beginTag)){
                //Log.d("tag", new String(beginTag) + matched + new String(endTag) );
                isException = true;
                break;
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (randomFile != null) {
            try {
                randomFile.close();
            } catch (IOException e1) {

               }
        }
    }

    return isException;
}


//在字节数组bytes中,从offset位置开始匹配sub字节序列,
//返回第一次匹配成功时sub的位置 。失配返回-1。

public static int index(byte[] bytes, byte[] sub, int offset) {
    int i = offset, j = 0;
    int next[] = getNext(sub);

    while (i < bytes.length - 1 && j < sub.length - 1) {
        if (j == 0 || bytes[i] == sub[j]) {
            i++;
            j++;
        } else{
            j = next[j] - 1;
        }
    }

    if (j > sub.length - 2) {
        return (i - sub.length + 1);
    } else {
        return -1;
    }
}

private static int[] getNext(byte[] sub) {
    int i = 0, j = -1;
    int[] next = new int[sub.length];
    while (i < next.length - 1) {
        if (j == -1 || sub[i] == sub[j]) {
            i++;
            j++;
            next[i] = sub[i] == sub[j] ? next[j] : j + 1;
        } else {
            j = next[j] - 1;
        }
    }

    return next;
}

private boolean search(byte[] src,byte[] matched){
    if(index(src,matched,0) != -1){
        return true;
    }else{
        return false;
    }
}

测试

本着“是骡子是马牵出来溜溜”的这种实事求是的态度,针对这三种实现写了一个测试用例,丢到测试工程里跑一跑。一共在固定路径下放了七种文件上去,三个是带有异常的日志文件(但是异常位置各不相同),一个普通无异常文件,一个异常乱码日志文件(某种情况下那个诡异的日志会出现乱码,貌似是系统bug),还有两个大号的日志文件(大小5M,是正常情况的10倍,一个带异常一个不带)。测试用例代码如下:

public void testFileAnalysis() throws Exception {
    final String tag = "tagFileAnalysis";
    final String testFileDir = Environment.getExternalStorageDirectory().getPath() + "/testlog/";
    final String[] testFileName = new String[]{"lastlog1","lastlog2","lastlog3"
    ,"lastexception","lastnormal","lastnormalex","lastlogex"};
    final String[] testFileInfo = new String[]{"带有异常的日志","带有异常的日志","带有异常的日志","乱码日志","无异常的日志","大号无异常日志 5M","大号带异常日志 5M"};
    boolean ret0 = false;
    boolean ret1 = false;
    boolean ret2 = false;
    long beginTime = 0;

    for(int index = 0; index < testFileName.length; index ++){
        String testFilePath = testFileDir + testFileName[index];
        Log.d(tag, testFilePath + "  " + testFileInfo[index]);

        beginTime = System.currentTimeMillis();
        ret0 = analysisContain(testFilePath);
        Log.d(tag, "analysis Contain:" + ret0);
        Log.d(tag, "cost time:" + (System.currentTimeMillis() - beginTime));
        Log.d(tag, "--------------------------------------------------------");

        beginTime = System.currentTimeMillis();
        ret1 = analysisRegex(testFilePath);
        Log.d(tag, "analysis Regex:" + ret1);
        Log.d(tag, "cost time:" + (System.currentTimeMillis() - beginTime));
        Log.d(tag, "--------------------------------------------------------");

        beginTime = System.currentTimeMillis();
        ret2 = analysisRandomAccess(testFilePath);
        Log.d(tag, "analysis RandomAccess:" + ret2);
        Log.d(tag, "cost time:" + (System.currentTimeMillis() - beginTime));
        assertEquals(ret0, ret1);
        assertEquals(ret0, ret2);
        Log.d(tag, "========================================================");
    }
}

测试结果如下:

总结

当然这并不是全部,因为中间还尝试了一些其他方面的组合。在保证分析结果正确的前提下,可以得出一下结论:
按行的情况下,老老实实的直接使用contain方法,正则表达式在这种情况下纯属牛刀杀鸡,既不方便也不实用。七种情况只有乱码文件的情况下正则扳回一局,其余的都是眼泪,原因是乱码文件的情况下是当作一行处理的,可以想象这个效率曲线,单纯的字符串匹配在一定数量的数据一下contain效率高,在这之上正则的效率就要高些;
没有异常的情况就意味着要遍历整个文件,这个时候分块处理采用KMP匹配的办法效率要高,尤其是文件变大之后尤为明显。实际上大部分情况这个日志都应该是没什么异常的,因为如果内核总是崩溃的话,产品根本无法上市;

在这个实现中暴露了一些问题,那就是一些基础还不够,对读文件、字符串匹配等基础的API特性和效率还认知不足。虽然说各路API浩如烟海,但是一些基础功能还是要做到耳熟能详,这样在设计的过程中就可以免去以上那些验证的过程。在编码之前要做好代码设计,这一番算计所设计出来的结果,除了整个模块的框架之外还在于各个节点的细节,如果连每个功能点的实现细节和效率情况都掌握不了就谈不上能做出好的设计。所以细节方面还需要努力积累。再有就是合适的东西做合适的事情,大到选择一门编程语言,小到选择一个字符串匹配的方法,都是如此。

感谢您赏个荷包蛋~