Sed - заменить первые k экземпляров слова в файле


24

Я хочу заменить только первые kэкземпляры слова.

Как я могу это сделать?

Например. Скажем, файл foo.txtсодержит 100 вхождений слова «linux».

Мне нужно заменить только первые 50 случаев.


1
Вы можете сослаться на это: unix.stackexchange.com/questions/21178/…
cuonglm

Вам нужен конкретно sed, или приемлемы другие инструменты? Вам нужно работать в командной строке, или текстовый редактор приемлем?
evilsoup

Все, что работает в командной строке, приемлемо.
Нарендра-Чоудхары

Ответы:


31

Первый раздел ниже описывает использование sedдля изменения первых k вхождений в строке. Второй раздел расширяет этот подход, чтобы изменить только первые k-вхождений в файле, независимо от того, на какой строке они появляются.

Линейно-ориентированное решение

В стандартном sed есть команда для замены k-го вхождения слова в строке. Если k3, например:

sed 's/old/new/3'

Или можно заменить все вхождения на:

sed 's/old/new/g'

Ни то, ни другое вы не хотите.

GNU sedпредлагает расширение, которое изменит k-й случай и все после этого. Если k равно 3, например:

sed 's/old/new/g3'

Их можно комбинировать, чтобы делать то, что вы хотите. Чтобы изменить первые 3 вхождения:

$ echo old old old old old | sed -E 's/\<old\>/\n/g4; s/\<old\>/new/g; s/\n/old/g'
new new new old old

где \nполезно здесь , потому что мы можем быть уверены , что никогда не происходит на линии.

Объяснение:

Мы используем три sedкоманды замещения:

  • s/\<old\>/\n/g4

    Это расширение GNU для замены четвертого и всех последующих вхождений oldс \n.

    Расширенная функция регулярного выражения \<используется, чтобы соответствовать началу слова и \>совпадать с концом слова. Это гарантирует, что сопоставляются только полные слова. Расширенное регулярное выражение требует -Eопции sed.

  • s/\<old\>/new/g

    Только первые три вхождения oldостаются, и это заменяет их всех new.

  • s/\n/old/g

    Четвертый и все остальные вхождения oldбыли заменены \nна первом шаге. Это возвращает их обратно в исходное состояние.

Решение без GNU

Если GNU sed недоступен и вы хотите изменить первые 3 вхождения oldна new, тогда используйте три sкоманды:

$ echo old old old old old | sed -E -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'
new new new old old

Это хорошо работает, когда kнебольшое число, но плохо масштабируется до большого k.

Так как некоторые не-GNU seds не поддерживают объединение команд с точкой с запятой, каждая команда здесь представлена ​​со своей -eопцией. Также может быть необходимо убедиться, что вы sedподдерживаете символы границы слова, \<и \>.

Файловое решение

Мы можем сказать sed прочитать весь файл и затем выполнить замены. Например, чтобы заменить первые три случая oldиспользования sed в стиле BSD:

sed -E -e 'H;1h;$!d;x' -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'

Команды sed H;1h;$!d;xчитают весь файл в.

Поскольку вышеупомянутое не использует никакого расширения GNU, оно должно работать на sed BSD (OSX). Обратите внимание, думал, что этот подход требует, sedчтобы можно было обрабатывать длинные строки. GNU sedдолжно быть хорошо. Те, кто использует не GNU-версию, sedдолжны проверить ее способность обрабатывать длинные строки.

С помощью GNU sed мы можем дополнительно использовать gописанный выше прием, но с \nзаменой на \x00, чтобы заменить первые три вхождения:

sed -E -e 'H;1h;$!d;x; s/\<old\>/\x00/g4; s/\<old\>/new/g; s/\x00/old/g'

Этот подход хорошо масштабируется и kстановится большим. Это предполагает, однако, что \x00это не в вашей исходной строке. Поскольку невозможно поместить символ \x00в строку bash, это обычно безопасное предположение.


5
Это работает только для строк и изменит первые 4 вхождения в каждой строке

1
@mikeserv Отличная идея! Ответ обновлен.
John1024

(1) Вы упоминаете GNU и non-GNU sed и предлагаете tr '\n' '|' < input_file | sed …. Но, конечно, это преобразует весь ввод в одну строку, и некоторые не-GNU seds не могут обрабатывать произвольно длинные строки. (2) Вы говорите: «… выше, строка в кавычках '|'должна быть заменена любым символом или строкой символов…» Но вы не можете использовать trдля замены символа строкой (длиной> 1). (3) В последнем примере вы говорите -e 's/\<old\>/new/' -e 's/\<old\>/w/' | tr '\000' '\n'\>/new. Кажется, это опечатка для -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/' | tr '\000' '\n'.
G-Man говорит: «Восстановите Монику»

@ G-Man Большое спасибо! Я обновил ответ.
John1024

это так безобразно
Луи Мэддокс,

8

Использование Awk

Команды awk могут использоваться для замены первых N вхождений слова на замену.
Команды будут заменены, только если слово полностью соответствует.

В приведенных ниже примерах, я вместо первых 27вхождений oldсnew

Используя суб

awk '{for(i=1;i<=NF;i++){if(x<27&&$i=="old"){x++;sub("old","new",$i)}}}1' file

Эта команда проходит по каждому полю до совпадения old, проверяет, что значение счетчика меньше 27, увеличивается и заменяет первое совпадение в строке. Затем перемещается на следующее поле / строку и повторяется.

Замена поля вручную

awk '{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file

Подобно команде ранее, но, поскольку у нее уже есть маркер, к которому она относится ($i), она просто меняет значение поля с oldна new.

Выполнение проверки перед

awk '/old/&&x<27{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file

Проверка того, что строка содержит старые данные и счетчик ниже 27, SHOULDобеспечивает небольшое повышение скорости, поскольку они не будут обрабатывать строки, если они ложные.

ПОЛУЧЕННЫЕ РЕЗУЛЬТАТЫ

Например

old bold old old old
old old nold old old
old old old gold old
old gold gold old old
old old old man old old
old old old old dog old
old old old old say old
old old old old blah old

в

new bold new new new
new new nold new new
new new new gold new
new gold gold new new
new new new man new new
new new new new dog new
new new old old say old
old old old old blah old

Первый (с использованием sub) делает неправильные вещи, если строка «old» предшествует * слову old; например, «Дайте немного золота старику». → «Дайте немного старика».
G-Man говорит: «Восстановите Монику»

@ G-Man Да, я забыл $iнемного, его отредактировали, спасибо :)

7

Скажем, вы хотите заменить только первые три экземпляра строки ...

seq 11 100 311 | 
sed -e 's/1/\
&/g'              \ #s/match string/\nmatch string/globally 
-e :t             \ #define label t
-e '/\n/{ x'      \ #newlines must match - exchange hold and pattern spaces
-e '/.\{3\}/!{'   \ #if not 3 characters in hold space do
-e     's/$/./'   \ #add a new char to hold space
-e      x         \ #exchange hold/pattern spaces again
-e     's/\n1/2/' \ #replace first occurring '\n1' string w/ '2' string
-e     'b t'      \ #branch back to label t
-e '};x'          \ #end match function; exchange hold/pattern spaces
-e '};s/\n//g'      #end match function; remove all newline characters

примечание: вышеупомянутое, скорее всего, не будет работать со встроенными комментариями
... или в моем примере с "1" ...

ВЫХОД:

22
211
211
311

Там я использую две известные техники. Во-первых, каждое вхождение 1на линии заменяется на \n1. Таким образом, поскольку я делаю рекурсивные замены далее, я могу быть уверен, что не заменим вхождение дважды, если моя строка замены содержит мою строку замены. Например, если я заменю heна heyнего, все равно будет работать.

Я делаю это так:

s/1/\
&/g

Во-вторых, я рассчитываю замены, добавляя символ в hстарое место для каждого вхождения. Как только я достигну трех, больше не произойдет. Если вы примените это к своим данным и измените \{3\}общее количество замен, которые вы хотите, и /\n1/адреса на то, что вы хотите заменить, вы должны заменить только столько, сколько пожелаете.

Я сделал все -eдля удобства чтения. POSIXly это может быть написано так:

nl='
'; sed "s/1/\\$nl&/g;:t${nl}/\n/{x;/.\{3\}/!{${nl}s/$/./;x;s/\n1/2/;bt$nl};x$nl};s/\n//g"

И ж / GNU sed:

sed 's/1/\n&/g;:t;/\n/{x;/.\{3\}/!{s/$/./;x;s/\n1/2/;bt};x};s/\n//g'

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

Вот небольшая функция оболочки, которая объединяет ее в просто выполняемую команду:

firstn() { sed "s/$2/\
&/g;:t 
    /\n/{x
        /.\{$(($1))"',\}/!{
            s/$/./; x; s/\n'"$2/$3"'/
            b t
        };x
};s/\n//g'; }

Итак, с этим я могу сделать:

seq 11 100 311 | firstn 7 1 5

...и получить...

55
555
255
311

...или...

seq 10 1 25 | firstn 6 '\(.\)\([1-5]\)' '\15\2'

...получить...

10
151
152
153
154
155
16
17
18
19
20
251
22
23
24
25

... или, в соответствии с вашим примером (в меньшем порядке) :

yes linux | head -n 10 | firstn 5 linux 'linux is an os kernel'
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux
linux
linux
linux
linux

4

Краткая альтернатива в Perl:

perl -pe 'BEGIN{$n=3} 1 while s/old/new/ && ++$i < $n' your_file

Измените значение `$ n $ по своему вкусу.

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

  • Для каждой строки, он продолжает пытаться заменить newна old( s/old/new/) и всякий раз , когда это возможно, это увеличивает переменную $i( ++$i).
  • Он продолжает работать со строкой ( 1 while ...) до тех пор, пока он произвел меньше, чем $nподстановок, и может сделать хотя бы одну замену в этой строке.

4

Используйте петлю оболочки и ex!

{ for i in {1..50}; do printf %s\\n '0/old/s//new/'; done; echo x;} | ex file.txt

Да, это немного глупо.

;)

Примечание. Может произойти сбой, если oldв файле содержится менее 50 экземпляров . (Я не проверял это.) Если так, это оставило бы файл без изменений.


Еще лучше использовать Vim.

vim file.txt
qqgg/old<CR>:s/old/new/<CR>q49@q
:x

Объяснение:

q                                # Start recording macro
 q                               # Into register q
  gg                             # Go to start of file
    /old<CR>                     # Go to first instance of 'old'
            :s/old/new/<CR>      # Change it to 'new'
                           q     # Stop recording
                            49@q # Replay macro 49 times

:x  # Save and exit

: s // новый <CR> должны работать , как хорошо, так как пустое регулярное выражение повторно последний использовавшийся поиска
Айке

3

Простое, но не очень быстрое решение - это циклическое переключение команд, описанных в /programming/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a -файл

for i in $(seq 50) ; do sed -i -e "0,/oldword/s//newword/"  file.txt  ; done

Эта конкретная команда sed, вероятно, работает только для GNU sed, и если newword не является частью oldword . Для не-GNU sed смотрите здесь, как заменить только первый шаблон в файле.


+1 за определение того, что замена «старого» на «жирный» может вызвать проблемы.
G-Man говорит: «Восстановите Монику»

2

С GNU awkвы можете установить разделитель RSна слово , чтобы быть замененными разделителей по границам слов. Тогда это случай установки разделителя записей на выходе для слова замены для первых kзаписей, в то же время сохраняя оригинальный разделитель записей для остатка

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, NR <= limit? replacement: RT}' file

ИЛИ

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, limit--? replacement: RT}' file
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.