正则表达式返回完整行而非匹配项

5
我正在尝试从文本文件中提取出一个日期。以下是该内容:

存储管理器 命令行管理员界面 - 版本7,发布1,级别1.4 版权所有1990, 2015公司和其他人。版权所有。

与服务器TSERVER建立的会话:Windows Server版本7,发布1,级别5.200 服务器日期/时间:11/22/2016 15:30:00 最后访问:11/22/2016 15:25:00

ANS8000I服务器命令。

我需要提取server date/time之后的日期/时间。我已经写了这个正则表达式:
/([0-9]{1,2}\/[0-9]{1,2}\/[0-9]{4} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2})/

这个在regex101上可以完美地运行。请参见https://regex101.com/r/MB7yB4/1上的示例。 然而,在Powershell中,它的反应是不同的。

$var -match "([0-9]{1,2}\/[0-9]{1,2}\/[0-9]{4} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2})"

提供

服务器日期/时间:2016年11月22日 16:30:00 最后访问时间:2016年11月22日 15:37:19

$var -match "([0-9]{1,2}\/[0-9]{1,2}\/[0-9]{4} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2})"

没有任何返回结果。

我不确定为什么匹配结果不一致。
谢谢帮助!


if($var -match '[0-9]{1,2}/[0-9]{1,2}/[0-9]{4} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}') { $Matches[0] } - Wiktor Stribiżew
你的第二个-match示例命令与第一个完全相同。请编辑它以显示真正未产生任何结果的变化(或者只需删除第二个命令)。 - mklement0
5个回答

2
< p > -match 运算符返回一个布尔值,显示是否找到匹配项。同时,它使用匹配数据(整个匹配和捕获组值)设置了$matches变量。您只需要访问整个匹配项:

if($var -match '[0-9]{1,2}/[0-9]{1,2}/[0-9]{4} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}') { $matches[0] }

请参阅在PowerShell中使用-match$matches变量。请注意,在PowerShell正则表达式中不需要转义/符号,因为这个字符不是特殊字符,并且在定义正则表达式时不使用正则表达式定界符(就像JS、PHP正则表达式中的外部/.../)。

谢谢!我尝试过了,但是我得到了“无法索引到空数组”的错误。 - mitch2k
1
错误提示表明$matches不是一个数组或者是一个空数组。请注意,我使用您发布的文本进行了测试,并且找到了匹配项。请分享您正在使用的完整代码。此外,请尝试在模式中使用\s代替字面上的空格,但我不认为这应该是问题的罪魁祸首。 - Wiktor Stribiżew
1
好的答案,但请注意,-match 只在 标量 左侧(单个字符串)返回一个布尔值。对于 数组 左侧,它返回 _匹配的数组元素_(在本例中为整行从文件中读取的内容,如问题所示)。 - mklement0

2
为了补充Wiktor Stribiżew的有用提示和有效解决方案,但没有解释-match运算符在数组输入中的行为:
  • -match运算符的行为会改变,如果LHS是一个字符串数组,则返回匹配的数组元素而不是布尔值,并且$Matches变量不会被填充。实际上,-match执行的是数组过滤。
    • 你可能只使用Get-Content将文件内容读入$var,它将以字符串数组而不是单个字符串的形式返回行。在PSv3+中,添加-Raw开关会将整个文件作为单个字符串读取。
    • 你的正则表达式仅匹配了输入数组的第5个元素(文件的第5行),因此该元素(整个行)被返回。
  • 如Wiktor的答案所解释的那样,为了访问自动生成的$Matches哈希表的条目,需要访问有关最近使用-match捕获的信息:$Matches[0]包含整个正则表达式捕获的内容,$Matches[1]包含第一个(未命名的)捕获组捕获的内容(第2个为$Matches[2],...),对于命名捕获组,使用$Matches['<name>'],如LotPing's helpful answer所示。(例如,$Matches.0$Matches[0]的另一种语法)。
  • 最好使用单引号字符串('...')定义正则表达式,这样 PowerShell 自己应用于双引号字符串("...")的字符串插值不会妨碍匹配。

说到使用正则表达式提取子字符串时,使用-replace通常可以提供更简洁的解决方案:
$var -join "`n" -replace '(?s).*?(\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{1,2}:\d{1,2}).*', '$1'

额外的-join "`n"步骤是必要的,以重新组装$var中的行数组为一个字符串以作为-replace的输入。下面的解释说明了如何使用Get-Content -Raw将整个文件作为一个单一的字符串读取。
# Read the text file as a *single* string, using -Raw.
# Note: Without -Raw, you get an *array* of strings representing 
#       the individual lines.
$var = Get-Content -Raw file.txt

# Define the regex that matches the *entire* input,
# with a single capture group capturing the substring of interest.
# The regex:
#   - is prefixed with an inline-option expression, (?s), which ensures
#     that . also matches a newline.
#   - starts with .*? a non-greedy expression matching any
#     sequence of characters at the start of the input,
#   - followed by the original capture-group regex (though without escaping of / as \/,
#     because that is not necessary in PowerShell, and \d used instead of [0-9])
#   - ends with .*, a greedy expression that matches everything through the
#     end of the input.
$re = '(?s).*?(\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{1,2}:\d{1,2}).*'

# Using -replace, we replace the entire input string - by virtue
# of the overall regex matching the entire string - with only 
# what the capture group captured ($1).
# The net effect is that only the capture group value is output.
# With the sample input, this outputs '1/22/2016 15:30:00', the first
# timestamp encountered.
$var -replace $re, '$1'

1
那个“原始注释”真是无价之宝。我不明白为什么我的“-match”返回的是一个数组,而不是像大家说的那样返回一个布尔值!谢谢。 - biso

1
由于您正在匹配多行,它会提取匹配的行,要从行中提取单个匹配项,请使用以下内容:
foreach ($line in $var) { if ($line -match "([0-9]{1,2}\/[0-9]{1,2}\/[0-9]{4} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2})") {write-output $matches[0]}}

1
如果你在处理冗长的正则表达式时,使用命名捕获组确实是有意义的。将一个正则表达式分成多个部分时,名称保持不变。当正则表达式可能跨越多行时,应使用(?smi),为了能够匹配crlf与.,你必须使用-raw选项进行get-content。我使用\d而不是[0-9]只是为了节省3个字符。
$var = Get-Content File.txt -Raw
if ($var -match "(?smi)Server date/time: (?<ServerDT>\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{1,2}:\d{1,2}).*access: (?<LastAc>\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{1,2}:\d{1,2})") { 
  "ServerDT  : "+$matches.ServerDT 
  "LastAccess: "+$matches.LastAc
}

输出

ServerDT  : 11/22/2016 15:30:00
LastAccess: 11/22/2016 15:25:00

1
在这种情况下,我仍然更喜欢直接使用.NET正则表达式类匹配方法——它更快、更精确、更详尽。如果您确定第一个日期是您要搜索的结果,您可以使用以下方法:
[regex]::Matches($var,'\d{1,2}/\d{1,2}/\d{4}\s\d{1,2}:\d{1,2}:\d{1,2}')[0].value

我个人会在正则表达式中加入"服务器日期/时间:",然后将其从结果中删除(如果需要,将清除的结果解析为DateTime对象)。
([regex]::Matches($a,'Server\sdate/time:\s\d{1,2}/\d{1,2}/\d{4}\s\d{1,2}:\d{1,2}:\d{1,2}').value) -replace "Server date/time: ",''

PS. 快速建议:即使是测试,也要避免使用 var 作为变量名。这是一个非常不好的习惯。


谢谢。使用.NET类肯定更容易,并且返回了我所需的内容。谢谢! - biso

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