假设您有一个包含许多 xml 文档的文件,例如
<a>
<b>
...
</a>
in between xml documents there may be plain text log messages
<x>
...
</x>
...
我如何过滤此文件以仅显示给定正则表达式与该 xml 文档的任何一行匹配的 xml 文档?我在这里讨论的是简单的文本匹配,因此正则表达式匹配部分可能完全不了解底层格式 - xml。
您可以假设根元素的开始和结束标记始终位于其自己的行上(尽管可能是空白填充),并且它们仅用作根元素,即具有相同名称的标记不会出现在下面根元素。这样就可以完成工作,而不必求助于 xml 感知工具。
答案1
概括
我编写了一个 Python 解决方案、一个 Bash 解决方案和一个 Awk 解决方案。所有脚本的想法都是相同的:逐行浏览并使用标志变量来跟踪状态(即我们当前是否在 XML 子文档中以及我们是否找到了匹配的行) )。
在 Python 脚本中,我将所有行读入列表中,并跟踪当前 XML 子文档开始的列表索引,以便在到达结束标记时可以打印出当前子文档。我检查每一行的正则表达式模式,并使用一个标志来跟踪处理完成后是否输出当前子文档。
在 Bash 脚本中,我使用临时文件作为缓冲区来存储当前的 XML 子文档,并等待其写入完成,然后再grep
检查它是否包含与给定正则表达式匹配的行。
Awk 脚本与 Base 脚本类似,但我使用 Awk 数组作为缓冲区而不是文件。
测试数据文件
data.xml
我根据您问题中给出的示例数据对照以下数据文件 ( ) 检查了这两个脚本:
<a>
<b>
string to search for: stuff
</b>
</a>
in between xml documents there may be plain text log messages
<x>
unicode string: øæå
</x>
Python解决方案
这是一个简单的 Python 脚本,可以完成您想要的操作:
#!/usr/bin/env python2
# -*- encoding: ascii -*-
"""xmlgrep.py"""
import sys
import re
invert_match = False
if sys.argv[1] == '-v' or sys.argv[1] == '--invert-match':
invert_match = True
sys.argv.pop(0)
regex = sys.argv[1]
# Open the XML-ish file
with open(sys.argv[2], 'r') if len(sys.argv) > 2 else sys.stdin as xmlfile:
# Read all of the data into a list
lines = xmlfile.readlines()
# Use flags to keep track of which XML subdocument we're in
# and whether or not we've found a match in that document
start_index = closing_tag = regex_match = False
# Iterate through all the lines
for index, line in enumerate(lines):
# Remove trailing and leading white-space
line = line.strip()
# If we have a start_index then we're inside an XML document
if start_index is not False:
# If this line is a closing tag then reset the flags
# and print the document if we found a match
if line == closing_tag:
if regex_match != invert_match:
print(''.join(lines[start_index:index+1]))
start_index = closing_tag = regex_match = False
# If this line is NOT a closing tag then we
# search the current line for a match
elif re.search(regex, line):
regex_match = True
# If we do NOT have a start_index then we're either at the
# beginning of a new XML subdocument or we're inbetween
# XML subdocuments
else:
# Check for an opening tag for a new XML subdocument
match = re.match(r'^<(\w+)>$', line)
if match:
# Store the current line number
start_index = index
# Construct the matching closing tag
closing_tag = '</' + match.groups()[0] + '>'
以下是运行脚本来搜索字符串“stuff”的方法:
python xmlgrep.py stuff data.xml
这是输出:
<a>
<b>
string to search for: stuff
</b>
</a>
以下是运行脚本来搜索字符串“øæå”的方法:
python xmlgrep.py øæå data.xml
这是输出:
<x>
unicode string: øæå
</x>
您还可以指定-v
或--invert-match
来搜索不匹配的文档,并在标准输入上工作:
cat data.xml | python xmlgrep.py -v stuff
重击解决方案
这是相同基本算法的 bash 实现。它使用标志来跟踪当前行是否属于 XML 文档,并使用临时文件作为缓冲区来存储正在处理的每个 XML 文档。
#!/usr/bin/env bash
# xmlgrep.sh
# Get the filename and search pattern from the command-line
FILENAME="$1"
REGEX="$2"
# Use flags to keep track of which XML subdocument we're in
XML_DOC=false
CLOSING_TAG=""
# Use a temporary file to store the current XML subdocument
TEMPFILE="$(mktemp)"
# Reset the internal field separator to preserver white-space
export IFS=''
# Iterate through all the lines of the file
while read LINE; do
# If we're already in an XML subdocument then update
# the temporary file and check to see if we've reached
# the end of the document
if "${XML_DOC}"; then
# Append the line to the temp-file
echo "${LINE}" >> "${TEMPFILE}"
# If this line is a closing tag then reset the flags
if echo "${LINE}" | grep -Pq '^\s*'"${CLOSING_TAG}"'\s*$'; then
XML_DOC=false
CLOSING_TAG=""
# Print the document if it contains the match pattern
if grep -Pq "${REGEX}" "${TEMPFILE}"; then
cat "${TEMPFILE}"
fi
fi
# Otherwise we check to see if we've reached
# the beginning of a new XML subdocument
elif echo "${LINE}" | grep -Pq '^\s*<\w+>\s*$'; then
# Extract the tag-name
TAG_NAME="$(echo "${LINE}" | sed 's/^\s*<\(\w\+\)>\s*$/\1/;tx;d;:x')"
# Construct the corresponding closing tag
CLOSING_TAG="</${TAG_NAME}>"
# Set the XML_DOC flag so we know we're inside an XML subdocument
XML_DOC=true
# Start storing the subdocument in the temporary file
echo "${LINE}" > "${TEMPFILE}"
fi
done < "${FILENAME}"
以下是运行脚本来搜索字符串“stuff”的方法:
bash xmlgrep.sh data.xml 'stuff'
这是相应的输出:
<a>
<b>
string to search for: stuff
</b>
</a>
以下是运行脚本来搜索字符串“øæå”的方法:
bash xmlgrep.sh data.xml 'øæå'
这是相应的输出:
<x>
unicode string: øæå
</x>
awk解决方案
这是一个awk
解决方案 -awk
虽然我的不是很好,所以它很粗糙。它使用与 Bash 和 Python 脚本相同的基本思想。它将每个 XML 文档存储在缓冲区(一个awk
数组)中,并使用标志来跟踪状态。当它完成处理文档时,如果文档包含与给定正则表达式匹配的任何行,则会打印该文档。这是脚本:
#!/usr/bin/env gawk
# xmlgrep.awk
# Variables:
#
# XML_DOC
# XML_DOC=1 if the current line is inside an XML document.
#
# CLOSING_TAG
# Stores the closing tag for the current XML document.
#
# BUFFER_LENGTH
# Stores the number of lines in the current XML document.
#
# MATCH
# MATCH=1 if we found a matching line in the current XML document.
#
# PATTERN
# The regular expression pattern to match against (given as a command-line argument).
#
# Initialize Variables
BEGIN{
XML_DOC=0;
CLOSING_TAG="";
BUFFER_LENGTH=0;
MATCH=0;
}
{
if (XML_DOC==1) {
# If we're inside an XML block, add the current line to the buffer
BUFFER[BUFFER_LENGTH]=$0;
BUFFER_LENGTH++;
# If we've reached a closing tag, reset the XML_DOC and CLOSING_TAG flags
if ($0 ~ CLOSING_TAG) {
XML_DOC=0;
CLOSING_TAG="";
# If there was a match then output the XML document
if (MATCH==1) {
for (i in BUFFER) {
print BUFFER[i];
}
}
}
# If we found a matching line then update the MATCH flag
else {
if ($0 ~ PATTERN) {
MATCH=1;
}
}
}
else {
# If we reach a new opening tag then start storing the data in the buffer
if ($0 ~ /<[a-z]+>/) {
# Set the XML_DOC flag
XML_DOC=1;
# Reset the buffer
delete BUFFER;
BUFFER[0]=$0;
BUFFER_LENGTH=1;
# Reset the match flag
MATCH=0;
# Compute the corresponding closing tag
match($0, /<([a-z]+)>/, match_groups);
CLOSING_TAG="</" match_groups[1] ">";
}
}
}
您可以这样称呼它:
gawk -v PATTERN="øæå" -f xmlgrep.awk data.xml
这是相应的输出:
<x>
unicode string: øæå
</x>