在前面的文章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的结果(其他任何格式的都一样)做一个统一处理。
以后如果要增加别的类型的设备, 方法签名不用变, 增加相应的正则即可。
甚至想要做的更灵活的话, 正则可由外部传入, 这样就可以做成类似于模板的东西, 想要结构化某种特定
格式文本, 就非常简单了。
最终的结果可以是:
- 动态定义正则
- 根据相应的文本结构化数据
- 输出结构化的数据
总结
正则很强大, 用好了可以省很多时间。
(完)