使用GO语言解析日志文件

4

我是一名Go语言的新手!

我正在努力编写一个Go程序,它可以解析日志文件并返回匹配行上特定的信息。

为了举例说明我的目标,我会从这样一个日志文件开始:

2019-09-30T04:17:02 - REQUEST-A
2019-09-30T04:18:02 - REQUEST-C
2019-09-30T04:19:02 - REQUEST-B
2019-09-30T04:20:02 - REQUEST-A
2019-09-30T04:21:02 - REQUEST-A
2019-09-30T04:22:02 - REQUEST-B

从这里开始,我想提取所有的“REQUEST-A”,然后将请求发生的时间打印到终端或文件中。 我尝试了使用os.Open和scanner,我可以使用scanner.Text记录它找到了字符串的发生情况,如下所示:
package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    request := 0
    f, err := os.Open("request.log")
    if err != nil {
        fmt.Print("There has been an error!: ", err)
    }
    defer f.Close()
    scanner := bufio.NewScanner(f)

    for scanner.Scan() {
        if strings.Contains(scanner.Text(), "REQUEST-A") {
            request = request + 1
        }

        if err := scanner.Err(); err != nil {
        }
        fmt.Println(request)
    }
}

但是我不确定如何使用它来检索我想要的信息。 通常我会使用Bash,但我想尝试一下能否使用Go。 任何建议将不胜感激。


除非你需要一些bash无法实现的东西,否则请优先选择bash而不是自制的go实现。这样可以获得更好的性能和更少的错误。 - user4466350
@mh-cbon:这个问题是关于“Go程序”的,并且被标记为“Go程序”。你的建议不是答案,而是“塔可钟编程”(Taco Bell Programming)。 - peterSO
3个回答

4
在Go语言中,我们尽力提高效率。不要做不必要的事情。
例如,
package main

import (
    "bufio"
    "bytes"
    "fmt"
    "os"
)

func main() {
    lines, requestA := 0, 0
    f, err := os.Open("request.log")
    if err != nil {
        fmt.Print("There has been an error!: ", err)
    }
    defer f.Close()

    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        lines++
        // filter request a
        line := scanner.Bytes()
        if len(line) <= 30 || line[30] != 'A' {
            continue
        }
        if !bytes.Equal(line[22:], []byte("REQUEST-A")) {
            continue
        }
        requestA++
        request := string(line)

        // handle request a
        fmt.Println(request)
    }
    if err := scanner.Err(); err != nil {
        fmt.Println(err)
    }
    fmt.Println(lines, requestA)
}

输出:

$ go run request.go

2019-09-30T04:17:02 - REQUEST-A
2019-09-30T04:20:02 - REQUEST-A
2019-09-30T04:21:02 - REQUEST-A
6 3

$ cat request.log
2019-09-30T04:17:02 - REQUEST-A
2019-09-30T04:18:02 - REQUEST-C
2019-09-30T04:19:02 - REQUEST-B
2019-09-30T04:20:02 - REQUEST-A
2019-09-30T04:21:02 - REQUEST-A
2019-09-30T04:22:02 - REQUEST-B

为了强调效率的重要性(记录可能非常大),让我们对Markus W Mahlberg的解决方案运行基准测试:https://play.golang.org/p/R2D_BeiJvx9

$ go test log_test.go -bench=. -benchmem
BenchmarkPeterSO-4   21285     56953 ns/op    4128 B/op      2 allocs/op
BenchmarkMarkusM-4     649   1817868 ns/op   84747 B/op   2390 allocs/op

log_test.go:

package main

import (
    "bufio"
    "bytes"
    "regexp"
    "strings"
    "testing"
)

var requestLog = `
2019-09-30T04:17:02 - REQUEST-A
2019-09-30T04:18:02 - REQUEST-C
2019-09-30T04:19:02 - REQUEST-B
2019-09-30T04:20:02 - REQUEST-A
2019-09-30T04:21:02 - REQUEST-A
2019-09-30T04:22:02 - REQUEST-B
`

var benchLog = strings.Repeat(requestLog[1:], 256)

func BenchmarkPeterSO(b *testing.B) {
    for N := 0; N < b.N; N++ {
        scanner := bufio.NewScanner(strings.NewReader(benchLog))
        for scanner.Scan() {
            // filter request a
            line := scanner.Bytes()
            if len(line) <= 30 || line[30] != 'A' {
                continue
            }
            if !bytes.Equal(line[22:], []byte("REQUEST-A")) {
                continue
            }
            request := string(line)
            // handle request a
            _ = request
        }
        if err := scanner.Err(); err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkMarkusM(b *testing.B) {
    for N := 0; N < b.N; N++ {
        var re *regexp.Regexp = regexp.MustCompile(`^(\S*) - REQUEST-A$`)
        scanner := bufio.NewScanner(strings.NewReader(benchLog))
        var res []string
        for scanner.Scan() {
            if res = re.FindStringSubmatch(scanner.Text()); len(res) > 0 {
                _ = res[1]
            }
        }
        if err := scanner.Err(); err != nil {
            b.Fatal(err)
        }
    }
}

为什么不用正则表达式?虽然一开始会有些开销,但后续性能足够好,而且代码量也大大减少。 - Markus W Mahlberg
正则表达式与字节比较和位置操作相比较慢。 - user4466350
@mh-cbon 好的,我对这两种方法进行了基准测试(如果您想要,我可以推送代码),无论哪种方式,我们都在谈论每个操作数的一位数字µs/op。不确定这是否值得。在文本解析库中?也许是。在Go实现的awk脚本中?也许不是。 - Markus W Mahlberg
这取决于你的需求水平、输入以及你可以接受的计算时间。但是我仍然看到两种方法之间有5倍的差异。这可能会产生影响。(基准测试结果-4 100000次操作需要12350纳秒,分配了4496字节和13个对象 / 基准测试结果Byte-4 1000000次操作需要2142纳秒,分配了4304字节和7个对象) - user4466350
@MarkusWMahlberg:你的Go解决方案效率低下。我已经添加了一个基准测试。 - peterSO
有趣的是,在我的基准测试中,我得到了非常不同的值。请参见https://github.com/mwmahlberg/textparsebench - Markus W Mahlberg

1
使用以下代码打印值字段为“REQUEST-A”的日志条目的时间字段。
for scanner.Scan() {
    line := scanner.Text()
    if len(line) < 19 {
        continue
    }
    if line[19:] == " - REQUEST-A" {
        fmt.Println(line[:19])
    }
}

在Go Playground上运行它!

要写入文件,请将stdout重定向到文件。

上面的代码假定时间戳之后的所有内容都是“- REQUEST-A”。如果“- REQUEST-A”是其他数据的前缀,请使用以下内容:

const lenTimestamp = 19
for scanner.Scan() {
    line := scanner.Text()
    if len(line) < lenTimestamp {
        continue
    }
    if strings.HasPrefix(line[lenTimestamp:], " - REQUEST-A") {
        fmt.Println(line[:lenTimestamp])
    }
}

在游乐场上运行此版本


0

如果您使用的是Linux或Mac系统,则无需编写Go程序:

$ echo "2019-09-30T04:17:02 - REQUEST-A
2019-09-30T04:18:02 - REQUEST-C
2019-09-30T04:19:02 - REQUEST-B
2019-09-30T04:20:02 - REQUEST-A
2019-09-30T04:21:02 - REQUEST-A
2019-09-30T04:22:02 - REQUEST-B" | awk '/REQUEST-A/{print $1}' | tee request.log
2019-09-30T04:17:02
2019-09-30T04:20:02
2019-09-30T04:21:02

然而,如果你真的想在Go中实现这个:

package main

import (
    "bufio"
    "fmt"
    "regexp"
    "strings"
)

const input = `
2019-09-30T04:17:02 - REQUEST-A
2019-09-30T04:18:02 - REQUEST-C
2019-09-30T04:19:02 - REQUEST-B
2019-09-30T04:20:02 - REQUEST-A
2019-09-30T04:21:02 - REQUEST-A
2019-09-30T04:22:02 - REQUEST-B
`

var scanner = bufio.NewScanner(strings.NewReader(input))

// Here comes the magic: We create an anonymous group containing all
// non-whitespace characters up to the first blank. Since it is
// a group, we can easily extract it later down the road.
var re *regexp.Regexp = regexp.MustCompile(`^(\S*) - REQUEST-A$`)

func main() {
    var res []string
    for scanner.Scan() {
        // We use re.FindStringSubmatch here, as it actually kills two
        // birds with one stone: We check wether it is REQUEST-A
        // and the anonymous group of the regexp contains what we are looking for.
        if res = re.FindStringSubmatch(scanner.Text()); len(res) > 0 {
            fmt.Println(res[1])
        }
    }
}

在游乐场上运行


这个问题是关于“Go程序”的标签。你的第一个解决方案不是答案;它是Taco Bell编程。你在Go中的第二个解决方案似乎效率低下;请看我的答案。 - peterSO
@peterSO 按行计算...正如所说,我对两种解决方案进行了基准测试,虽然差距显著,但并不是数量级的差异。你的解决方案比另一个快了4倍多:平均每个操作1.4微秒对比5.8微秒。可以肯定的是,I/O操作更可能成为瓶颈,因此我选择了更少的代码行数来提高效率。我认为重要的是要跳出思维定势,按照谚语“如果你手中只有一把锤子,那么所有东西看起来都像钉子”去思考:第一个解决方案提供了所需的结果,而且没有任何实现上的麻烦。 - Markus W Mahlberg
我很想看看你的基准测试代码。请将其添加到您的回答中。 - peterSO
@peterSO 仍然有兴趣吗?抱歉,我刚刚才看到你的请求。 - Markus W Mahlberg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接