Не жадное (неохотное) сопоставление регулярных выражений в sed?

407

Я пытаюсь использовать sed для очистки строк URL-адресов, чтобы извлечь только домен.

Так из:

http://www.suepearson.co.uk/product/174/71/3816/

Я хочу:

http://www.suepearson.co.uk/

(с косой чертой или без нее, это не имеет значения)

Я пытался:

 sed 's|\(http:\/\/.*?\/\).*|\1|'

и (избегая не жадного квантификатора)

sed 's|\(http:\/\/.*\?\/\).*|\1|'

но я не могу заставить ?работать не жадный квантификатор ( ), поэтому он всегда совпадает со всей строкой.

Joel
источник
54
Примечание: если вы ограничиваете свои регулярные выражения с помощью «|», вам не нужно избегать «/». На самом деле, большинство людей разделяют знаком «|» вместо "/", чтобы избежать "заборов".
AttishOculus
12
@AttishOculus Первый символ после 's' в выражении замены в sed - это разделитель. Следовательно 's ^ foo ^ bar ^' или 's! Foo! Bar!' также работает
Squidly
1
Для расширенного регулярного выражения используйте sed -E 's.... Тем не менее, неохотный оператор.
Ондра Жижка
Не ответьте на название вопроса, но в данном конкретном случае все cut -d'/' -f1-3работает просто .
Петр Яворик

Ответы:

422

Ни базовое, ни расширенное регулярное выражение Posix / GNU не распознает не жадный квантификатор; вам нужно позднее регулярное выражение К счастью, Perl регулярное выражение для этого контекста довольно легко получить:

perl -pe 's|(http://.*?/).*|\1|'
хаос
источник
13
Для этого на месте используйте варианты -pi -e.
действительно хороший день
12
Черт возьми, я не могу поверить, что это сработало :-) Единственное, что отстой - теперь мой скрипт имеет зависимость от Perl :-( С другой стороны, практически каждый дистрибутив Linux имеет Perl, так что, вероятно, это не проблема :-)
Freedom_Ben
7
@Freedom_Ben: IIRC perlэто требуется по стандарту POSIX
MestreLion
4
@ dolphus333: «Ни базовое, ни расширенное регулярное выражение Posix / GNU не распознает не жадный квантификатор», значит «вы не можете использовать не жадный квантификатор в sed».
хаос
3
@ Sérgio это то, как вы делаете запрошенную вещь, в которой невозможно sed, используя синтаксис, в основном идентичный синтаксисуsed
хаос
251

В этом конкретном случае вы можете выполнить работу без использования не жадных регулярных выражений.

Попробуйте это не жадное регулярное выражение [^/]*вместо .*?:

sed 's|\(http://[^/]*/\).*|\1|g'
гумбо
источник
3
Как сделать, чтобы sed соответствовал не жадной фразе, используя эту технику?
user3694243
6
К сожалению, вы не можете; см ответ Хаоса .
Даниэль Х
Большое спасибо ... поскольку Perl больше не является базой установки по умолчанию во многих дистрибутивах Linux!
st0ne
@DanielH На самом деле можно сопоставлять фразы, не жадно, используя эту технику по запросу. Это может занять некоторую боль, чтобы написать любой шаблон с достаточной точностью. Например, при разборе назначения значения ключа в запросе URL может потребоваться поиск назначения с использованием ([^&=#]+)=([^&#]*). Существуют случаи, которые точно не работают таким образом, например, при разборе URL-адреса для его части хоста и имени пути с окончательной косой чертой, которую необязательно исключать из захвата:^(http:\/\/.+?)/?$
Thomas Urban
121

С помощью sed я обычно реализую не жадный поиск, ища что-либо, кроме разделителя до разделителя:

echo "http://www.suon.co.uk/product/1/7/3/" | sed -n 's;\(http://[^/]*\)/.*;\1;p'

Вывод:

http://www.suon.co.uk

это:

  • не выводить -n
  • поиск, сопоставление шаблона, замена и печать s/<pattern>/<replace>/p
  • используйте ;разделитель команды поиска вместо того, /чтобы было проще набиратьs;<pattern>;<replace>;p
  • помните матч между кронштейнами \(... \), потом доступны с \1, \2...
  • матч http://
  • после чего - либо в скобках [], [ab/]будет означать либо aили bили/
  • сначала ^в []средствах not, поэтому следуют все, кроме вещи в[]
  • так [^/]значит что угодно, кроме /персонажа
  • *повторять предыдущую группу, [^/]*значит, символы кроме /.
  • до сих пор sed -n 's;\(http://[^/]*\)означает поиск и запоминание, http://за которым следуют любые символы, кроме как, /и помните, что вы нашли
  • мы хотим выполнить поиск до конца домена, поэтому остановитесь на следующем, /добавьте еще один /в конце: sed -n 's;\(http://[^/]*\)/'но мы хотим сопоставить остаток строки после домена, поэтому добавьте.*
  • Теперь совпадение, запомненное в группе 1 ( \1), является доменом, поэтому замените сопоставленную строку материалом, сохраненным в группе, \1и напечатайте:sed -n 's;\(http://[^/]*\)/.*;\1;p'

Если вы хотите включить обратную косую черту и после домена, добавьте еще одну обратную косую черту в группу, чтобы запомнить:

echo "http://www.suon.co.uk/product/1/7/3/" | sed -n 's;\(http://[^/]*/\).*;\1;p'

вывод:

http://www.suon.co.uk/
stefanB
источник
8
Относительно недавних правок: круглые скобки являются своего рода символом в скобках, поэтому их нельзя называть скобками, особенно если вы следите за словом с реальными символами, как это сделал автор. Кроме того, это предпочтительное использование в некоторых культурах, поэтому замена его на предпочтительное использование в вашей собственной культуре кажется немного грубым, хотя я уверен, что это не то, что задумал редактор. Лично я думаю, что лучше использовать чисто описательные имена, такие как круглые , квадратные и угловые скобки .
Алан Мур
2
Можно ли заменить разделитель на строку?
Calculemus
37

sed не поддерживает «не жадный» оператор.

Вы должны использовать оператор «[]», чтобы исключить «/» из совпадения.

sed 's,\(http://[^/]*\)/.*,\1,'

PS нет необходимости использовать обратную косую черту "/".

andcoz
источник
на самом деле, нет. если разделитель может быть одним из многих возможных символов (скажем, только в виде строки чисел), ваше совпадение отрицания может становиться все более и более сложным. это хорошо, но было бы неплохо иметь возможность сделать. * non greedy
gesell
1
Вопрос был более общим. Эти решения работают для URL-адресов, но не (например) для моего случая использования конечных нулей. s/([[:digit:]]\.[[1-9]]*)0*/\1/очевидно, не будет работать хорошо 1.20300. Поскольку первоначальный вопрос касался URL-адресов, их следует указать в принятом ответе.
Даниэль Х
33

Имитация ленивого (не жадного) квантификатора в sed

И все другие регулярные выражения!

  1. Нахождение первого вхождения выражения:

    • POSIX ERE (используя -rопцию)

      Regex:

      (EXPRESSION).*|.

      Sed:

      sed -r 's/(EXPRESSION).*|./\1/g' # Global `g` modifier should be on

      Пример (поиск первой последовательности цифр) Демо :

      $ sed -r 's/([0-9]+).*|./\1/g' <<< 'foo 12 bar 34'
      12

      Как это работает ?

      Это регулярное выражение извлекает выгоду из чередования |. В каждой позиции движок пытается выбрать самое длинное совпадение (это стандарт POSIX, за которым следуют также несколько других движков), что означает, что оно продолжается .до тех пор, пока не будет найдено совпадение ([0-9]+).*. Но порядок тоже важен.

      введите описание изображения здесь

      Поскольку установлен глобальный флаг, движок пытается продолжить сопоставление символ за символом до конца входной строки или нашей цели. Как только первая и единственная группа захвата левой стороны чередования совпадает, (EXPRESSION)остаток линии также потребляется немедленно .*. Теперь мы держим нашу ценность в первой группе захвата.

    • POSIX BRE

      Regex:

      \(\(\(EXPRESSION\).*\)*.\)*

      Sed:

      sed 's/\(\(\(EXPRESSION\).*\)*.\)*/\3/'

      Пример (поиск первой последовательности цифр):

      $ sed 's/\(\(\([0-9]\{1,\}\).*\)*.\)*/\3/' <<< 'foo 12 bar 34'
      12

      Это похоже на версию ERE, но без чередования. Это все. В каждой позиции двигатель пытается сопоставить цифру.

      введите описание изображения здесь

      Если он найден, другие следующие цифры потребляются и захватываются, и остальная часть строки сопоставляется немедленно, в противном случае, так как *означает больше или ноль, он пропускает вторую группу захвата \(\([0-9]\{1,\}\).*\)*и достигает точки, .чтобы соответствовать одному символу, и этот процесс продолжается.

  2. Нахождение первого вхождения с разделителями выражения :

    Этот подход будет соответствовать самому первому вхождению строки с разделителями. Мы можем назвать это блоком строк.

    sed 's/\(END-DELIMITER-EXPRESSION\).*/\1/; \
         s/\(\(START-DELIMITER-EXPRESSION.*\)*.\)*/\1/g'

    Строка ввода:

    foobar start block #1 end barfoo start block #2 end

    -EDE: end

    -SDE: start

    $ sed 's/\(end\).*/\1/; s/\(\(start.*\)*.\)*/\1/g'

    Вывод:

    start block #1 end

    Первое регулярное выражение \(end\).*сопоставляет и захватывает первый конечный разделитель endи заменяет все совпадения на последние захваченные символы, которые являются конечным разделителем. На этом этапе наш вывод: foobar start block #1 end.

    введите описание изображения здесь

    Затем результат передается второму регулярному выражению \(\(start.*\)*.\)*, которое совпадает с вышеприведенной версией POSIX BRE. Он соответствует одному символу, если начальный разделитель startне совпадает, в противном случае он совпадает и захватывает начальный разделитель и соответствует остальным символам.

    введите описание изображения здесь


Непосредственно отвечая на ваш вопрос

Используя подход № 2 (выражение с разделителями), вы должны выбрать два подходящих выражения:

  • EDE: [^:/]\/

  • SDE: http:

Применение:

$ sed 's/\([^:/]\/\).*/\1/g; s/\(\(http:.*\)*.\)*/\1/' <<< 'http://www.suepearson.co.uk/product/174/71/3816/'

Вывод:

http://www.suepearson.co.uk/

Примечание: это не будет работать с одинаковыми разделителями.

Revo
источник
3) предлагая такие сайты, как regex101 для демонстрации, добавьте примечание, что оно не всегда подходит для инструментов cli из-за синтаксиса и различий в
функциях
1
@ Sundeep Спасибо. Я превратил все эти цитаты в одинарные. Также я посчитал упомянутое самое длинное правило совпадения. Однако sedи во всех других двигателях, следующих тому же стандартному порядку, имеет значение, когда дело доходит до равенства. Так echo 'foo 1' | sed -r 's/.|([0-9]+).*/\1/g'что нет совпадения, но echo 'foo 1' | sed -r 's/([0-9]+).*|./\1/g'есть.
Revo
@Sundeep также обходной путь для выражений с разделителями не работал для идентичных начальных и конечных разделителей, для которых я добавил примечание.
Revo
Замечательно, что происходит, когда различные чередования начинаются с одного и того же места и имеют одинаковую длину, предположим, что они будут следовать в порядке слева направо, как и другие двигатели ... нужно посмотреть, если это описано в руководстве
Sundeep
Здесь есть странный случай: stackoverflow.com/questions/59683820/…
Sundeep
20

Нежадное решение для более чем одного персонажа

Эта ветка действительно старая, но я предполагаю, что людям она все еще нужна. Допустим, вы хотите убить все до самого первого появления HELLO. Вы не можете сказать [^HELLO]...

Таким образом, хорошее решение состоит из двух этапов, при условии, что вы можете сэкономить уникальное слово, которое, например, вы не ожидаете при вводе top_sekrit.

В этом случае мы можем:

s/HELLO/top_sekrit/     #will only replace the very first occurrence
s/.*top_sekrit//        #kill everything till end of the first HELLO

Конечно, при более простом вводе вы можете использовать меньшее слово или, возможно, даже один символ.

НТН!

ishahak
источник
4
Чтобы сделать его еще лучше, полезно в ситуации, когда вы не можете ожидать, что неиспользуемый символ: 1. замените этот специальный символ на действительно неиспользуемое СЛОВО, 2. замените конечную последовательность специальным символом, 3. выполните поиск, заканчивающийся специальным символом, 4 Заменить специальный символ назад, 5. Заменить специальный WORD обратно. Например, вам нужен жадный оператор между <hello> и </ hello>:
Jakub
3
Вот пример: echo "Найти: <hello> fir ~ st <br> yes </ hello> <hello> sec ~ ond </ hello>" | sed -e "s, ~, VERYSPECIAL, g" -e "s, </ hello>, ~, g" -e "s,. * Найти: <hello> ([^ ~] *). *, \ 1 , "-e", \ ~, </ hello>, "-e", VERYSPECIAL, ~, "
Якуб
2
Согласен. Хорошее решение. Я бы перефразировал комментарий следующим образом: если вы не можете полагаться на то, что ~ не используется, сначала замените его текущие вхождения, используя s / ~ / VERYspeciaL / g, затем выполните вышеуказанный трюк, а затем верните оригинал ~, используя s / VERYspeciaL / ~ / g.
Ишахак
1
Я предпочитаю использовать более редкие «переменные» для такого рода вещей, поэтому вместо этого `я бы использовал <$$>(поскольку $$расширяется до идентификатора вашего процесса в оболочке, хотя вам придется использовать двойные кавычки, а не одинарные кавычки, и это может сломать другие части вашего регулярного выражения) или, если доступен Юникод, что-то вроде <∈∋>.
Адам Кац
В какой - то момент вы должны спросить себя , почему вы не просто использовать perlили pythonвместо или какой - либо другой язык. perlделает это менее хрупким способом в одной строке ...
ArtOfWarfare
18

sed - не жадное совпадение от Christoph Sieghart

Уловка, чтобы получить не жадное соответствие в sed, состоит в том, чтобы сопоставить все символы, кроме того, который заканчивает соответствие. Я знаю, нетрудно, но я потратил драгоценные минуты на это, и сценарии оболочки должны быть, в конце концов, быстрыми и легкими. Так что в случае, если это может понадобиться кому-то другому:

Жадный подход

% echo "<b>foo</b>bar" | sed 's/<.*>//g'
bar

Не жадное совпадение

% echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar
gresolio
источник
17

Это можно сделать с помощью cut:

echo "http://www.suepearson.co.uk/product/174/71/3816/" | cut -d'/' -f1-3
Dee
источник
9

Другой способ, не используя регулярное выражение, это использовать метод fields / delimiter, например

string="http://www.suepearson.co.uk/product/174/71/3816/"
echo $string | awk -F"/" '{print $1,$2,$3}' OFS="/"
ghostdog74
источник
5

sed конечно есть свое место но это не один из них!

Как указал Ди: просто используйте cut. В этом случае все гораздо проще и безопаснее. Вот пример, где мы извлекаем различные компоненты из URL, используя синтаксис Bash:

url="http://www.suepearson.co.uk/product/174/71/3816/"

protocol=$(echo "$url" | cut -d':' -f1)
host=$(echo "$url" | cut -d'/' -f3)
urlhost=$(echo "$url" | cut -d'/' -f1-3)
urlpath=$(echo "$url" | cut -d'/' -f4-)

дает тебе:

protocol = "http"
host = "www.suepearson.co.uk"
urlhost = "http://www.suepearson.co.uk"
urlpath = "product/174/71/3816/"

Как видите, это гораздо более гибкий подход.

(все заслуги перед Ди)

peterh
источник
3
sed 's|(http:\/\/[^\/]+\/).*|\1|'
Лусеро
источник
1
Если вы используете "|" как ваш разделитель, вам не нужно экранировать "/".
Майкл Бэк
3

sed -E интерпретирует регулярные выражения как расширенные (современные) регулярные выражения

Обновление: -E в MacOS X, -r в GNU sed.

stepancheg
источник
4
Нет, это не так ... По крайней мере, не GNU sed.
Мишель де
7
В более широком смысле, -Eэто уникально для BSD sedи, следовательно, OS X. Ссылки на справочные страницы. -rдействительно приносит расширенные регулярные выражения в GNU,sed как отмечено в исправлении @ stephancheg. Остерегайтесь при использовании команды известной изменчивости между дистрибутивами 'nix. Я узнал, что трудный путь.
2012 г.
1
Это правильный ответ, если вы хотите использовать sed, и он наиболее применим к первоначальному вопросу.
Уилл Tice
8
-rОпция GNU sed изменяет только правила экранирования в соответствии с Appendix A Extended regular expressionsинформационным файлом и некоторыми быстрыми тестами; фактически он не добавляет не жадный квалификатор ( GNU sed version 4.2.1по крайней мере.)
eichin
1
Некоторое время GNU sed распознавался -Eкак недокументированный вариант, но в выпуске 4.2.2.177 документация была обновлена, чтобы отразить это, так что в -Eнастоящее время это нормально.
Бенджамин В.
3

Все еще есть надежда решить эту проблему с помощью чистого (GNU) sed. Несмотря на то, что это не универсальное решение, в некоторых случаях вы можете использовать «циклы» для удаления всех ненужных частей строки, например:

sed -r -e ":loop" -e 's|(http://.+)/.*|\1|' -e "t loop"
  • -r: использовать расширенное регулярное выражение (для + и неэкранированных скобок)
  • ": loop": определить новую метку с именем "loop"
  • -e: добавить команды в sed
  • «t loop»: вернуться к метке «loop», если произошла успешная замена

Единственная проблема здесь заключается в том, что он также обрезает последний символ-разделитель ('/'), но если он вам действительно нужен, вы все равно можете просто вернуть его после окончания цикла, просто добавьте эту дополнительную команду в конце предыдущего командная строка:

-e "s,$,/,"
mTUX
источник
2

Поскольку вы специально указали, что пытаетесь использовать sed (вместо perl, cut и т. Д.), Попробуйте группировать. Это позволяет обойтись без жадного идентификатора, который может быть не распознан. Первая группа - это протокол (т. Е. «Http: //», «https: //», «tcp: //» и т. Д.). Вторая группа - это домен:

echo "http://www.suon.co.uk/product/1/7/3/" | sed "s | ^ \ (. * // \) \ ([^ /] * \). * $ | \ 1 \ 2 |"

Если вы не знакомы с группировкой, начните здесь .

BrianB
источник
1

Я понимаю, что это старая запись, но кто-то может найти ее полезной. Поскольку полное доменное имя не должно превышать общую длину 253 символов, замените. * На. \ {1, 255 \}

Иэн Хендерсон
источник
1

Это - то, как надежно выполнить не жадное сопоставление многосимвольных строк, используя sed. Допустим , вы хотите изменить каждый , foo...barчтобы <foo...bar>так, например , этот вход:

$ cat file
ABC foo DEF bar GHI foo KLM bar NOP foo QRS bar TUV

должен стать этот вывод:

ABC <foo DEF bar> GHI <foo KLM bar> NOP <foo QRS bar> TUV

Для этого вы конвертируете foo и bar в отдельные символы, а затем используете отрицание этих символов между ними:

$ sed 's/@/@A/g; s/{/@B/g; s/}/@C/g; s/foo/{/g; s/bar/}/g; s/{[^{}]*}/<&>/g; s/}/bar/g; s/{/foo/g; s/@C/}/g; s/@B/{/g; s/@A/@/g' file
ABC <foo DEF bar> GHI <foo KLM bar> NOP <foo QRS bar> TUV

В приведенном выше:

  1. s/@/@A/g; s/{/@B/g; s/}/@C/gпреобразует {и }в строки-заполнители, которые не могут существовать во входных данных, поэтому эти символы затем доступны для преобразования fooиbar в.
  2. s/foo/{/g; s/bar/}/gпреобразует fooи barк {и} , соответственно ,
  3. s/{[^{}]*}/<&>/gвыполняет операцию, которую мы хотим - преобразование foo...barв<foo...bar>
  4. s/}/bar/g; s/{/foo/gпреобразует {и }обратно к fooиbar .
  5. s/@C/}/g; s/@B/{/g; s/@A/@/g преобразует строки заполнителя обратно в их исходные символы.

Обратите внимание, что вышеприведенное не зависит от какой-либо конкретной строки, отсутствующей во входных данных, поскольку она производит такие строки на первом шаге, и не заботится о том, какое вхождение какого-либо конкретного регулярного выражения вы хотите сопоставить, поскольку вы можете использовать {[^{}]*}столько раз, сколько необходимо в выражении, чтобы изолировать фактическое совпадение, которое вы хотите, и / или с оператором числового совпадения seds, например, чтобы заменить только второе вхождение:

$ sed 's/@/@A/g; s/{/@B/g; s/}/@C/g; s/foo/{/g; s/bar/}/g; s/{[^{}]*}/<&>/2; s/}/bar/g; s/{/foo/g; s/@C/}/g; s/@B/{/g; s/@A/@/g' file
ABC foo DEF bar GHI <foo KLM bar> NOP foo QRS bar TUV
Эд Мортон
источник
1

Еще не видели этот ответ, поэтому вот как вы можете сделать это с помощью viили vim:

vi -c '%s/\(http:\/\/.\{-}\/\).*/\1/ge | wq' file &>/dev/null

Это запускает vi :%sподстановку глобально (завершающий g), воздерживается от выдачи ошибки, если шаблон не найден ( e), затем сохраняет полученные изменения на диск и завершает работу. Это &>/dev/nullпредотвращает кратковременное мигание графического интерфейса на экране, что может раздражать.

Мне нравится viиногда использовать для сверхсложных регулярных выражений, потому что (1) perl умер замертво , (2) vim имеет очень продвинутый движок регулярных выражений, и (3) я уже близко знаком с viрегулярными выражениями в моем повседневном редактировании использования документы.

Люк Дэвис
источник
0
echo "/home/one/two/three/myfile.txt" | sed 's|\(.*\)/.*|\1|'

не беспокойтесь, я получил это на другом форуме :)

Dee
источник
4
так что вы получите жадное совпадение:, /home/one/two/three/если вы добавите еще одно, /как /home/one/two/three/four/myfile.txtвы будете жадно совпадать four:, /home/one/two/three/fourвопрос о не жадном
stefanB
0

sed 's|\(http:\/\/www\.[a-z.0-9]*\/\).*|\1| тоже работает

GL2014
источник
0

Вот что вы можете сделать с помощью двухэтапного подхода и awk:

A=http://www.suepearson.co.uk/product/174/71/3816/  
echo $A|awk '  
{  
  var=gensub(///,"||",3,$0) ;  
  sub(/\|\|.*/,"",var);  
  print var  
}'  

Выход: http://www.suepearson.co.uk

Надеюсь, это поможет!

ВИНАЙ НАИР
источник
0

Еще одна версия sed:

sed 's|/[:alnum:].*||' file.txt

Это соответствует /сопровождаемый буквенно-цифровым символом (таким образом, не другой слеш), а также остальными символами до конца строки. После этого он заменяет его ничем (т.е. удаляет его).

sycamorex
источник
1
Думаю, так и должно быть "[[:alnum:]]", нет "[:alphanum:]".
oli_arborum