Jak wyodrębnić ciąg następujący po wzorcu za pomocą grep, regex lub perl

90

Mam plik, który wygląda mniej więcej tak:

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

I trzeba wyodrębnić coś wewnątrz cudzysłowów, które następują name=, to znaczy content_analyzer, content_analyzer2i content_analyzer_items.

Robię to na Linuksie, więc rozwiązanie wykorzystujące sed, perl, grep lub bash jest w porządku.

kowboj
źródło
5
nie musisz być nieśmiały, witaj tutaj!
Benoit,
8
Uważam, że niewłaściwe byłoby nie umieszczanie linku do stackoverflow.com/questions/1732348/…
Christoffer Hammarström
Dziękuję wszystkim za przydatne komentarze. Przepraszam, że XML nie został poprawnie sformatowany. Usunąłem kilka tagów dla uproszczenia.
wrangler

Odpowiedzi:

167

Ponieważ musisz dopasować zawartość bez uwzględniania jej w wyniku (musi pasować, name=" ale nie jest częścią pożądanego wyniku), wymagana jest pewna forma dopasowania o zerowej szerokości lub przechwytywania grupowego. Można to łatwo zrobić za pomocą następujących narzędzi:

Perl

W Perlu możesz użyć n opcji zapętlenia wiersz po wierszu i wydrukowania zawartości grupy przechwytywania, jeśli pasuje:

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

GNU grep

Jeśli masz ulepszoną wersję grep, taką jak GNU grep, możesz mieć -Pdostępną opcję. Ta opcja włączy regex w stylu Perla, umożliwiając użycie \Kskrótu lookbehind. Zresetuje pozycję dopasowania, więc wszystko, co jest przed nią, ma zerową szerokość.

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

Ta o opcja powoduje, że grep drukuje tylko dopasowany tekst zamiast całej linii.

Vim - edytor tekstu

Innym sposobem jest bezpośrednie użycie edytora tekstu. W Vimie jednym z różnych sposobów na osiągnięcie tego byłoby usunięcie linii bez, name=a następnie wyodrębnienie zawartości z linii wynikowych:

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

Standardowy grep

Jeśli z jakiegoś powodu nie masz dostępu do tych narzędzi, coś podobnego można osiągnąć za pomocą standardowego grep. Jednak bez rozglądania się dookoła będzie wymagać późniejszego uporządkowania:

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

Uwaga dotycząca zapisywania wyników

We wszystkich powyższych poleceniach wyniki zostaną przesłane do stdout. Należy pamiętać, że zawsze można je zapisać, przesyłając je do pliku, dołączając:

> result

do końca polecenia.

sidyll
źródło
12
Lookarounds (w GNU grep):grep -Po '.*name="\K.*?(?=".*)'
Wstrzymane do odwołania.
@Dennis Williamson, świetnie. Odpowiednio zaktualizowałem odpowiedź, ale .*odłożyłem obie na bok, mam nadzieję, że się na mnie nie zdenerwujesz. Chciałbym zapytać, czy dostrzegasz jakieś korzyści wynikające z niechciwej walki zamiast „czegokolwiek oprócz "”? Nie traktuj tego jako walki, jestem po prostu ciekawy i nie jestem ekspertem od regexów. Również \Kwskazówka, naprawdę fajna. Dzięki, Dennis.
sidyll
2
Dlaczego miałbym być zły? Bez .*, możesz to zrobić grep -Po '(?<=name=").*?(?=")'. \KMogą być wykorzystywane do stenografii, ale to naprawdę potrzebne tylko wtedy, gdy mecz się jej lewej stronie jest zmienna długość. W takich przypadkach powód używania obejść jest dość oczywisty. Niegrzeczne operacje wyglądają trochę schludniej (w [^"]*przeciwieństwie do .*?i nie musisz powtarzać znaku kotwicy. Nie wiem o szybkości. Myślę, że to zależy w dużej mierze od kontekstu. Mam nadzieję, że to pomoże.)
Wstrzymano do odwołania.
@Dennis Williamson: z pewnością proszę pana, tutaj wiele pomocnych informacji. Myślę, że powód, dla którego zachowałem \K(po zbadaniu tego) i usunąłem, .*był taki sam: nadaj mu ładny wygląd (prostszy). I nigdy nie myślałem o użyciu .*?zamiast „tradycyjnego sposobu”, którego się gdzieś nauczyłem. Ale niechciwość tutaj naprawdę ma sens. Dzięki Dennis, najlepsze życzenia.
sidyll
+1 za opisanie polecenia. Byłby wdzięczny, gdybyś mógł zaktualizować swoją odpowiedź, aby wyjaśnić część „[...]” wyrażenia regularnego.
lreeder
5

Wyrażenie regularne wyglądałoby tak:

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

Wtedy grupowanie byłoby w \ 1

Golarka matowa
źródło
5

Jeśli używasz Perla, pobierz moduł do analizy XML: XML :: Simple , XML :: Twig lub XML :: LibXML . Nie wymyślaj ponownie koła.

shawnhcorey
źródło
3
Zauważ, że podany przykład OP nie jest poprawnie sformułowany ( <type="global"na przykład), więc większość parserów XML po prostu narzeka i umiera.
bvr
5

W tym celu należy użyć parsera HTML zamiast wyrażeń regularnych. Program w Perlu, który wykorzystuje HTML::TreeBuilder:

Program

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

Wynik

content_analyzer
content_analyzer2
content_analyzer_items
Alan Haggai Alavi
źródło
2

może to zrobić:

perl -ne 'if(m/name="(.*?)"/){ print $1 . "\n"; }'
Benoit
źródło
2

Oto rozwiązanie wykorzystujące HTML tidy i 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
mitma
źródło
1

Ups, polecenie sed musi oczywiście poprzedzać polecenie uporządkowane:

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
mitma
źródło
0

Jeśli struktura twojego xml (lub ogólnie tekstu) jest ustalona, ​​najłatwiej jest użyć cut. W Twoim przypadku:

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 '"'
Carlos Lindado
źródło