Resumo
Eu escrevi uma solução Python, uma solução Bash e uma solução Awk. A ideia para todos os scripts é a mesma: passar linha por linha e usar variáveis de sinalização para acompanhar o estado (ou seja, se estamos ou não dentro de um subdocumento XML e se encontramos ou não uma linha correspondente ).
No script Python, eu leio todas as linhas em uma lista e monitora o índice da lista onde o subdocumento XML atual começa, para que eu possa imprimir o subdocumento atual quando chegarmos à tag de fechamento. Eu verifico cada linha para o padrão regex e uso um sinalizador para manter ou não a saída do subdocumento atual quando terminar de processá-lo.
No script Bash eu uso um arquivo temporário como um buffer para armazenar o subdocumento XML atual e aguardo até que esteja pronto para ser escrito antes de usar grep
para verificar se ele contém uma linha que corresponde à regex dada.
O script Awk é semelhante ao script Base, mas eu uso o array Awk para o buffer em vez de um arquivo.
Arquivo de dados de teste
Eu verifiquei os dois scripts em relação ao arquivo de dados a seguir ( data.xml
) com base nos dados de exemplo fornecidos na sua pergunta:
<a>
<b>
string to search for: stuff
</b>
</a>
in between xml documents there may be plain text log messages
<x>
unicode string: øæå
</x>
Solução Python
Aqui está um script Python simples que faz o que você quer:
#!/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] + '>'
Veja como você executa o script para pesquisar a string "stuff":
python xmlgrep.py stuff data.xml
E aqui está a saída:
<a>
<b>
string to search for: stuff
</b>
</a>
E aqui está como você executa o script para pesquisar a string "øæå":
python xmlgrep.py øæå data.xml
E aqui está a saída:
<x>
unicode string: øæå
</x>
Você também pode especificar -v
ou --invert-match
para pesquisar documentos não correspondentes e trabalhar em stdin:
cat data.xml | python xmlgrep.py -v stuff
Solução de bash
Aqui está a implementação bash do mesmo algoritmo básico. Ele usa sinalizadores para rastrear se a linha atual pertence a um documento XML e usa um arquivo temporário como um buffer para armazenar cada documento XML conforme está sendo processado.
#!/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*$//;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}"
Veja como você pode executar o script para pesquisar a string 'stuff':
bash xmlgrep.sh data.xml 'stuff'
E aqui está a saída correspondente:
<a>
<b>
string to search for: stuff
</b>
</a>
Veja como você pode executar o script para pesquisar a string 'øæå':
bash xmlgrep.sh data.xml 'øæå'
E aqui está a saída correspondente:
<x>
unicode string: øæå
</x>
Solução Awk
Aqui está uma solução awk
- meu awk
não é ótimo, por isso é bem difícil. Ele usa a mesma ideia básica dos scripts Bash e Python. Ele armazena cada documento XML em um buffer (um array awk
) e usa sinalizadores para controlar o estado. Quando terminar de processar um documento, ele será impresso se contiver linhas que correspondam à expressão regular dada. Aqui está o script:
#!/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] ">";
}
}
}
Aqui está como você chamaria:
gawk -v PATTERN="øæå" -f xmlgrep.awk data.xml
E aqui está a saída correspondente:
<x>
unicode string: øæå
</x>