Comment extraire une chaîne en suivant un modèle avec grep, regex ou perl


90

J'ai un fichier qui ressemble à ceci:

    <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>

J'ai besoin d'extraire tout ce qui se trouve dans les guillemets qui suivent name=, c'est-à-dire content_analyzer, content_analyzer2et content_analyzer_items.

Je fais cela sur une machine Linux, donc une solution utilisant sed, perl, grep ou bash est bien.


5
pas besoin d'être timide, bienvenue ici!
Benoit

8
Je pense que ce serait une erreur de ne pas créer un
Christoffer Hammarström

Merci à tous pour les commentaires utiles. Je m'excuse pour le XML pas correctement formaté. J'ai supprimé quelques balises pour simplifier.
wrangler

Réponses:


167

Puisque vous devez faire correspondre le contenu sans l'inclure dans le résultat (doit correspondre name=" mais ne fait pas partie du résultat souhaité), une forme de correspondance de largeur nulle ou de capture de groupe est requise. Cela peut être fait facilement avec les outils suivants:

Perl

Avec Perl, vous pouvez utiliser l' n option pour boucler ligne par ligne et imprimer le contenu d'un groupe de capture s'il correspond:

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

Grep GNU

Si vous avez une version améliorée de grep, telle que GNU grep, vous pouvez avoir l' -Poption disponible. Cette option activera les expressions régulières de type Perl, vous permettant d'utiliser \Kce qui est un raccourci vers l'arrière. Il réinitialisera la position de correspondance, donc tout ce qui est avant sa largeur zéro.

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

L' o option fait que grep n'imprime que le texte correspondant, au lieu de la ligne entière.

Vim - Éditeur de texte

Une autre façon consiste à utiliser directement un éditeur de texte. Avec Vim, l'une des différentes manières d'y parvenir serait de supprimer les lignes sans name=puis d'extraire le contenu des lignes résultantes:

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

Grep standard

Si vous n'avez pas accès à ces outils, pour une raison quelconque, quelque chose de similaire pourrait être réalisé avec grep standard. Cependant, sans le regard autour, il nécessitera un nettoyage plus tard:

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

Une note sur l'enregistrement des résultats

Dans toutes les commandes ci-dessus, les résultats seront envoyés à stdout. Il est important de se rappeler que vous pouvez toujours les enregistrer en le redirigeant vers un fichier en ajoutant:

> result

à la fin de la commande.


12
Lookarounds (dans GNU grep):grep -Po '.*name="\K.*?(?=".*)'
Suspendu jusqu'à nouvel ordre.

@Dennis Williamson, super. J'ai mis à jour la réponse en conséquence, mais .*j'ai laissé les deux de côté, j'espère que vous ne vous fâchez pas contre moi. Je voudrais demander, voyez-vous des avantages d'un match non gourmand par rapport à «autre chose "»? Ne prenez pas cela comme un combat, je suis juste curieux et je ne suis pas un expert en regex. Aussi, le \Kconseil, vraiment sympa. Merci Dennis.
sidyll

2
Pourquoi serais-je en colère? Sans le .*, vous pouvez le faire grep -Po '(?<=name=").*?(?=")'. Le \Kpeut être utilisé pour un raccourci, mais il n'est vraiment nécessaire que si la correspondance à sa gauche est de longueur variable. Dans des cas comme celui-ci, la raison d'utiliser des lookarounds est assez évidente. Les opérations non gênées semblent un peu plus soignées ( [^"]*par rapport à .*?et vous n'avez pas à répéter le caractère d'ancrage. Je ne sais pas à propos de la vitesse. Cela dépend beaucoup du contexte, je pense. J'espère que c'est utile.
pause jusqu'à nouvel ordre.

@Dennis Williamson: certainement monsieur, beaucoup d'informations utiles ici. Je pense que la raison pour laquelle j'ai gardé le \K(après avoir fait des recherches à ce sujet) et supprimé le .*était la même: lui donner un aspect joli (plus simple). Et je n'ai jamais pensé à utiliser .*?au lieu de la "manière traditionnelle" que j'ai appris quelque part. Mais non gourmand ici a vraiment du sens. Merci Dennis, meilleurs voeux.
sidyll

+1 pour décrire la commande. J'apprécierais si vous pouviez mettre à jour votre réponse pour expliquer la partie «[...]» de l'expression régulière.
lreeder

5

L'expression régulière serait:

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

Ensuite, le regroupement serait dans le \ 1


5

Si vous utilisez Perl, téléchargez un module pour analyser le XML: XML :: Simple , XML :: Twig ou XML :: LibXML . Ne réinventez pas la roue.


3
Notez que l'exemple donné par OP n'est pas bien formé ( <type="global"par exemple), donc la plupart des analyseurs XML se plaignent et meurent.
bvr

5

Un analyseur HTML doit être utilisé à cette fin plutôt que des expressions régulières. Un programme Perl qui utilise HTML::TreeBuilder:

Programme

#!/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>

Production

content_analyzer
content_analyzer2
content_analyzer_items

2

cela pourrait le faire:

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

2

Voici une solution utilisant HTML tidy et 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

Oups, la commande sed doit bien sûr précéder la commande tidy:

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

Si la structure de votre xml (ou du texte en général) est fixe, le moyen le plus simple est d'utiliser cut. Pour votre cas particulier:

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 '"'
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.