filter xml documents correspondência certains ids

5

Suponha que você tenha um arquivo contendo muitos documentos xml, como

<a>
  <b>
  ...
</a>
in between xml documents there may be plain text log messages
<x>
  ...
</x>

...

Como eu filtraria esse arquivo para mostrar apenas os documentos xml em que um determinado regexp corresponde a qualquer uma das linhas desse documento xml? Eu estou falando sobre uma simples correspondência textual aqui, então a parte correspondente da expressão regular também pode ser totalmente ignorante do formato subjacente - xml.

Você pode assumir que as tags de abertura e fechamento do elemento raiz estão sempre em linhas próprias (embora possam ser preenchidas com espaços em branco) e que elas sejam usadas apenas como elementos raiz, ou seja, tags com o mesmo nome não aparece abaixo do elemento raiz. Isso deve possibilitar a conclusão do trabalho sem precisar recorrer a ferramentas que reconhecem o xml.

    
por Eugene Beresovsky 18.12.2017 / 01:07

1 resposta

5

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>
    
por 18.12.2017 / 01:53