#perl #bash #optimization #replace #sed
#perl #bash #оптимизация #заменить #sed
Вопрос:
Я хочу выполнить около многих операций поиска и замены над некоторым текстом. У меня есть файл CSV в формате UTF-8, содержащий то, что нужно найти (в первом столбце) и чем его заменить (во втором столбце), упорядоченный от самого длинного к самому короткому.
Например.:
orange,fruit2
carrot,vegetable1
apple,fruit3
pear,fruit4
ink,item1
table,item2
Исходный файл:
"I like to eat apples and carrots"
Результирующий выходной файл:
"I like to eat fruit3s and vegetable1s."
Однако я хочу убедиться, что если одна часть текста уже была заменена, она не будет мешать тексту, который уже был заменен. Другими словами, я не хочу, чтобы это выглядело так (оно соответствовало «таблице» из vegetable1):
"I like to eat fruit3s and vegeitem21s."
В настоящее время я использую этот метод, который довольно медленный, потому что мне приходится выполнять весь поиск и замену дважды:
(1) Преобразуйте CSV в три файла, например:
a.csv b.csv c.csv
orange 0001 fruit2
carrot 0002 vegetable1
apple 0003 fruit3
pear 0004 fruit4
ink 0005 item1
table 0006 item 2
(2) Затем замените все элементы из a.csv
in file.txt
на соответствующий столбец в b.csv
, используя ZZZ
вокруг слов, чтобы убедиться, что позже не будет ошибки при сопоставлении чисел:
a=1
b=`wc -l < ./a.csv`
while [ $a -le $b ]
do
for i in `sed -n "$a"p ./b.csv`; do
for j in `sed -n "$a"p ./a.csv`; do
sed -i "s/$i/ZZZ$jZZZ/g" ./file.txt
echo "Instances of '"$i"' replaced with '"ZZZ$jZZZ"' ("$a"/"$b")."
a=`expr $a 1`
done
done
done
(3) Затем снова запустите этот же скрипт, но для замены ZZZ0001ZZZ
на fruit2
from c.csv
.
Выполнение первой замены занимает около 2 часов, но поскольку я должен запустить этот код дважды, чтобы избежать редактирования уже замененных элементов, это занимает в два раза больше времени. Есть ли более эффективный способ запуска поиска и замены, который не выполняет замены уже замененного текста?
Комментарии:
1. На каком языке или технологии вы хотите это сделать?
2. В Linux. Я не имею в виду какой-либо конкретный язык, но мне нужно убедиться, что он поддерживает UTF-8.
3. сколько строк в каждом файле?
4. Редактируемые файлы и списки состоят из 100 000 строк каждый.
Ответ №1:
Вот решение perl, которое выполняет замену в «один этап».
#!/usr/bin/perl
use strict;
my %map = (
orange => "fruit2",
carrot => "vegetable1",
apple => "fruit3",
pear => "fruit4",
ink => "item1",
table => "item2",
);
my $repl_rx = '(' . join("|", map { quotemeta } keys %map) . ')';
my $str = "I like to eat apples and carrots";
$str =~ s{$repl_rx}{$map{$1}}g;
print $str, "n";
Ответ №2:
У Tcl есть команда для выполнения именно этого: string map
tclsh <<'END'
set map {
"orange" "fruit2"
"carrot" "vegetable1"
"apple" "fruit3"
"pear" "fruit4"
"ink" "item1"
"table" "item2"
}
set str "I like to eat apples and carrots"
puts [string map $map $str]
END
I like to eat fruit3s and vegetable1s
Вот как это реализовать в bash (требуется bash v4 для ассоциативного массива)
declare -A map=(
[orange]=fruit2
[carrot]=vegetable1
[apple]=fruit3
[pear]=fruit4
[ink]=item1
[table]=item2
)
str="I like to eat apples and carrots"
echo "$str"
i=0
while (( i < ${#str} )); do
matched=false
for key in "${!map[@]}"; do
if [[ ${str:$i:${#key}} = $key ]]; then
str=${str:0:$i}${map[$key]}${str:$((i ${#key}))}
((i =${#map[$key]}))
matched=true
break
fi
done
$matched || ((i ))
done
echo "$str"
I like to eat apples and carrots
I like to eat fruit3s and vegetable1s
Это не будет быстрым.
Очевидно, что вы можете получить разные результаты, если упорядочите карту по-другому. На самом деле, я считаю, что порядок "${!map[@]}"
не определен, поэтому вы можете указать порядок ключей явно:
keys=(orange carrot apple pear ink table)
# ...
for key in "${keys[@]}"; do
Ответ №3:
Одним из способов сделать это было бы выполнить двухфазную замену:
фаза 1: s/orange/@@1##/ s/carrot/@@2##/ ... фаза 2: s/@@1##/fruit2/ s/@@2###/vegetable1/ ...
Маркеры @@1 ## следует выбирать так, чтобы они, конечно, не появлялись в исходном тексте или заменах.
Вот примерная реализация концепции в perl:
#!/usr/bin/perl -w
#
my $repls = $ARGV[0];
die ("first parameter must be the replacement list file") unless defined ($repls);
my $tmpFmt = "@@@%d###";
open(my $replsFile, "<", $repls) || die("$!: $repls");
shift;
my @replsList;
my $i = 0;
while (<$replsFile>) {
chomp;
my ($from, $to) = /"([^"]*)","([^"]*)"/;
if (defined($from) amp;amp; defined($to)) {
push(@replsList, [$from, sprintf($tmpFmt, $i), $to]);
}
}
while (<>) {
foreach my $r (@replsList) {
s/$r->[0]/$r->[1]/g;
}
foreach my $r (@replsList) {
s/$r->[1]/$r->[2]/g;
}
print;
}
Ответ №4:
Я бы предположил, что большая часть вашей медлительности связана с созданием такого количества команд sed, каждая из которых должна индивидуально обрабатывать весь файл. Некоторые незначительные корректировки вашего текущего процесса значительно ускорили бы это, запустив 1 sed на файл за шаг.
a=1
b=`wc -l < ./a.csv`
while [ $a -le $b ]
do
cmd=""
for i in `sed -n "$a"p ./a.csv`; do
for j in `sed -n "$a"p ./b.csv`; do
cmd="$cmd ; s/$i/ZZZ${j}ZZZ/g"
echo "Instances of '"$i"' replaced with '"ZZZ${j}ZZZ"' ("$a"/"$b")."
a=`expr $a 1`
done
done
sed -i "$cmd" ./file.txt
done
Ответ №5:
Выполнение этого дважды, вероятно, не ваша проблема. Если бы вам удалось сделать это только один раз, используя вашу базовую стратегию, это все равно заняло бы у вас час, верно? Вероятно, вам нужно использовать другую технологию или инструмент. Переключение на Perl, как указано выше, может сделать ваш код намного быстрее (попробуйте)
Но, продолжая путь других плакатов, следующим шагом может быть конвейеризация. Напишите небольшую программу, которая заменяет два столбца, затем запустите эту программу дважды, одновременно. При первом запуске строки в столбце 1 заменяются строками в столбце 2, при следующем — строки в столбце 2 заменяются строками в столбце 3.
Ваша командная строка будет выглядеть следующим образом
cat input_file.txt | perl replace.pl replace_file.txt 1 2 | perl replace.pl replace_file.txt 2 3 > completely_replaced.txt
И заменить.pl был бы таким (похож на другие решения)
#!/usr/bin/perl -w
my $replace_file = $ARGV[0];
my $before_replace_colnum = $ARGV[1] - 1;
my $after_replace_colnum = $ARGV[2] - 1;
open(REPLACEFILE, $replace_file) || die("couldn't open $replace_file: $!");
my @replace_pairs;
# read in the list of things to replace
while(<REPLACEFILE>) {
chomp();
my @cols = split /t/, $_;
my $to_replace = $cols[$before_replace_colnum];
my $replace_with = $cols[$after_replace_colnum];
push @replace_pairs, [$to_replace, $replace_with];
}
# read input from stdin, do swapping
while(<STDIN>) {
# loop over all replacement strings
foreach my $replace_pair (@replace_pairs) {
my($to_replace,$replace_with) = @{$replace_pair};
$_ =~ s/${to_replace}/${replace_with}/g;
}
print STDOUT $_;
}
Комментарии:
1.
cat
действительно бесполезно, и только одного perl должно быть достаточно.2. два perl позволяют выполнять конвейерную обработку
3. Вы могли бы реализовать замену как подпрограмму и вызвать ее дважды в одном perl. На самом деле вам вообще не нужны каналы.
4. это будет намного медленнее, правда. ваш подход будет использовать только один процессор / ядро.
5. Возможно, вы правы. Я собираюсь провести несколько тестов производительности… Но
cat
все еще не требуется. 😉
Ответ №6:
Подход bash sed:
count=0
bigfrom=""
bigto=""
while IFS=, read from to; do
read countmd5sum x < <(md5sum <<< $count)
count=$(( $count 1 ))
bigfrom="$bigfrom;s/$from/$countmd5sum/g"
bigto="$bigto;s/$countmd5sum/$to/g"
done < replace-list.csv
sed "${bigfrom:1}$bigto" input_file.txt
Я выбрал md5sum, чтобы получить некоторый уникальный токен. Но для генерации такого токена также можно использовать какой-либо другой механизм; например, чтение из /dev/urandom
или shuf -n1 -i 10000000-20000000
Ответ №7:
Подход awk sed:
awk -F, '{a[NR-1]="s/####"NR"####/"$2"/";print "s/"$1"/####"NR"####/"}; END{for (i=0;i<NR;i )print a[i];}' replace-list.csv > /tmp/sed_script.sed
sed -f /tmp/sed_script.sed input.txt
Подход cat sed sed:
cat -n replace-list.csv | sed -rn 'H;g;s|(.*)n *([0-9] ) *[^,]*,(.*)|1ns/####2####/3/|;x;s|.*n *([0-9] )[ t]*([^,] ).*|s/2/####1####/|p;${g;s/^n//;p}' > /tmp/sed_script.sed
sed -f /tmp/sed_script.sed input.txt
Механизм:
- Здесь сначала генерируется сценарий sed, использующий csv в качестве входного файла.
- Затем использует другой экземпляр sed для работы input.txt
Примечания:
- Созданный промежуточный файл — sed_script.sed можно использовать повторно, если входной файл csv не изменится.
####<number>####
выбирается как некоторый шаблон, которого нет во входном файле. Измените этот шаблон, если требуется.cat -n |
это не UUOC 🙂
Ответ №8:
Это может сработать для вас (GNU sed):
sed -r 'h;s/./amp;\n/g;H;x;s/([^,]*),.*,(.*)/s|1|2|g/;$s/$/;s|\n||g/' csv_file | sed -rf - original_file
Преобразуйте csv
файл в sed
скрипт. Хитрость здесь заключается в том, чтобы заменить строку подстановки на ту, которая не будет заменена повторно. В этом случае каждый символ в строке подстановки заменяется самим собой и a n
. Наконец, как только все замены выполнены, n
‘ы удаляются, оставляя готовую строку.
Ответ №9:
Здесь уже есть много интересных ответов. Я публикую это, потому что использую несколько иной подход, делая некоторые большие предположения о данных для замены ( на основе выборочных данных ).:
- Слова для замены не содержат пробелов
- Слова заменяются на основе самого длинного, точно совпадающего префикса
- Каждое слово, подлежащее замене, точно представлено в формате csv
Это один проход, awk отвечает только с очень небольшим количеством регулярных выражений.
Он считывает файл «repl.csv» в ассоциативный массив ( см. BEGIN{} ), затем пытается сопоставить префиксы каждого слова, когда длина слова ограничена ограничениями длины ключа, стараясь по возможности избегать поиска в ассоциативном массиве:
#!/bin/awk -f
BEGIN {
while( getline repline < "repl.csv" ) {
split( repline, replarr, "," )
replassocarr[ replarr[1] ] = replarr[2]
# set some bounds on the replace word sizes
if( minKeyLen == 0 || length( replarr[1] ) < minKeyLen )
minKeyLen = length( replarr[1] )
if( maxKeyLen == 0 || length( replarr[1] ) > maxKeyLen )
maxKeyLen = length( replarr[1] )
}
close( "repl.csv" )
}
{
i = 1
while( i <= NF ) { print_word( $i, i == NF ); i }
}
function print_word( w, end ) {
wl = length( w )
for( j = wl; j >= 0 amp;amp; prefix_len_bound( wl, j ); j-- ) {
key = substr( w, 1, j )
wl = length( key )
if( wl >= minKeyLen amp;amp; key in replassocarr ) {
printf( "%s%s%s", replassocarr[ key ],
substr( w, j 1 ), !end ? " " : "n" )
return
}
}
printf( "%s%s", w, !end ? " " : "n" )
}
function prefix_len_bound( len, jlen ) {
return len >= minKeyLen amp;amp; (len <= maxKeyLen || jlen > maxKeylen)
}
На основе ввода, подобного:
I like to eat apples and carrots
orange you glad to see me
Some people eat pears while others drink ink
Это дает результат, подобный:
I like to eat fruit3s and vegetable1s
fruit2 you glad to see me
Some people eat fruit4s while others drink item1
Конечно, любая «экономия», связанная с отсутствием поиска в replassocarr, исчезает, когда слова, подлежащие замене, достигают длины = 1 или если средняя длина слова намного больше, чем слова для замены.
Комментарии:
1. Я заметил, но не редактировал пример, что цикл print_word() действительно должен быть переработан так, чтобы просматривались только substr() ы, которые привязаны к максимальному и минимальному значению ключа. Прямо сейчас, это тратит некоторое время с конца более длинных слов.