Чередование текстовых файлов с заданным соотношением строк из файла1 в файл2 - PullRequest
7 голосов
/ 07 февраля 2020

У меня есть 2 текстовых файла. Одна содержит в 3 раза больше строк, чем другая. Меньший содержит заголовки, которые я хотел бы чередовать со строками большего текстового файла в соотношении 3: 1

например,

маленький файл:

header1
header2
header3

большой файл

lines1.1
lines1.2
lines1.3
lines2.1
lines2.2
lines2.3
lines3.1
lines3.2
lines3.3

становится:

header1
lines1.1
lines1.2
lines1.3
header2
lines2.1
lines2.2
lines2.3
header3
lines3.1
lines3.2
lines3.3

У меня есть решение оболочки для моей проблемы:

new_reads_no="$(wc -l small_file.txt | awk '{print $1}')"
sequence="$(seq 1 $new_reads_no)"

for i in $sequence
do
    start=$((3*($i-1)+1))
    end=$(($start+2))
    awk -v c1=$i 'FNR==c1' small_file.txt >> Output.txt
    awk -v s="$start" -v e="$end" 'NR>=s&&NR<=e' big_file.txt >> Output.txt
done

, которое прекрасно работает. Тем не менее, мой маленький файл составляет 10 миллионов строк. На данный момент, по моим прогнозам, он будет завершен примерно через 1 год.

Любая помощь в ускорении этого процесса будет принята с благодарностью. Либо простая оболочка без одного вкладыша, либо просто быстрый инструмент на другом языке - это было бы здорово.

Ответы [ 7 ]

5 голосов
/ 07 февраля 2020

Старый добрый paste:

paste -d '\n' fsmall - - - <fbig

СИНОПСИС paste [-s] [-d list] file ... file OPERANDS file : путь к входному файлу. Если - указано для одного или нескольких файлов, должен использоваться стандартный ввод; стандартный ввод должен читаться по одной строке за раз, по кругу, для каждого экземпляра -.

источник: паста POSIX

Это означает, что каждый символ читает строку из stdin, которая в данном случае определена как fbig. Три дефиса, означает три строки.

Старый добрый awk без буферизации:

awk -v r=3 '1;{for(i=1;i<=r;++i) {getline < "-"; print}}' fsmall <fbig

Этот метод имитирует идею решения paste. Он использует getline, чтобы избежать буферизации небольшого файла. Это не очень гибко, и всегда следует соблюдать осторожность при использовании getline [См. Все о getline ]

Старый добрый awk с буферизацией:

awk -v r=3 '(NR==FNR){b[FNR]=$0;next}(FNR%r==1){print b[++c]}1' fsmall fbig

Это буферизует маленький файл. Это может привести к проблемам с производительностью, когда маленький файл действительно большой. (См. комментарий от Tripleee)

4 голосов
/ 07 февраля 2020

С GNU sed

sed -e 'R f2' -e 'R f2' -e 'R f2' f1

, где f1 - файл меньшего размера. Команда R читает по одной строке за раз из данного файла. Строки, полученные таким образом, добавляются после текущей строки, прочитанной из f1

2 голосов
/ 07 февраля 2020

Если ваш маленький файл достаточно мал, чтобы поместиться в памяти:

$ awk 'NR==FNR{hdrs[NR]=$0; next} NR%3 == 1{print hdrs[++c]} 1' small big
header1
lines1.1
lines1.2
lines1.3
header2
lines2.1
lines2.2
lines2.3
header3
lines3.1
lines3.2
lines3.3

в противном случае:

$ awk '(NR%3 == 1) && ((getline hdr < "small") > 0){print hdr} 1' big
header1
lines1.1
lines1.2
lines1.3
header2
lines2.1
lines2.2
lines2.3
header3
lines3.1
lines3.2
lines3.3

См. http://awk.freeshell.org/AllAboutGetline, почему я используя синтаксис, который я использую для вызова getline и почему его лучше избегать, если не нужно.

2 голосов
/ 07 февраля 2020

С Bash версия 4 и выше:

while IFS= read -r; do
  # Map 3 lines of big_file.txt without capturing newline character
  mapfile -t -n 3 -u 3

  # Output header $REPLY from small_file.txt
  # followed by ${MAPFILE[@]} mapped lines of big_file.txt
  printf '%s\n' "$REPLY" "${MAPFILE[@]}"
done <small_file.txt 3<big_file.txt

С обратным вызовом для печати строк заголовка:

#!/usr/bin/env bash

while IFS= read -r; do
  mapfile -t -n 3 -u 3 -C'echo "$REPLY";:' -c 3
  printf '%s\n' "${MAPFILE[@]}"
done <small_file.txt 3<big_file.txt

mapfile используется для чтения 3 строк в время из дескриптора файла 3:

  • -t: не захватывать символ новой строки.

  • -n 3: отобразить 3 строки в массив MAPFILE.

  • -u: чтение из дескриптора файла 3, указывающего на big_file.txt.

  • -C'echo "$REPLY";:': Инструкции обратного вызова.

  • -c 3: Квант для вызова обратного вызова (каждые 3 сопоставленных строки).

обратный вызов:

  • echo "$REPLY": выведите переменную $REPLY, содержащую строку, считанную из small_file.txt в while l oop.

  • :: фиктивная команда NOP для использования аргументов, переданных обратному вызову из команды mapfile.

mapfile вызов -back не хватает документации о своих аргументах. Вот они:

  1. Отображен последний индекс массива

  2. Отображено последнее значение массива

2 голосов
/ 07 февраля 2020

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

Вот простой Python скрипт, который делает то, что вы просите, просто сохраняя оба файла открытыми и читая каждый из них, как вы go.

with open('small_file.txt') as small, open('big_file.txt') as large:
    for line in small:
        print(line, end='')
        for x in range(3):
            print(large.readline(), end='')

Если вы хотите параметризовать имена файлов, попробуйте

import sys

with open(sys.argv[1]) as small, open(sys.argv[2]) as large:
    ...

Вывод на стандартный вывод, поэтому, если вы сохранили вышеприведенное в path/to/script.py, вы можете просто запустить его в командной строке:

python3 path/to/script.py small_file.txt big_file.txt >Output.txt

Использование end='' - это небольшой взлом, чтобы избежать необходимости отрывать новую строку и print добавлять его обратно.

В качестве запоздалой мысли вы можете сделать почти то же самое в сценарий оболочки;

while IFS= read -r line; do
    printf '%s\n' "$line"
    for x in 1 2 3; do
        IFS= read -u 3 -r other
        printf '%s\n' "$other"
    done
done <small_file.txt 3<big_file.txt >Output.txt

, но while read -r l oop оболочки по своей сути намного медленнее.

0 голосов
/ 08 февраля 2020

Давайте отредактируем bigfile напрямую через ed(1) plus shell.

Сохраните smallfile в массиве.

mapfile -t array < smallfile

Mapfile - это функция bash4, bash3, которую вы можете сделать.

array=()
while IFS= read -r line; do
   array+=("$line")
done < smallfile

Подсчитайте количество строк в bigfile плюс интервал, в который мы вставим строку smallfiles и сохраним ее в массив с именем count

count=(); for ((i=1;i<=$(wc -l < bigfile); i += 4 )); do count+=("$i"); done

Хотя printf '%s\n' '$=' | ed -s bigfile может подсчитать общее количество количество строк в бигфайле, wc написано для этой цели. $(wc -l < bigfile)

Записать изменения в bigfile, скопировав строки из smallfile.

printf '%s\n' "${count[0]}i" "${array[0]}" . "${count[1]}i" "${array[1]}" . "${count[2]}i" "${array[2]}" . w | ed -s bigfile
0 голосов
/ 07 февраля 2020

Я не спал из-за этого. Эта команда R, на которую указывает Sundeep, действительно классная, но реализация разочаровывает. Поэтому я немного покопался в документах Седа и нашел это first~step

   first~step
          Match  every  step'th  line starting with line first.  For example, ``sed -n 1~2p'' will print all the odd-numbered lines in the input stream,
          and the address 2~5 will match every fifth line, starting with the second.  first can be zero; in this case, sed operates as if it were  equal
          to step.  (This is an extension.)

И я попробовал это

sed '1~3R headers' lines

Но результат оказался не таким, как ожидалось

line1.1
header1
line1.2
line1.3
line2.1
header2
line2.2
line2.3
line3.1
header3
line3.2

Поскольку R добавляет строки, этого можно избежать, добавив дополнительную первую строку к файлу строк

sed -i '1s/^/\n/' lines

Чем мы обрабатываем файлы

sed '1~3R headers' lines > output

И удаляем эта дополнительная строка из вывода

sed -i '1d' output

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

Интересно, достаточно ли grep? Не могли бы вы попробовать?

headers=( $(cat headers) )
for header in ${headers[@]}; {
    echo $header >> output
    digit=${header//[!0-9]/}
    grep .*$digit. lines >> output
}

Хорошо, какой метод быстрее? Я сделал тестовые файлы с этими

for i in {1..100}; { echo "header$i" >> headers; }
for i in {1..100}; { for e in {1..3}; { echo "line$i.$e" >> lines;}; }

100 и 300 строками и протестировал все методы

paste -d '\n' headers - - - <lines

real    0m0,003s
user    0m0,000s
sys     0m0,003s

sed -e 'R lines' -e 'R lines' -e 'R lines' headers

real    0m0,003s
user    0m0,000s
sys     0m0,003s

awk -v r=3 '1;{for(i=1;i<=r;++i) {getline < "-"; print}}' headers <lines
awk -v r=3 '(NR==FNR){b[FNR]=$0;next}(FNR%r==1){print b[++c]}1' headers lines

real    0m0,005s
user    0m0,000s
sys     0m0,005s

Это явные победители. И это тоже быстро, но неверно.

$ time awk 'NR==FNR{hdrs[NR]=$0; next} NR%3 == 1{print hdrs[++c]} 1' headers lines | head -n7
line1.1
line1.2
header1
line1.3
line2.1
line2.2
header2

real    0m0,004s
user    0m0,002s
sys     0m0,003s

Хорошо, это то, что используют TS.

fun1 () {
    new_reads_no="$(wc -l headers | awk '{print $1}')"
    sequence="$(seq 1 $new_reads_no)"

    for i in $sequence
    do
        start=$((3*($i-1)+1))
        end=$(($start+2))
        awk -v c1=$i 'FNR==c1' headers
        awk -v s="$start" -v e="$end" 'NR>=s&&NR<=e' lines
    done
}

real    0m0,341s
user    0m0,219s
sys     0m0,132s

Ну, это совсем не быстро)

fun2 () {
    headers=( $(cat headers) )
    for header in ${headers[@]}; {
        echo $header
        digit=${header//[!0-9]/}
        grep .*$digit. lines
    }
}

real    0m0,167s
user    0m0,105s
sys     0m0,068s

Мой тоже не работает, но все же он быстрее 1-го) Так что я получил то, что действительно нужно здесь, и сделал это.

fun3 () {
    exec 3< headers
    exec 4< lines

    while read -u3 head do;
        echo $head
        for i in {1..3}; {
            read -u4 line; echo $line
        }
    done

    exec 3<&-
    exec 4<&-
}

real    0m0,009s
user    0m0,009s
sys     0m0,000s

И это довольно быстро)

import sys

with open(sys.argv[1]) as small, open(sys.argv[2]) as large:
    for line in small:
        print(line, end='')
        for x in range(3):
            print(large.readline(), end='')


real    0m0,021s
user    0m0,016s
sys     0m0,004s

Python тоже неплохо. И эти два тоже хорошо.

fun4 () {
    while IFS= read -r; do
      # Map 3 lines of big_file.txt without capturing newline character
      mapfile -t -n 3 -u 3

      # Output header $REPLY from small_file.txt
      # followed by ${MAPFILE[@]} mapped lines of big_file.txt
      printf '%s\n%s' "$REPLY" "${MAPFILE[@]}"
    done <lines 3<headers
}

real    0m0,017s
user    0m0,012s
sys     0m0,004s

fun5 () {
    while IFS= read -r; do
      mapfile -t -n 3 -u 3 -C'echo "$REPLY";:' -c 3
      printf '%s\n' "${MAPFILE[@]}"
    done <headers 3<lines
}

real    0m0,011s
user    0m0,011s
sys     0m0,000s
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...