Comment extraire plusieurs lignes d'un fichier par une expression régulière?

10

Comment extraire plusieurs lignes d'un fichier par une expression régulière?

Je voudrais souvent obtenir plusieurs lignes / modifier plusieurs lignes par une expression régulière. Un exemple de cas:

J'essaie de lire une partie d'un fichier XML / SGML (ils ne sont pas nécessairement bien formés ou dans une syntaxe prévisible, donc une expression régulière serait plus sûre qu'un analyseur approprié. De plus, j'aimerais pouvoir le faire aussi complètement fichiers non structurés où seuls quelques mots clés sont connus.) dans un script shell (fonctionnant sous Solaris et Linux).

Exemple XML:

<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>

De cela, je voudrais lire <tag1>si elle contient fooquelque part en elle.

Un regex comme (<tag1>.*?foo.*?</tag1>)devrait donner la bonne partie mais des outils comme grepet sedne fonctionnent que pour moi avec des lignes simples. Comment puis-je avoir

<tag1>
 <tag2>foo</tag2>
</tag1>

dans cet exemple?

Tanière
la source
3
Lien obligatoire
evilsoup
@evilsoup C'est vrai, mais ma question ne concerne pas spécifiquement les fichiers XML / SGML, à peu près tous les fichiers texte.
Den

Réponses:

7

Si GNU grep est installé, vous pouvez faire une recherche multiligne en passant le -Pdrapeau (perl-regex) et en activant PCRE_DOTALLavec(?s)

grep -oP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
<tag1>
<tag2>foo</tag2>
</tag1>

Si ce qui précède ne fonctionne pas sur votre plate-forme, essayez de passer le -zdrapeau en plus, cela force grep à traiter NUL comme séparateur de ligne, ce qui fait que le fichier entier ressemble à une seule ligne.

grep -ozP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
iruvar
la source
Cela ne donne aucune sortie sur mon système lorsqu'il est exécuté sur le fichier d'exemple de l'OP.
terdon
Travaille pour moi. +1. Merci pour l' (?s)astuce
Nathan Wallace
@terdon, quelle version de GNU grep utilisez-vous?
iruvar
@ 1_CR (GNU grep) 2.14sur Debian. J'ai copié l'exemple OPs tel grepquel (en ajoutant uniquement la nouvelle ligne finale) et je l'ai exécuté sans obtenir de résultats.
terdon
1
@slm, je suis sur pcre 6.6, GNU grep 2.5.1 sur RHEL. Cela vous dérange d'essayer grep -ozPplutôt que grep -oPsur vos plateformes?
iruvar
3
#begin command block
#append all lines between two addresses to hold space 
    sed -n -f - <<\SCRIPT file.xml
        \|<tag1>|,\|</tag1>|{ H 
#at last line of search block exchange hold and pattern space 
            \|</tag1>|{ x
#if not conditional ;  clear buffer ; branch to script end
                \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
#do work ; print result; clear buffer ; close blocks
    s?*?*?;p;s/.*//;h;b}}
SCRIPT

Si vous faites ce qui précède, compte tenu des données que vous affichez, avant cette dernière ligne de nettoyage, vous devriez travailler avec un sedespace de modèle qui ressemble à:

 ^\n<tag1>\n<tag2>foo</tag2>\n</tag1>$

Vous pouvez imprimer votre espace de motif à tout moment avec look. Vous pouvez ensuite vous adresser aux \npersonnages.

sed l <file

Vous montrera que chaque ligne la sedtraite au stade où elle lest appelée.

Je viens donc de le tester et il en fallait un de plus \backslashaprès ,commala première ligne, mais sinon il fonctionne tel quel . Ici, je le mets dans un _sed_functionafin que je puisse facilement l'appeler à des fins de démonstration tout au long de cette réponse: (fonctionne avec les commentaires inclus, mais sont supprimés ici par souci de concision)

_sed_function() { sed -n -f /dev/fd/3 
} 3<<\SCRIPT <<\FILE 
    \|<tag1>|,\|</tag1>|{ H
        \|</tag1>|{ x
            \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
    s?*?*?;p;s/.*//;h;b}}
#END
SCRIPT
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
FILE


_sed_function
#OUTPUT#
<tag1>
 <tag2>foo</tag2>
</tag1>

Maintenant, nous allons changer le ppour un lafin que nous puissions voir avec quoi nous travaillons pendant que nous développons notre script et supprimons la démo non-op s?pour que la dernière ligne de notre sed 3<<\SCRIPTressemble à ceci :

l;s/.*//;h;b}}

Ensuite, je vais l'exécuter à nouveau:

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

D'accord! J'avais donc raison - c'est un bon sentiment. Maintenant, mélangeons notre lregard pour voir les lignes qu'il tire mais supprime. Nous allons supprimer notre courant let en ajouter un pour !{block}qu'il ressemble à ceci:

!{l;s/.*//;h;b}

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$

Voilà à quoi cela ressemble juste avant de l'effacer.

Une dernière chose que je veux vous montrer est l' Hancien espace que nous construisons. Il y a quelques concepts clés que j'espère pouvoir démontrer. Je retire donc le dernier look et modifie la première ligne pour ajouter un aperçu dans l' Hancien espace à la fin:

{ H ; x ; l ; x

_sed_function
#OUTPUT#
\n<tag1>$
\n<tag1>\n <tag2>bar</tag2>$
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$
\n<tag1>$
\n<tag1>\n <tag2>foo</tag2>$
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

Hle vieil espace survit aux cycles des lignes - d'où son nom. Donc, ce que les gens font souvent trébucher - ok, ce que je fais souvent trébucher - c'est qu'il faut le supprimer après l'avoir utilisé. Dans ce cas, je ne xchange qu'une seule fois, donc l'espace de maintien devient l'espace de motif et vice-versa et ce changement survit également aux cycles de ligne.

L'effet est que je dois supprimer mon espace d'attente qui était mon espace de motif. Pour ce faire, je vide d'abord l'espace de motif actuel avec:

s/.*//

Qui sélectionne simplement chaque personnage et le supprime. Je ne peux pas l'utiliser dcar cela mettrait fin à mon cycle de ligne en cours et la prochaine commande ne se terminerait pas, ce qui mettrait à peu près à la poubelle mon script.

h

Cela fonctionne de manière similaire à Hmais il écrase l' espace de rétention , donc je viens de copier mon espace de motif vierge au-dessus de mon espace de rétention, le supprimant efficacement. Maintenant je peux juste:

b

en dehors.

Et c'est comme ça que j'écris des sedscripts.

mikeserv
la source
Merci @slm! Tu es vraiment un mec bien, tu le sais?
mikeserv
Merci, beau travail, montée très rapide à 3k, prochain 5k 8-)
slm
Je sais pas, @slm. Je commence à voir que j'apprends de moins en moins ici - peut-être que j'ai dépassé son utilité. Je dois y penser. ive viennent à peine sur le site ces dernières semaines.
mikeserv
Atteignez au moins 10k. Tout ce qui mérite d'être déverrouillé se trouve à ce niveau. Continuez à ébrécher, 5k viendra assez rapidement maintenant.
slm
1
Eh bien, @slm - vous êtes de toute façon une race rare. Je suis cependant d'accord sur les réponses multiples. C'est pourquoi ça me dérange quand certains qs se ferment. Mais cela arrive rarement, en fait. Merci encore, slm.
mikeserv
2

La réponse de @ jamespfinn fonctionnera parfaitement bien si votre fichier est aussi simple que votre exemple. Si vous avez une situation plus complexe qui <tag1>peut s'étendre sur plus de 2 lignes, vous aurez besoin d'une astuce légèrement plus complexe. Par exemple:

$ cat foo.xml
<tag1>
 <tag2>bar</tag2>
 <tag3>baz</tag3>
</tag1>
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>
$ perl -ne 'if(/<tag1>/){$a=1;} 
            if($a==1){push @l,$_}
            if(/<\/tag1>/){
              if(grep {/foo/} @l){print "@l";}
               $a=0; @l=()
            }' foo.xml
<tag1>

  <tag2>foo</tag2>
 </tag1>
<tag1>
  <tag2>bar</tag2>

  <tag2>foo</tag2>
  <tag3>baz</tag3>
 </tag1>

Le script perl traitera chaque ligne de votre fichier d'entrée et

  • if(/<tag1>/){$a=1;}: la variable $aest définie sur 1si une balise d'ouverture ( <tag1>) est trouvée.

  • if($a==1){push @l,$_}: pour chaque ligne, si $ac'est le cas 1, ajoutez cette ligne au tableau @l.

  • if(/<\/tag1>/) : si la ligne actuelle correspond à la balise de fermeture:

    • if(grep {/foo/} @l){print "@l"}: si l'une des lignes enregistrées dans le tableau @l(ce sont les lignes entre <tag1>et </tag1>) correspond à la chaîne foo, imprimez le contenu de @l.
    • $a=0; @l=(): vide la liste ( @l=()) et $aremet à 0.
terdon
la source
Cela fonctionne bien sauf dans le cas où plusieurs <tag1> contiennent "foo". Dans ce cas, il imprime tout depuis le début du premier <tag1> jusqu'à la fin du dernier </tag1> ...
Den
@den Je l'ai testé avec l'exemple montré dans ma réponse qui contient 3 <tag1>avec fooet cela fonctionne très bien. Quand cela échoue-t-il pour vous?
terdon
ça fait tellement mal d'analyser xml en utilisant regex :)
Braiam
1

Voici une sedalternative:

sed -n '/<tag1/{:x N;/<\/tag1/!b x};/foo/p' your_file

Explication

  • -n signifie ne pas imprimer de lignes sauf indication contraire.
  • /<tag1/ correspond d'abord à la balise d'ouverture
  • :x est une étiquette pour permettre de sauter à ce point plus tard
  • N ajoute la ligne suivante à l'espace de motif (tampon actif).
  • /<\/tag1/!b xsignifie que si l'espace de motif actuel ne contient aucune balise de fermeture, branchez-vous sur l' xétiquette créée précédemment. Nous continuons donc à ajouter des lignes à l'espace de motif jusqu'à ce que nous trouvions notre balise de fermeture.
  • /foo/psignifie que si l'espace de motif actuel correspond foo, il doit être imprimé.
Joseph R.
la source
1

Vous pouvez le faire avec GNU awk je pense, en traitant la balise de fin comme un séparateur d'enregistrement, par exemple pour une balise de fin connue </tag1>:

gawk -vRS="\n</tag1>\n" '/foo/ {printf "%s%s", $0, RT}'

ou plus généralement (avec une expression régulière pour la balise de fin)

gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}'

Le tester sur @ terdon foo.xml:

$ gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}' foo.xml
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>
tournevis
la source
0

Si votre fichier est structuré exactement comme vous l'avez montré ci-dessus, vous pouvez utiliser les indicateurs -A (lignes après) et -B (lignes avant) pour grep ... par exemple:

$ cat yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
$ grep -A1 -B1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
$ grep -A1 -B1 foo yourFile.txt 
<tag1>
 <tag2>foo</tag2>
</tag1>

Si votre version de le grepprend en charge, vous pouvez également utiliser l' -Coption plus simple (pour le contexte) qui imprime les N lignes environnantes:

$ grep -C 1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
jamespfinn
la source
Merci, mais non. Ce n'est qu'un exemple et les vrais trucs semblent assez imprévisibles ;-)
Den
1
Ce n'est pas de trouver un tag contenant foo, c'est juste de trouver foo et d'afficher des lignes de contexte
Nathan Wallace
@NathanWallace oui, c'est exactement ce que le PO demandait, cette réponse fonctionne parfaitement dans le cas donné dans la question.
terdon
@terdon ce n'est pas du tout ce que la question pose. Citation: "Je voudrais lire le <tag1> s'il contient foo quelque part." Cette solution est comme "Je voudrais lire 'foo' et 1 ligne de contexte indépendamment de l'endroit où 'foo' apparaît". Suivant votre logique, une réponse tout aussi valable à cette question serait tail -3 input_file.xml. Oui, cela fonctionne pour cet exemple spécifique, mais ce n'est pas une réponse utile à la question.
Nathan Wallace
@NathanWallace, mon point de vue était que l'OP a spécifiquement déclaré que ce n'est pas un format XML valide, dans ce cas, cela aurait bien pu être suffisant pour imprimer les N lignes autour de la chaîne que l'OP recherche. Avec les informations disponibles, cette réponse était suffisamment décente.
terdon