Como extrair string seguindo um padrão com grep, regex ou perl


91

Eu tenho um arquivo parecido com este:

    <table name="content_analyzer" primary-key="id">
      <type="global" />
    </table>
    <table name="content_analyzer2" primary-key="id">
      <type="global" />
    </table>
    <table name="content_analyzer_items" primary-key="id">
      <type="global" />
    </table>

Preciso extrair qualquer coisa entre as aspas a seguir name=, ou seja content_analyzer, content_analyzer2e content_analyzer_items.

Estou fazendo isso em uma máquina Linux, então uma solução usando sed, perl, grep ou bash é adequada.


5
não precisa ser tímido, seja bem-vindo aqui!
Benoit

8
Acho que seria errado não vincular a stackoverflow.com/questions/1732348/…
Christoffer Hammarström

Obrigado a todos pelos comentários úteis. Peço desculpas pelo XML não estar formatado corretamente. Excluí algumas tags para simplificação.
Wrangler

Respostas:


172

Visto que você precisa combinar o conteúdo sem incluí-lo no resultado (deve corresponder, name=" mas não é parte do resultado desejado), alguma forma de correspondência de largura zero ou captura de grupo é necessária. Isso pode ser feito facilmente com as seguintes ferramentas:

Perl

Com o Perl, você pode usar a n opção de fazer um loop linha por linha e imprimir o conteúdo de um grupo de captura se corresponder a:

perl -ne 'print "$1\n" if /name="(.*?)"/' filename

GNU grep

Se você tem uma versão melhorada do grep, como GNU grep, você pode ter a -Popção disponível. Esta opção habilitará o regex semelhante ao Perl, permitindo que você use o \Kque é um lookbehind abreviado. Ele irá redefinir a posição de correspondência, então qualquer coisa antes de ter largura zero.

grep -Po 'name="\K.*?(?=")' filename

A o opção faz com que o grep imprima apenas o texto correspondente, ao invés de toda a linha.

Vim - Editor de Texto

Outra maneira é usar um editor de texto diretamente. Com o Vim, uma das várias maneiras de fazer isso seria excluir as linhas sem name=e, em seguida, extrair o conteúdo das linhas resultantes:

:v/.*name="\v([^"]+).*/d|%s//\1

Grep padrão

Se você não tiver acesso a essas ferramentas, por algum motivo, algo semelhante pode ser alcançado com o grep padrão. No entanto, sem olhar ao redor, será necessária alguma limpeza mais tarde:

grep -o 'name="[^"]*"' filename

Uma nota sobre como salvar os resultados

Em todos os comandos acima os resultados serão enviados para stdout. É importante lembrar que você sempre pode salvá-los direcionando-os para um arquivo anexando:

> result

ao final do comando.


12
Lookarounds (no GNU grep):grep -Po '.*name="\K.*?(?=".*)'
Dennis Williamson

@Dennis Williamson, ótimo. Atualizei a resposta de acordo, mas deixei ambas de .*lado, espero que você não fique com raiva de mim. Eu gostaria de perguntar, você vê algum benefício na combinação não gananciosa em relação a "qualquer coisa exceto ""? Não leve isso como uma luta, estou apenas curioso e não sou um especialista em regex. Além disso, a \Kdica, muito legal. Obrigado Dennis.
Sidyll

2
Por que eu ficaria com raiva? Sem o .*, você pode fazer grep -Po '(?<=name=").*?(?=")'. O \Kpode ser usado para abreviar, mas é realmente necessário apenas se a correspondência à sua esquerda tiver comprimento variável. Em casos como esse, a razão para usar lookarounds é bastante óbvia. Operações desagradáveis ​​parecem um pouco mais organizadas ( [^"]*versus .*?e você não precisa repetir o caractere âncora. Não sei sobre velocidade. Isso depende muito do contexto, eu acho. Espero que seja útil.
Dennis Williamson

@Dennis Williamson: certamente senhor, muitas informações úteis aqui. Acho que o motivo pelo qual mantive o \K(depois de pesquisar sobre ele) e removi o .*foi o mesmo: torná-lo bonito (mais simples). E nunca pensei em usar em .*?vez do "jeito tradicional" que aprendi em algum lugar. Mas não ganancioso aqui realmente faz sentido. Obrigado Dennis, melhores votos.
sidyll

1 para descrever o comando. Agradeceria se você pudesse atualizar sua resposta para explicar a parte "[...]" do regex.
lreeder

5

A expressão regular seria:

.+name="([^"]+)"

Então o agrupamento seria no \ 1


5

Se você estiver usando Perl, baixe um módulo para analisar o XML: XML :: Simple , XML :: Twig ou XML :: LibXML . Não reinvente a roda.


3
Observe que o exemplo fornecido por OP não está bem formado ( <type="global"por exemplo), então a maioria dos analisadores XML simplesmente reclama e morre.
bvr

5

Um analisador HTML deve ser usado para esse propósito, em vez de expressões regulares. Um programa Perl que faz uso de HTML::TreeBuilder:

Programa

#!/usr/bin/env perl

use strict;
use warnings;

use HTML::TreeBuilder;

my $tree = HTML::TreeBuilder->new_from_file( \*DATA );
my @elements = $tree->look_down(
    sub { defined $_[0]->attr('name') }
);

for (@elements) {
    print $_->attr('name'), "\n";
}

__DATA__
<table name="content_analyzer" primary-key="id">
  <type="global" />
</table>
<table name="content_analyzer2" primary-key="id">
  <type="global" />
</table>
<table name="content_analyzer_items" primary-key="id">
  <type="global" />
</table>

Resultado

content_analyzer
content_analyzer2
content_analyzer_items

2

isso poderia fazer isso:

perl -ne 'if(m/name="(.*?)"/){ print $1 . "\n"; }'

2

Aqui está uma solução usando HTML tidy & xmlstarlet:

htmlstr='
<table name="content_analyzer" primary-key="id">
<type="global" />
</table>
<table name="content_analyzer2" primary-key="id">
<type="global" />
</table>
<table name="content_analyzer_items" primary-key="id">
<type="global" />
</table>
'

echo "$htmlstr" | tidy -q -c -wrap 0 -numeric -asxml -utf8 --merge-divs yes --merge-spans yes 2>/dev/null |
sed '/type="global"/d' |
xmlstarlet sel -N x="http://www.w3.org/1999/xhtml" -T -t -m "//x:table" -v '@name' -n

1

Ops, o comando sed deve preceder o comando tidy, é claro:

echo "$htmlstr" | 
sed '/type="global"/d' |
tidy -q -c -wrap 0 -numeric -asxml -utf8 --merge-divs yes --merge-spans yes 2>/dev/null |
xmlstarlet sel -N x="http://www.w3.org/1999/xhtml" -T -t -m "//x:table" -v '@name' -n

0

Se a estrutura do seu xml (ou texto em geral) for fixa, a maneira mais fácil é usando cut. Para o seu caso específico:

echo '<table name="content_analyzer" primary-key="id">
  <type="global" />
</table>
<table name="content_analyzer2" primary-key="id">
  <type="global" />
</table>
<table name="content_analyzer_items" primary-key="id">
  <type="global" />
</table>' | grep name= | cut -f2 -d '"'
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.