У меня есть файл, содержащий список пар замены (около 100 из них), которые используются sed для замены строк в файлах.

Пары выглядят так:

old|new
tobereplaced|replacement
(stuffiwant).*(too)|\1\2

И мой текущий код:

cat replacement_list | while read i
do
    old=$(echo "$i" | awk -F'|' '{print $1}')    #due to the need for extended regex
    new=$(echo "$i" | awk -F'|' '{print $2}')
    sed -r "s/`echo "$old"`/`echo "$new"`/g" -i file
done

Не могу не думать, что есть более оптимальный способ выполнения замен. Я попытался повернуть цикл, чтобы сначала просмотреть строки файла, но это оказалось намного дороже.

Есть ли другие способы ускорить этот скрипт?

ИЗМЕНИТЬ

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

Одна вещь, которую нужно прояснить: мне также нужна функциональность подвыражений / групп. Например, мне может понадобиться одна замена:

([0-9])U|\10  #the extra brackets and escapes were required for my original code

Некоторые подробности об улучшениях (будут обновлены):

  • Метод: время обработки
  • Исходный скрипт: 0.85 сек.
  • cut вместо awk: 0,71 с
  • метод анубхавы: 0,18 с
  • метод chthonicdaemon: 0,01 с
8
Reuben L. 29 Авг 2014 в 10:50
На этот вопрос есть ответы здесь. Да, вы ищете скорость, но пожалуйста, почему два вопроса.
 – 
martin
29 Авг 2014 в 12:22
1
Честно говоря, этот вопрос на самом деле не затрагивает элемент скорости или подвыражения. Ответы, которые дали здесь, были гораздо более полезными.
 – 
Reuben L.
29 Авг 2014 в 18:24
1
Хорошо, тогда уточните свой вопрос относительно подвыражений, поместив их в данные и предоставив ввод и желаемый результат, что значительно улучшит ваш вопрос и четко отличит его от других.
 – 
martin
29 Авг 2014 в 18:36
+1 за выполнение всех тестов. Я сам научился нескольким трюкам.
 – 
anubhava
29 Авг 2014 в 19:18

9 ответов

Лучший ответ

Вы можете использовать sed для создания правильно отформатированного ввода sed:

sed -e 's/^/s|/; s/$/|g/' replacement_list | sed -r -f - file
12
chthonicdaemon 31 Авг 2014 в 09:42
1
Хммм sed: -e expression #1, char 17: unknown option to 's'. символ 17 оказывается | разделитель в моем файле замен
 – 
Reuben L.
29 Авг 2014 в 18:45
Сказав это, теперь я понял концепцию и пытаюсь ее проверить.
 – 
Reuben L.
29 Авг 2014 в 18:58
2
Проблема с запятой (опечатка?). но в любом случае, абсолютно невероятная скорость и довольно экономичная! Благодарность!
 – 
Reuben L.
29 Авг 2014 в 19:06
Извините за это - я редактировал выражение и не проверял последнюю итерацию. Рад, что ты понял это.
 – 
chthonicdaemon
31 Авг 2014 в 09:43
1
Вы можете использовать FIFO. Замените file в конце на <( whatever your command to generate input )
 – 
chthonicdaemon
27 Ноя 2014 в 18:50

Недавно я протестировал различные методы замены строк, в том числе пользовательскую программу, sed -e, perl -lnpe и, вероятно, не очень известная утилита командной строки MySQL, replace. replace оптимизирован для замены строк. был почти на порядок быстрее, чем sed. Результаты выглядели примерно так (сначала самые медленные):

custom program > sed > LANG=C sed > perl > LANG=C perl > replace

Если вам нужна производительность, используйте replace . Однако, чтобы он был доступен в вашей системе, вам необходимо установить некоторый дистрибутив MySQL.

Из replace.c:

Заменить строки в текстовом файле

Эта программа заменяет строки в файлах или с stdin на stdout. Он принимает список пар от строки до строки и заменяет каждое вхождение строки от соответствующей строкой до. Сопоставляется первое вхождение найденной строки. Если существует более одной возможности для замены строки, более длинные совпадения предпочтительнее, чем более короткие совпадения.

...

Программы создают из строк конечный автомат DFA, и скорость не зависит от количества заменяемых строк (только от количества замен). Предполагается, что строка заканчивается \ n или \ 0. Нет ограничений на длину строк, кроме памяти.


Подробнее о sed. Вы можете использовать несколько ядер с sed, разделив ваши замены на группы #cpus, а затем перенаправив их через команды sed, примерно так:

$ sed -e 's/A/B/g; ...' file.txt | \
  sed -e 's/B/C/g; ...' | \
  sed -e 's/C/D/g; ...' | \
  sed -e 's/D/E/g; ...' > out

Кроме того, если вы используете sed или perl и ваша система имеет настройку UTF-8, то это также повышает производительность, если поместить LANG=C перед командами:

$ LANG=C sed ...
4
Community 20 Июн 2020 в 12:12
Что касается этой темы, sed работает быстрее с N числом -e или N числом единичных команд sed? Когда N > 100.
 – 
Reuben L.
29 Авг 2014 в 11:03
IIRC, было немного быстрее использовать N замен в одной команде sed, чем N количество команд sed. Помню, меня немного удивило, что параллельное выполнение нескольких сотен процессов не слишком сильно снизило производительность.
 – 
miku
29 Авг 2014 в 11:12
1
Mysql replace может заменять только фиксированные строки. sd — аналогичный инструмент в rust.
 – 
milahu
27 Янв 2022 в 10:52

Вы можете сократить ненужные вызовы awk и использовать BASH для разрыва пар имя-значение:

while IFS='|' read -r old new; do
   # echo "$old :: $new"
   sed -i "s~$old~$new~g" file
done < replacement_list

IFS = '|' даст разрешение на чтение для заполнения значения имени в двух разных переменных оболочки old и new.

Предполагается, что ~ не присутствует в ваших парах "имя-значение". Если это не так, не стесняйтесь использовать альтернативный разделитель sed.

1
anubhava 29 Авг 2014 в 11:01
1
Это кажется очень быстрым, но у меня проблемы с подвыражениями. Вместо того, чтобы возвращать значения, хранящиеся в группах, я получаю их буквально (например, \1 \2 и т. д.).
 – 
Reuben L.
29 Авг 2014 в 18:10
Можете ли вы сказать мне несколько примеров строк с этими подвыражениями, чтобы я мог воспроизвести их и предложить вам исправление.
 – 
anubhava
29 Авг 2014 в 18:15
Спасибо за ответ, одним из примеров является ([0-9])U|\\10.
 – 
Reuben L.
29 Авг 2014 в 18:16
1
Спасибо за ответ и дополнительную помощь! К сожалению, мне придется проголосовать за ответ chthonicdaemon за то, что он быстрее и немного экономнее.
 – 
Reuben L.
29 Авг 2014 в 19:14
1
Нет сомнений в достоинстве ответа chthonicdaemon. Я сам проголосовал за этот новаторский трюк.
 – 
anubhava
29 Авг 2014 в 19:16

Вот что я бы попробовал:

  1. сохраните вашу пару поиска-замены sed в массиве Bash, например:
  2. создайте свою команду sed на основе этого массива, используя расширение параметра
  3. Команда запуска.
patterns=(
  old new
  tobereplaced replacement
)
pattern_count=${#patterns[*]} # number of pattern
sedArgs=() # will hold the list of sed arguments

for (( i=0 ; i<$pattern_count ; i=i+2 )); do # don't need to loop on the replacement…
  search=${patterns[i]};
  replace=${patterns[i+1]}; # … here we got the replacement part
  sedArgs+=" -e s/$search/$replace/g"
done
sed ${sedArgs[@]} file

Это приведет к этой команде:

sed -e s / старый / новый / g -e s / tobereplaced / замена / g файл

1
Édouard Lopez 29 Авг 2014 в 12:04

Вы можете попробовать это.

pattern=''
cat replacement_list | while read i
do
    old=$(echo "$i" | awk -F'|' '{print $1}')    #due to the need for extended regex
    new=$(echo "$i" | awk -F'|' '{print $2}')
    pattern=${pattern}"s/${old}/${new}/g;"
done
sed -r ${pattern} -i file

Это запустит команду sed только один раз для файла со всеми заменами. Вы также можете заменить awk на cut. cut может быть более оптимизирован, чем awk, хотя я в этом не уверен.

old=`echo $i | cut -d"|" -f1`
new=`echo $i | cut -d"|" -f2`
0
nisargjhaveri 29 Авг 2014 в 11:03
Улучшение на 0,3 с. Неплохо.
 – 
Reuben L.
29 Авг 2014 в 11:11
Я ошибся, cut действительно ускорил процесс, но бит шаблона на самом деле не работал. По какой-то причине первый символ имени файла, переданного sed, был удален. Пытаюсь понять почему.
 – 
Reuben L.
29 Авг 2014 в 17:48
бесполезное использование cat и несколько ошибки цитирования не сулит ничего хорошего для этого ответа. Действовать с осторожностью.
 – 
tripleee
25 Мар 2019 в 14:42

Возможно, вы захотите проделать все это на awk:

awk -F\| 'NR==FNR{old[++n]=$1;new[n]=$2;next}{for(i=1;i<=n;++i)gsub(old[i],new[i])}1' replacement_list file

Составьте список старых и новых слов из первого файла. next гарантирует, что остальная часть скрипта не будет запущена для первого файла. Для второго файла прокрутите список замен и выполните их каждую по очереди. 1 в конце означает, что строка напечатана.

0
Tom Fenech 29 Авг 2014 в 11:40
Одна проблема для меня заключается в том, что я использую группы (т.е. \1) в заменах sed.
 – 
Reuben L.
29 Авг 2014 в 18:02
Вы используете гавк? Если это так, это может быть адаптировано для использования gensub
 – 
Tom Fenech
29 Авг 2014 в 18:10
{ cat replacement_list;echo "-End-"; cat YourFile; } | sed -n '1,/-End-/ s/$/³/;1h;1!H;$ {g
t again
:again
   /^-End-³\n/ {s///;b done
      }
   s/^\([^|]*\)|\([^³]*\)³\(\n\)\(.*\)\1/\1|\2³\3\4\2/
   t again
   s/^[^³]*³\n//
   t again
:done
  p
  }'

Больше удовольствия от кода через sed. Попробуйте, может быть, какое-то время, потому что это запускает только 1 рекурсивный sed.

Для posix sed (так что --posix с GNU sed)

< Сильный > объяснений

  • скопируйте список замены перед содержимым файла с разделителем (для строки с ³ и для списка с -End-) для упрощения обработки sed (трудно использовать \ n в символе класса в posix sed.
  • поместить всю строку в буфер (добавить разделитель строки для списка замены и -End- перед)
  • если это -End-³, удалите строку и перейдите к окончательной печати
  • заменить каждый первый образец (группа 1) в тексте вторым образцом (группа 2)
  • если найдено, перезапустите (t again)
  • удалить первую строку
  • перезапустить процесс (t again). T необходим, потому что b не сбрасывает тест, а следующий t всегда истинен.
0
NeronLeVelu 29 Авг 2014 в 12:43

Спасибо @miku выше;

У меня есть файл размером 100 МБ со списком строк замены 80k.

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

Вместо этого я помещаю свой список в последовательность сценариев вроде «cat in | replace aold anew bold bnew cold cnew ...> out; rm in; mv out in».

Я случайным образом выбрал 1000 замен для каждого файла, поэтому все происходило так:

# first, split my replace-list into manageable chunks (89 files in this case)
split -a 4 -l 1000 80kReplacePairs rep_

# next, make a 'replace' script out of each chunk
for F in rep_* ; do \
    echo "create and make executable a scriptfile" ; \
    echo '#!/bin/sh' > run_$F.sh ; chmod +x run_$F.sh ; \
    echo "for each chunk-file line, strip line-ends," ; \
    echo "then with sed, turn '{long list}' into 'cat in | {long list}' > out" ; \
    cat $F | tr '\n' ' ' | sed 's/^/cat in | replace /;s/$/ > out/' >> run_$F.sh ;
    echo "and append commands to switch in and out files, for next script" ; \
    echo -e " && \\\\ \nrm in && mv out in\n" >> run_$F.sh ; \
done

# put all the replace-scripts in sequence into a main script
ls ./run_rep_aa* > allrun.sh

# make it executable
chmod +x allrun.sh 

# run it
nohup ./allrun.sh &

.. который работал менее чем за 5 минут, намного меньше чем за 20 часов!

Оглядываясь назад, я мог бы использовать больше пар для каждого сценария, если бы определил, сколько строк составят предел.

xargs --show-limits </dev/null 2>&1 | grep --color=always "actually use:"
    Maximum length of command we could actually use: 2090490

Так что чуть меньше 2 МБ; сколько пар это было бы для моего скрипта?

head -c 2090490 80kReplacePairs | wc -l

    76923

Кажется, я мог бы использовать 2 * 40000-строчных фрагментов

0
jmullee 23 Фев 2021 в 16:41

Чтобы расширить решение chthonicdaemon

живая демонстрация

#! /bin/sh

# build regex from text file

REGEX_FILE=some-patch.regex.diff

# test
# set these with "export key=val"
SOME_VAR_NAME=hello
ANOTHER_VAR_NAME=world


escape_b() {
  echo "$1" | sed 's,/,\\/,g'
}


regex="$(
  (echo; cat "$REGEX_FILE"; echo) \
  | perl -p -0 -e '
    s/\n#[^\n]*/\n/g;
    s/\(\(SOME_VAR_NAME\)\)/'"$(escape_b "$SOME_VAR_NAME")"'/g;
    s/\(\(ANOTHER_VAR_NAME\)\)/'"$(escape_b "$ANOTHER_VAR_NAME")"'/g;
    s/([^\n])\//\1\\\//g;
    s/\n-([^\n]+)\n\+([^\n]*)(?:\n\/([^\n]+))?\n/s\/\1\/\2\/\3;\n/g;
  '
)"

echo "regex:"; echo "$regex" # debug

exec perl -00 -p -i -e "$regex" "$@"

Строки с префиксом -+/ допускают пустые значения «плюс» и защищают начальные пробелы от ошибочных текстовых редакторов.

Пример ввода: some-patch.regex.diff

# file format is similar to diff/patch
# this is a comment

# replace all "a/a" with "b/b"
-a/a
+b/b
/g

-a1|a2
+b1|b2
/sg
# this is another comment

-(a1).*(a2)
+b\1b\2b

-a\na\na
+b

-a1-((SOME_VAR_NAME))-a2
+b1-((ANOTHER_VAR_NAME))-b2

Образец вывода

s/a\/a/b\/b/g;

s/a1|a2/b1|b2/;;

s/(a1).*(a2)/b\1b\2b/;

s/a\na\na/b/;

s/a1-hello-a2/b1-world-b2/;

Этот формат регулярного выражения совместим с sed и perl

Поскольку miku упомянул mysql replace: замена фиксированных строк регулярным выражением нетривиальна, так как вы должны экранировать все символы регулярных выражений, но вы также должны обрабатывать экранирование обратной косой черты...

Наивный беглец:

echo '\(\n' | perl -p -e 's/([.+*?()\[\]])/\\\1/g' 
\\(\n
0
milahu 27 Янв 2022 в 15:36