Не жадібний (неохоче) збіг регулярних виразів у sed?


406

Я намагаюся використовувати sed для очищення рядків URL-адрес, щоб витягти лише домен.

Отже з:

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

Мені потрібно:

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

(з або без кінцевої косої риси, це не має значення)

Я намагався:

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

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

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

але я, здається, не можу змусити не жадібний квантор ( ?) працювати, тому він завжди закінчується відповідності цілому рядку.


54
Побічна примітка: якщо ви розмежуєте свої реджекси символом "|", вам не потрібно уникати "/" s. Насправді більшість людей розмежовують слово "|" замість "/" s, щоб уникнути "пікетних огорож".
AttishOculus

12
@AttishOculus Першим символом після 's' у виразі замінника в sed є роздільник. Отже 's ^ foo ^ bar ^' або 's! Foo! Bar!' також працюють
Кальмари

1
Для розширеного регулярного вираження використовуйте sed -E 's.... Все-таки жоден неохочий оператор.
Ондра Жижка

Не відповідає на заголовок питання, але в даному конкретному випадку просто cut -d'/' -f1-3працює.
Петро Яворик

Відповіді:


421

Ні базовий, ні розширений регулярний вираз Posix / GNU не визнає не жадібний кількісний показник; вам потрібна пізніша регулярна виразка. На щастя, підсумок Perl для цього контексту досить легко отримати:

perl -pe 's|(http://.*?/).*|\1|'

12
Для цього на місці використовуйте варіанти -pi -e.
realnice

11
Святий курить, я не можу повірити, що спрацювало :-) Єдине, що зараз смокче - це мій сценарій має залежність від Perl :-( З іншого боку, практично у всіх дистрибутивах Linux Perl вже є, ймовірно, це не проблема :-)
Freedom_Ben

6
@Freedom_Ben: IIRC perlце потрібно за стандартом POSIX
MestreLion

4
@ dolphus333: "Ні базовий, ні розширений регулярний вираз Posix / GNU не розпізнає не жадібний квантор", означає "ви не можете використовувати не жадібний квантор у sed".
хаос

3
@ Sérgio - це те, як ти робиш запитувану річ, чого неможливо sed, використовуючи синтаксис, принципово ідентичний такомуsed
хаос

250

У цьому конкретному випадку ви можете виконати роботу, не використовуючи не жадібний регулярний вираз.

Спробуйте цей не жадібний регулярний вираз [^/]*замість .*?:

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

3
Як змусити цю техніку за допомогою цієї методики відповідати не жадібній фразі?
користувач3694243

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/

8
Щодо останніх редагувань: дужки є своєрідним символом дужок, тому невірно називати їх дужками, особливо якщо ви дотримуєтесь слова за фактичними символами, як це робив автор. Крім того, це переважне використання в деяких культурах, тому замінити його на бажане використання у вашій власній культурі здається трохи грубим, хоча я впевнений, що це не те, що редактор задумав. Особисто я вважаю, що найкраще використовувати суто описові назви, наприклад круглі дужки , квадратні дужки та кутові дужки .
Алан Мур

2
Чи можна замінити роздільник рядком?
Калькулем

37

sed не підтримує оператора "не жадібний".

Ви повинні використовувати оператор "[]", щоб виключити "/" з відповідності.

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

PS немає потреби в нахилі "/".


не зовсім. якщо роздільник може бути одним із багатьох можливих символів (скажімо лише рядок чисел), ваш заперечувальний збіг може ставати все більш складним. це добре, але, безумовно, було б непогано мати можливість зробити. * non
pohedy

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/

Примітка: це не працюватиме з однаковими роздільниками.


3) пропонуючи такі сайти, як regex101 для демонстрації, додайте зауваження, що він не завжди підходить для інструментів cli через синтаксис та відмінності функцій
Sundeep

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/…
Сундіп

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

Звичайно, за допомогою більш простого введення ви могли б використовувати слово менше, а може, навіть один символ.

HTH!


4
Щоб зробити його ще кращим, корисним у ситуації, коли ви не можете очікувати невживаного символу: 1. замініть цей спеціальний символ справді невикористаним словом, 2. замініть закінчувальну послідовність на спеціальний символ, 3. виконайте пошук, що закінчується спеціальним символом, 4 замінити спеціальний символ назад, 5. замінити спеціальне слово WORD назад. Наприклад, вам потрібен жадібний оператор між <hello> і </hello>:
Якуб

3
Ось приклад: echo "Знайти: <hello> fir ~ st. Так </hello> <hello> sec ~ ond </hello>" | sed -e "s, ~, VERYSPECIAL, g" -e "s, </hello>, ~, g" -e "s,. * Знайдіть: <hello> ([^ ~] *). *, \ 1 , "-e" s, \ ~, </hello>, "-e" s, ВЕЛИЧЕЗНИЙ, ~, "
Якуб

2
Я згоден. приємне рішення. Я б переформулював коментар так: якщо ви не можете розраховувати на те, що ~ не використовується, замініть його поточні події спочатку за допомогою s / ~ / VERYspeciaL / g, потім виконайте вищевказаний трюк, а потім поверніть оригінал ~ за допомогою s / VERYspeciaL / ~ / g
ішахак

1
Я зазвичай люблю використовувати рідші "змінні" для подібних речей, тому замість цього `я б використовував <$$>(оскільки $$розширюється на ваш ідентифікатор процесу в оболонці, хоча вам доведеться використовувати подвійні лапки, а не одиничні лапки, і це може зламати інші частини вашого регулярного виразу) або, якщо доступно unicode, щось подібне <∈∋>.
Адам Кац

У якийсь момент ви повинні запитати себе, чому ви просто не використовуєте perlабо pythonчи якусь іншу мову. perlробить це менш тендітним способом в одному рядку ...
ArtOfWarfare

18

sed - не жадібна відповідність Крістофа Зігарта

Трюк, щоб отримати не жадібну відповідність у sed, полягає в тому, щоб відповідати всім персонажам, крім того, що припиняє збіг. Я знаю, що не вимагає розуму, але я витрачав на це дорогоцінні хвилини і сценарії оболонки, зрештою, повинен бути швидким і легким. Тож у випадку, якщо комусь це може знадобитися:

Жадібна відповідність

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

Не жадібна відповідність

% echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar

17

Це можна зробити за допомогою вирізання:

echo "http://www.suepearson.co.uk/product/174/71/3816/" | cut -d'/' -f1-3

9

Інший спосіб, не використовуючи регулярний вираз, - це використовувати поля / метод розділення, наприклад

string="http://www.suepearson.co.uk/product/174/71/3816/"
echo $string | awk -F"/" '{print $1,$2,$3}' OFS="/"

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

Як бачите, це набагато гнучкіший підхід.

(весь кредит на Ді)



3

sed -E інтерпретує регулярні вирази як розширені (сучасні) регулярні вирази

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


4
Ні, ні ... Принаймні, не GNU sed.
Мішель де Руйтер

7
Загалом, -Eвона унікальна для BSD, sedа тому OS X. Посилання на підручні сторінки. -rприносить розширені регулярні вирази до GNU,sed як зазначено в корекції @ stephancheg. Остерігайтеся, коли використовуєте команду з відомою мінливістю для nix-розподілів. Я дізнався, що важкий шлях.
fny

1
Це правильна відповідь, якщо ви хочете використовувати sed, і є найбільш застосовною для початкового питання.
Буде Тіс

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 цикл": поверніться до мітки "цикл", якщо відбулася успішна заміна

Єдина проблема тут полягає в тому, що він також виріже останній символ роздільника ('/'), але якщо він вам справді потрібен, ви все одно можете просто повернути його назад після завершення "циклу", просто додайте цю додаткову команду в кінці попередньої командний рядок:

-e "s,$,/,"

2

Оскільки ви спеціально заявили, що намагаєтесь використовувати sed (замість perl, cut тощо), спробуйте групувати. Це дозволяє обійти негативний ідентифікатор, який потенційно не може бути розпізнаний. Перша група - це протокол (тобто 'http: //', 'https: //', 'tcp: //' тощо). Друга група - домен:

відлуння "http://www.suon.co.uk/product/1/7/3/" | sed "s | ^ \ (. * // \) \ ([^ /] * \). * $ | \ 1 \ 2 |"

Якщо ви не знайомі з групуванням, починайте тут .


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

Для цього ви перетворюєте колонтитул і смугу в окремі символи, а потім використовуєте заперечення цих символів між ними:

$ 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 перетворює рядки заповнювача у свої початкові символи.

Зауважте, що вищезгадане не покладається на те, що якась конкретна рядок не присутня на вході, оскільки вона виробляє такі рядки на першому кроці, і не дбає про те, яке виникнення конкретного повторного виклику ви хочете відповідати, оскільки ви можете використовувати {[^{}]*}стільки разів, скільки потрібно в виразі, щоб виділити фактичну відповідність, яку ви хочете, та / або з оператором відповідності числовим числом, наприклад, замінити лише 2-е входження:

$ 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) ВІМ має дуже просунутий движок регулярних виразів, і (3) Я вже добре знайомі з viрегулярними виразами в моїй день у день редагування використання документи.


0
echo "/home/one/two/three/myfile.txt" | sed 's|\(.*\)/.*|\1|'

не турбуй, я отримав це на іншому форумі :)


4
тож ви отримуєте жадібну відповідність: /home/one/two/three/якщо ви додасте ще одну, /як /home/one/two/three/four/myfile.txtви будете жадібно збігатися four:, /home/one/two/three/fourпитання про ненаситну
stefanB


0

Ось що ви можете зробити з двоступеневим підходом і див:

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

Він співпадає /з буквено-цифровим символом (таким чином, не іншим нахилом вперед), а також рештою символів до кінця рядка. Потім замінює його нічим (тобто видаляє.)


1
Гадаю, так і має бути "[[:alnum:]]", ні "[:alphanum:]".
oli_arborum
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.