在前面的文章golang正则之命名分组中介绍了如何在go语言中使用正则命名分组。

最近也用到了这个知识点, 结合实际例子, 看看如何使用它。

问题描述

简单来说, 就是对几种路由器的ping的结果提取。以Cisco的某型号的ping结果为例子来看下:

cisco设备 ping结果
成功时:
Success rate is 100 percent (1000/1000), round-trip min/avg/max = 1/1/4 ms

失败时
Success rate is 0 percent (0/10)

需要提取的字段: 发包数sent, 收包数received, rtt之min/avg/max三个。 共5个字段。

分析

要区分成功和失败的情况

代码

基本的结构及正则定义, 根据实际ping的结果来定义正则。
注意: 无用的分组可用 非捕获分组(?:xxxx)来匹配, 保持各种类型的设备的正则 捕获分组数一致。

package utils

import (
	"fmt"
	"regexp"
	"strconv"
)

const (
	pingSent     = "sent"
	pingReceived = "received"
	pingMin      = "min"
	pingAvg      = "avg"
	pingMax      = "max"
)

type PingResultDetail struct {
	// ping结果 原始数据(只取有用的)<br>
	// 如: Success rate is 100 percent (1000/1000), round-trip min/avg/max = 1/1/4 ms
	Source string `json:"source"`

	// 总共发送的包
	TotalSent int `json:"total_sent"`

	// 总共收到的包
	TotalReceived int `json:"total_received"`

	// 成功率(TotalReceived/TotalSent), 值范围: [0, 1]
	SuccessRate float64 `json:"success_rate"`

	// round-trip min, 单位毫秒
	Min float64 `json:"min"`

	// round-trip avg, 单位毫秒
	Avg float64 `json:"avg"`

	// round-trip max, 单位毫秒
	Max float64 `json:"max"`
}

// 对各厂商的设备上的ping结果统一:
// 共5个字段:
// sent: 发出的包个数
// received: 接收到的包的个数
// rtt共三个字段
// min/avg/max: 最小/平均最大往返时间
var (
	// cisco设备 ping结果
	// 成功时:
	// Success rate is 100 percent (1000/1000), round-trip min/avg/max = 1/1/4 ms
	//
	// 失败时:
	// Success rate is 0 percent (0/10)
	reCiscoPingResult = regexp.MustCompile(`Success rate is \d+ percent \((?P<received>\d+)/(?P<sent>\d+)\)(?:, round-trip min/avg/max = (?P<min>\d+(?:\.\d+)?)/(?P<avg>\d+(?:\.\d+)?)/(?P<max>\d+(?:\.\d+)?) ms)?`)

	// h3c设备 ping结果
	// 成功时:
	// ...
	// --- Ping statistics for a.b.c.e ---
	// 5 packet(s) transmitted, 5 packet(s) received, 0.0% packet loss
	// round-trip min/avg/max/std-dev = 0.277/0.362/0.537/0.099 ms
	//
	// 失败时:
	// 	Request time out
	//
	// --- Ping statistics for a.b.c.e ---
	// 	5 packet(s) transmitted, 0 packet(s) received, 100.0% packet loss
	reH3CPingResult = regexp.MustCompile(`` +
		`(?P<sent>\d+) packet(?:\(s\)|s) transmitted, (?P<received>\d+) packet(?:\(s\)|s) received, \d+(?:\.\d+)?% packet loss(?:(?:\s+)` +
		`round-trip min/avg/max/std-dev = (?P<min>\d+(?:\.\d+)?)/(?P<avg>\d+(?:\.\d+)?)/(?P<max>\d+(?:\.\d+)?)/\d+(?:\.\d+)? ms)?`)

	// huawei设备 ping结果
	// 成功时:
	// --- a.b.c.e ping statistics ---
	// 100 packet(s) transmitted
	// 100 packet(s) received
	// 0.00% packet loss
	// round-trip min/avg/max = 60/78/180 ms
	// 失败时:
	// --- a.b.c.e ping statistics ---
	// 100 packet(s) transmitted
	// 100 packet(s) received
	// 0.00% packet loss
	reHuaweiPingResult = regexp.MustCompile(`` +
		`(?P<sent>\d+) packet(?:\(s\)|s) transmitted(?:\s+)` +
		`(?P<received>\d+) packet(?:\(s\)|s) received(?:\s+)` +
		`\d+(?:\.\d+)?% packet loss(?:\s+)` +
		`(?:round-trip min/avg/max = (?P<min>\d+(?:\.\d+)?)/(?P<avg>\d+(?:\.\d+)?)/(?P<max>\d+(?:\.\d+)?) ms)?`)
)

重点方法:

  • reObj.SubexpNames()来获取正则的命名分组的名称列表
  • 本代码中的extractMatch()方法计算 捕获组名称和相应匹配值的映射
func GetPingResultDetail(pingResult string) (detail PingResultDetail) {
	// 初始化为-1, 表示未知
	detail.Min, detail.Avg, detail.Max = -1, -1, -1

	const expectedMatchLen = 6 // 如果正常, 会匹配到6个分组(包括原串)
	var match []string
	// groupNames := reCiscoPingResult.SubexpNames() // 所有捕获组的名字

	extractMatch := func(groupNames []string, match []string) (groupMapping map[string]string) {
		groupMapping = make(map[string]string)
		for i, v := range match {
			if i != 0 {
				// 如: {sent: "5"}
				groupMapping[groupNames[i]] = v
			} else {
				detail.Source = v // 第0个为正则刚好匹配的串(即为便于阅读的ping结果)
			}
		}
		return
	}

	var groupMapping map[string]string // 捕获组的名字 => 相应的值
	// 三个厂商的挨个尝试
	res := []*regexp.Regexp{reCiscoPingResult, reH3CPingResult, reHuaweiPingResult}
	for _, re := range res {
		if match = re.FindStringSubmatch(pingResult); len(match) == expectedMatchLen {
			groupMapping = extractMatch(re.SubexpNames(), match)
			break
		}
	}
	if groupMapping == nil {
		// 外部可通过 detail.Min==-1来判断是否匹配到了
		detail.Source = pingResult
		return
	}

	// 到这里来, 说明三个厂商有一个匹配到了
	detail.TotalSent, _ = strconv.Atoi(groupMapping[pingSent])
	detail.TotalReceived, _ = strconv.Atoi(groupMapping[pingReceived])
	if len(groupMapping[pingMin]) > 0 {
		detail.Min, _ = strconv.ParseFloat(groupMapping[pingMin], 64)
	}
	if len(groupMapping[pingAvg]) > 0 {
		detail.Avg, _ = strconv.ParseFloat(groupMapping[pingAvg], 64)
	}
	if len(groupMapping[pingMax]) > 0 {
		detail.Max, _ = strconv.ParseFloat(groupMapping[pingMax], 64)
	}

	if detail.TotalSent > 0 {

		// 计算成功率
		detail.SuccessRate = float64(detail.TotalReceived) / float64(detail.TotalSent)
		// 保留两位小数
		detail.SuccessRate = Retain2(detail.SuccessRate)
	}

	return
}

// 保留两位小数
func Retain2(f float64) float64 {
	f, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", f), 64)
	return f
}

通过上述的GetPingResultDetail()方法, 可以对ping的结果(其他任何格式的都一样)做一个统一处理。
以后如果要增加别的类型的设备, 方法签名不用变, 增加相应的正则即可。
甚至想要做的更灵活的话, 正则可由外部传入, 这样就可以做成类似于模板的东西, 想要结构化某种特定
格式文本, 就非常简单了。

最终的结果可以是:

  • 动态定义正则
  • 根据相应的文本结构化数据
  • 输出结构化的数据

总结

正则很强大, 用好了可以省很多时间。

(完)