Замініть рядок у величезному (70 Гб), одному рядку, текстовому файлі


126

У мене величезна (70 Гб), один рядок , текстовий файл, і я хочу замінити рядок (маркер) в ньому. Я хочу замінити маркер <unk>на інший фіктивний маркер ( випуск рукавичок ).

Я спробував sed:

sed 's/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new

але вихідний файл corpus.txt.newмає нульові байти!

Я також спробував використовувати perl:

perl -pe 's/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new

але у мене виникла помилка пам'яті.

Для менших файлів працюють обидві вищевказані команди.

Як я можу замінити рядок такого файлу? Це пов'язане питання, але жодна з відповідей не працювала для мене.

Редагувати : Що з розділенням файлів на шматки по 10 ГБ (або що завгодно) і нанесення sedна кожен з них, а потім їх об'єднання cat? Чи має це сенс? Чи є більш елегантне рішення?


як зауважив @Gilles, чи можете ви виявити деякий повторний символ, який міг би послужити спеціальним роздільником у вашій великій лінійці?
RomanPerekhrest

Я думаю, що інструмент, який може робити лише пошук та заміну, але не будь-який складніший вираз, буде швидшим. Також не було б користі робити рядки за один раз, тому не задихатиметься цього файлу. На жаль, я не маю уявлення про існування такого інструменту, хоча це було б важко написати. Якщо це одноразово, то заміна символів нового рядка, як в одній з відповідей, мабуть, буде найпростішим.
ctrl-alt-delor

Чи містить ваш файл що-небудь, крім ASCII? Якщо так, то всі обробці unicode можуть бути опущені, а необроблені байти - оброблені.
Патрік Бючер

Я згоден з @PatrickButcher Подивіться на більшу картину. Окрім негайної необхідності заміни цього тексту, для чого ще слід використовувати цей файл? Якщо це якийсь журнал, ніхто не зможе ефективно з ним працювати. Якщо це файл даних, який використовується деяким додатком, то ця програма повинна нести відповідальність за збереження даних у цьому файлі.
Томас Карлайл

2
Ви можете використовувати splitз -bваріантом визначення розмірів шматок файлу в байтах. Обробляйте кожен по черзі, використовуючи sedта повторно збирайте. Є ризик, що <unk>його можна розділити на два файли і не знайти ...
Владислав Довгалеч

Відповіді:


106

Звичайні інструменти для обробки тексту не розроблені для обробки ліній, які не вміщуються в ОЗУ. Вони прагнуть працювати, читаючи один запис (один рядок), маніпулюючи ним та видаючи результат, а потім переходять до наступного запису (рядка).

Якщо у файлі є символ ASCII, який часто з’являється і не відображається в, <unk>або <raw_unk>ви можете використовувати його як роздільник записів. Оскільки більшість інструментів не дозволяють користувальницьким роздільникам записів, переключайтесь між цим символом та новими рядками. trобробляє байти, а не рядки, тому це не хвилює будь-який розмір запису. Припустимо, що це ;працює:

<corpus.txt tr '\n;' ';\n' |
sed 's/<unk>/<raw_unk>/g' |
tr '\n;' ';\n' >corpus.txt.new

Ви також можете закріпити перший символ тексту, який ви шукаєте, припускаючи, що він не повторюється в тексті пошуку, і він з’являється досить часто. Якщо файл може починатися з unk>, змініть команду sed, sed '2,$ s/…щоб уникнути помилкового збігу.

<corpus.txt tr '\n<' '<\n' |
sed 's/^unk>/raw_unk>/g' |
tr '\n<' '<\n' >corpus.txt.new

Як варіант, використовувати останній символ.

<corpus.txt tr '\n>' '>\n' |
sed 's/<unk$/<raw_unk/g' |
tr '\n>' '>\n' >corpus.txt.new

Зауважте, що ця методика передбачає, що sed працює безперебійно у файлі, який не закінчується новим рядком, тобто обробляє останній частковий рядок, не обрізаючи його та не додаючи остаточний новий рядок. Він працює з GNU sed. Якщо ви можете вибрати останній символ файлу як роздільник записів, ви уникнете будь-яких проблем з переносом.


8
У мене немає такого файлу для тестування, але в Awk ви можете вказати "Сепаратор запису" та "Роздільник записів виводу". Тож припускаючи, що у вашому файлі є пристойне запалювання коми, можливо, ви могли вирішити це за допомогою: awk -v RS=, -v ORS=, '{gsub(/<unk>/, "<raw_unk>"); print}' Ні?
Wildcard

4
@Wildcard Так, це інше рішення. Хоча Awk проходить повільніше, ніж sed, тому я не пропоную це як найкраще рішення для величезного файлу.
Жиль

Ви можете встановити роздільник записів у Perl за допомогою параметра командного рядка -0та вісімкового значення char, або всередині сценарію його можна встановити за допомогою спеціальної змінної$/
beasy

@Gilles: Але використовуючи awkуникати передачі потоку двічі до tr. Так би все-таки повільніше?
користувач285259

2
@ user285259 Як правило, ні. trдуже швидко, і труба навіть може бути паралельна.
Жиль

110

Для такого великого файлу одна з можливостей - Flex. Нехай unk.lбуде:

%%
\<unk\>     printf("<raw_unk>");  
%%

Потім компілюйте та виконайте:

$ flex -o unk.c  unk.l
$ cc -o unk -O2 unk.c -lfl
$ unk < corpus.txt > corpus.txt.new

5
makeдля цього є правила за замовчуванням, замість flex / cc ви можете додати %option mainяк перший рядок unk.l, а потім просто make unk. Я більш-менш рефлекторно використовую %option main 8bit fastі маю export CFLAGS='-march=native -pipe -Os'в своєму .bashrc.
jthill

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

@jthill, дякую: %option main+ make+ необов'язково CFLAGS- це дуже приємний трюк !! Чи -march=nativeповедінка за замовчуванням?
JJoao

1
@jamesqf, як ви сказали, - важко буде це зробити на тему питання - але я хотів би це також побачити
Стівен Пенні

1
@jamesqf Професор мого університету використовував flex для створення інструменту, який розпізнавав типи тканин для фабрики! Як щодо запитання чогось типу: "flex здається дуже потужним інструментом, але я навряд чи пишу будь-які компілятори / аналізатори - чи є інші випадки використання для flex?"
Пол Еванс

41

Таким чином, у вас недостатньо фізичної пам'яті (ОЗУ), щоб вмістити весь файл одразу, але у 64-бітній системі у вас достатньо віртуального адресного простору для картування всього файлу. Віртуальні відображення можуть бути корисні як простий злом у таких випадках.

Усі необхідні операції включені в Python. Є кілька прикрих тонкощів, але це уникає необхідності писати код C. Зокрема, потрібно дбати про те, щоб не копіювати файл у пам'ять, який би повністю перемог точки. З іншого боку, ви отримуєте повідомлення про помилки безкоштовно (python "винятки") :).

#!/usr/bin/python3
# This script takes input from stdin
# (but it must be a regular file, to support mapping it),
# and writes the result to stdout.

search = b'<unk>'
replace = b'<raw_unk>'


import sys
import os
import mmap

# sys.stdout requires str, but we want to write bytes
out_bytes = sys.stdout.buffer

mem = mmap.mmap(sys.stdin.fileno(), 0, access=mmap.ACCESS_READ)
i = mem.find(search)
if i < 0:
    sys.exit("Search string not found")

# mmap object subscripts to bytes (making a copy)
# memoryview object subscripts to a memoryview object
# (it implements the buffer protocol).
view = memoryview(mem)

out_bytes.write(view[:i])
out_bytes.write(replace)
out_bytes.write(view[i+len(search):])

Якщо в моїй системі є близько 4 Гб послідовної вільної пам'яті з 8 ГБ, чи означає mem = mmap.mmap (sys.stdin.fileno (), 0, access = mmap.ACCESS_READ), що вона розміщує дані в цьому просторі? Або було б набагато нижче (1 Гб?)>
Рахул

1
@Rahul "Отже, вам не вистачає оперативної пам'яті, але у 64-бітній системі у вас достатньо віртуального адресного простору для відображення всього файлу". Це запрошення в режим фізичного тарана на вимогу (або її відсутність). Ця програма повинна працювати, не вимагаючи великої кількості фізичної оперативної пам’яті. 64-бітні системи мають набагато більше віртуального адресного простору, ніж максимальний фізичний таран. Також у кожного запущеного процесу є власний віртуальний адресний простір. Це означає, що система в цілому не вистачає віртуального адресного простору - це не річ, це не допустима концепція.
sourcejedi

4
@Rahul так! python mmap.mmap () - досить тонка обгортка навколо функції C mmap (). І mmap () - той самий механізм, який використовується для запуску виконуваних файлів та коду із спільних бібліотек.
sourcejedi

2
@jamesqf Я можу помилитися, але я вважаю, що це лише особистий вибір. Оскільки втрати продуктивності були б незначними (оскільки, за його словами, фактична функція викликає функцію c), витрата накладних витрат дуже низька, оскільки між ними не відбувається жоден інший матеріал. C було б краще, але це рішення не ставило за мету оптимізацію, а лише вирішити більшу та складну проблему 70 Гбіт.
Рахул

1
Взагалі, написання пітона є більш компактним. У цьому випадку виявилося, що у версії python є кілька деталей, і версію C, можливо, було б приємніше писати. (Хоча це не так просто, якщо searchможе містити символ NUL. І я помічаю, що інша версія C тут не підтримує символи NUL replace.). Ви можете отримати версію C для порівняння. Однак пам’ятайте, що моя версія включає базові повідомлення про помилки для операцій, які вона виконує. Версія C, принаймні, буде більше прикро читати IMO, коли буде включено повідомлення про помилки.
sourcejedi

17

Я думаю, що версія C може працювати набагато краще:

#include <stdio.h>
#include <string.h>

#define PAT_LEN 5

int main()
{
    /* note this is not a general solution. In particular the pattern
     * must not have a repeated sequence at the start, so <unk> is fine
     * but aardvark is not, because it starts with "a" repeated, and ababc
     * is not because it starts with "ab" repeated. */
    char pattern[] = "<unk>";          /* set PAT_LEN to length of this */
    char replacement[] = "<raw_unk>"; 
    int c;
    int i, j;

    for (i = 0; (c = getchar()) != EOF;) {
        if (c == pattern[i]) {
            i++;
            if (i == PAT_LEN) {
                printf("%s", replacement);
                i = 0;
            }
        } else {
            if (i > 0) {
                for (j = 0; j < i; j++) {
                    putchar(pattern[j]);
                }
                i = 0;
            }
            if (c == pattern[0]) {
                i = 1;
            } else {
                putchar(c);
            }
        }
    }
    /* TODO: fix up end of file if it ends with a part of pattern */
    return 0;
}

EDIT: Змінено відповідно до пропозицій із коментарів. Також виправлена ​​помилка з малюнком <<unk>.


2
ви можете надрукувати (візерунок [j]) замість (buf [j]) (вони рівні на даний момент, тому вам не потрібен буфер
RiaD

3
також код не буде працювати для рядка "<" ideone.com/ncM2yy
RiaD

10
30 Мб за 0,3 секунди? Це всього 90 Мб / секунду. memcpyшвидкість (тобто вузьке місце пам’яті) - це щось на зразок 12 Гб / секунду в недавньому процесорі x86 (наприклад, Skylake). Навіть із накладними витратами на системний дзвінок stdio +, для 30-мегабайтного файлу, гарячого в кеш-диску, я б очікував, що, можливо, 1 Гб / секунду для ефективної реалізації. У вас компіляція з відключеною оптимізацією чи введення / виведення одночасної роботи в цей час справді повільне? getchar_unlocked/ putchar_unlockedможе допомогти, але, безумовно, краще читати / писати шматками, можливо, 128 КБ (половина розміру кешу L2 на більшості процесорів x86, тому ви здебільшого потрапляєте в L2 під час циклу після читання)
Пітер Кордес

2
від верхньої частини моєї голови, GetChar і putchar є повільним.
Rui F Ribeiro

3
Програма fixдо цих "<<unk>"пір не працює, якщо patternзапускається з повторної послідовності символів (тобто не буде працювати, якщо ви намагалися замінити aardvark на zebra і у вас був вхід aaardvak, або ви намагалися замінити ababc і мав введення abababc). Як правило, ви не можете рухатися вперед за кількістю прочитаних символів, якщо не знаєте, що у прочитаних символів немає можливості починати відповідність.
ікар

16

У replaceпакунку mariadb-server / mysql-server є утиліта. Він замінює прості рядки (не регулярні вирази) і на відміну від grep / sed / awk replaceне хвилює \nі \0. Споживання пам'яті постійне для будь-якого вхідного файлу (близько 400 кбіт на моїй машині).

Звичайно, вам не потрібно запускати сервер mysql для того, щоб використовувати replace, він лише таким чином упакований у Fedora. Інші дистрибутиви / операційні системи можуть упакувати їх окремо.


14

GNU grepможе показати вам компенсацію збігів у "двійкових" файлах, не змушуючи читати цілі рядки в пам'яті. Потім можна скористатися ddдля читання цього зміщення, пропустити відповідність, а потім продовжити копіювання з файлу.

file=...
newfile=...
replace='<raw_unk>'
grep -o -b -a -F '<unk>' <"$file" |
(   pos=0
    while IFS=$IFS: read offset pattern
    do size=${#pattern}
       let skip=offset-pos
       let big=skip/1048576
       let skip=skip-big*1048576
       dd bs=1048576 count=$big <&3
       dd bs=1 count=$skip <&3
       dd bs=1 count=$size of=/dev/null <&3
       printf "%s" "$replace"
       let pos=offset+size
    done
    cat <&3
) 3<"$file" >"$newfile"

Для швидкості я розділив ddна велике зчитування блочного розміру 1048576 та менший зчитування в 1 байт за один раз, але ця операція все ще буде трохи повільною на такому великому файлі. Наприклад, grepрезультат є, 13977:<unk>і це розділяється на двокрапку зчитуванням на змінні offsetта pattern. Ми повинні відслідковувати, posскільки байтів вже скопійовано з файлу.


11

Ось ще один єдиний командний рядок UNIX, який може працювати краще, ніж інші параметри, тому що ви можете «полювати» на «розмір блоку», який працює добре. Щоб це було надійно, ви повинні знати, що у вас є принаймні один пробіл у кожному X символі, де X - ваш довільний "розмір блоку". У наведеному нижче прикладі я вибрав "розмір блоку" з 1024 символів.

fold -w 1024 -s corpus.txt | sed 's/<unk>/<raw_unk>/g' | tr '/n' '/0'

Тут, fold захопить до 1024 байтів, але -s гарантує, що він перерветься на пробіл, якщо з останнього перерви залишився принаймні один.

Команда sed - це ваше і робить те, що ви очікуєте.

Тоді команда tr "розгорне" файл, перетворюючи нові рядки, які були вставлені назад ні до чого.

Спробуйте спробувати більші розміри блоків, щоб побачити, чи працює вона швидше. Замість 1024 ви можете спробувати 10240 та 102400 та 1048576 для опції -w.

Ось приклад, розбитий на кожен крок, який перетворює всі N у малі регістри:

[root@alpha ~]# cat mailtest.txt
test XJS C4JD QADN1 NSBN3 2IDNEN GTUBE STANDARD ANTI UBE-TEST EMAIL*C.34X test

[root@alpha ~]# fold -w 20 -s mailtest.txt
test XJS C4JD QADN1
NSBN3 2IDNEN GTUBE
STANDARD ANTI
UBE-TEST
EMAIL*C.34X test

[root@alpha ~]# fold -w 20 -s mailtest.txt | sed 's/N/n/g'
test XJS C4JD QADn1
nSBn3 2IDnEn GTUBE
STAnDARD AnTI
UBE-TEST
EMAIL*C.34X test

[root@alpha ~]# fold -w 20 -s mailtest.txt | sed 's/N/n/g' | tr '\n' '\0'
test XJS C4JD QADn1 nSBn3 2IDnEn GTUBE STAnDARD AnTI UBE-TEST EMAIL*C.34X test

Вам потрібно буде додати новий рядок до самого кінця файлу, якщо він має такий, оскільки команда tr видалить його.


1
Як ви переконаєтесь, що ви не порушуєте схему в крайніх випадках, коли не вистачає пробілів?
rackandboneman

1
Як було сказано, для того, щоб це було надійно, існує потреба, щоб на кожному X символі було принаймні один пробіл. Ви можете зробити цей аналіз досить просто, з будь-яким обраним вами блоком: fold -w X mailtest.txt | grep -v "" | wc -l Число, яке воно повертає, - це кількість складених ліній з потенційними крайовими корпусами. Якщо він дорівнює нулю, рішення гарантовано спрацює.
alfreema

10

Використання perl

Керування власними буферами

Ви можете використовувати IO::Handle's setvbufдля управління буферами за замовчуванням, або ви можете керувати власними буферами за допомогою sysreadта syswrite. Перевірте, perldoc -f sysreadі perldoc -f syswriteдля отримання додаткової інформації вони по суті пропускають буферований io.

Тут ми прокатуємо власний IO буфера, але це робимо вручну та довільно на 1024 байтах. Ми також відкриваємо файл для RW, тому ми робимо все це на тому самому FH відразу.

use strict;
use warnings;
use Fcntl qw(:flock O_RDWR);
use autodie;
use bytes;

use constant CHUNK_SIZE => 1024 * 32;

sysopen my $fh, 'file', O_RDWR;
flock($fh, LOCK_EX);

my $chunk = 1;
while ( sysread $fh, my $bytes, CHUNK_SIZE * $chunk ) {
  if ( $bytes =~ s/<unk>/<raw_unk>/g ) {
    seek( $fh, ($chunk-1)* CHUNK_SIZE, 0 );
    syswrite( $fh, $bytes, 1024);
    seek( $fh, $chunk * CHUNK_SIZE, 0 );
  }
  $chunk++;
}

Якщо ви збираєтеся йти цим маршрутом

  1. Переконайтеся в тому , <unk>і <raw_unk>мають той же розмір байт.
  2. Ви можете переконатися, що наш буферний метод не переходить CHUNKSIZEмежу, якщо ви замінюєте більше 1 байта.

2
Що робити, якщо <unk>потрапляє на межу між шматками?
liori

8

Ви можете спробувати bbe ( редактор двійкового блоку ), " sedдля бінарних файлів".

Я мав добрий успіх, використовуючи його в текстовому файлі об'ємом 7 ГБ без EOLсимволів, замінивши кілька входжень рядка на один з різною довжиною. Не намагаючись оптимізувати, він давав середню пропускну здатність> 50 Мб / с.


5

З perl, ви можете працювати з записами фіксованої довжини, як:

perl -pe 'BEGIN{$/=\1e8}
          s/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new

І сподіваємось, <unk>що між двома цими записами на 100 МБ не буде місця .


Я також думав про цей метод, але використовував while read -N 1000 chunk;( 1000обраний як приклад). Рішення для <unk>, розбитого між шматками, - це два проходи через файл: перший із шматками 100MB і другий з відрізками "100MB + 5 байт". Але це не оптимальне рішення у випадку з файлом 70 Гб.
MiniMax

3
Вам навіть не потрібно два проходи. Блок читання A. Хоча це не EOF, прочитайте блок B. Пошук / Заміна в A + B. A: = B. Петля. Складність полягає в тому, що ви не заміните всередині замінника.
roaima

@MiniMax, цей другий прохід не обов'язково допоможе, оскільки перший пропуск додав би 5 байт за кожне виникнення <unk>.
Стефан Шазелас

1
@roaima, так, це було б набагато більше залученим рішенням. Ось простий підхід, який є дуже імовірним (якщо припустити, що <unk>випадки є набагато частіше, якщо ні, то використовуйте $/ = ">"і s/<unk>\z/<raw_unk>/g) є правильним.
Стефан Шазелас

5

Ось невеличка програма Go, яка виконує завдання ( unk.go):

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
)

func main() {
    const (
        pattern     = "<unk>"
        replacement = "<raw_unk>"
    )
    var match int
    var char rune
    scanner := bufio.NewScanner(os.Stdin)
    scanner.Split(bufio.ScanRunes)
    for scanner.Scan() {
        char = rune(scanner.Text()[0])
        if char == []rune(pattern)[match] {
            match++
            if match == len(pattern) {
                fmt.Print(replacement)
                match = 0
            }
        } else {
            if match > 0 {
                fmt.Print(string(pattern[:match]))
                match = 0
            }
            if char == rune(pattern[0]) {
                match = 1
            } else {
                fmt.Print(string(char))
            }
        }
    }
    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }
}

Просто побудуйте його go build unk.goта запустіть як ./unk <input >output.

Редагувати:

Вибачте, я не прочитав, що все в одному рядку, тому я спробував прочитати символ файлу за символом зараз.

Редагування II:

Застосовується те саме виправлення, що і для програми C.


1
це уникає читання всього файлу в пам'яті?
кіт

1
Він зчитує символ файлу за символом і ніколи не зберігає в пам'яті весь файл, а лише окремі символи.
Патрік Бючер

1
scanner.Split(bufio.ScanRunes)чинить магію.
Патрік Бючер

Перевірте go doc bufio.MaxScanTokenSizeтакож розмір буфера за замовчуванням.
Патрік Бючер

Як і у вашій Cпрограмі, це не працює для заміни aardvark на zebra на введення aaardvark.
ікар

1

Це може бути зайвим для файлу на 70 ГБ і простого пошуку та заміни, але рамка Hadoop MapReduce вирішить вашу проблему прямо зараз (виберіть опцію "Один вузол" при налаштуванні для запуску локально) - і це може бути масштабуватися до нескінченної спроможності в майбутньому без необхідності змінювати код.

Офіційний підручник за адресою https://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html використовує (надзвичайно просту) Java, але ви можете знайти клієнтські бібліотеки для Perl або якою б мовою ви не хотіли користуватися.

Тож якщо пізніше ви виявите, що ви робите складніші операції над текстовими файлами 7000 ГБ - і це потрібно робити 100 разів на день - ви можете розподілити навантаження на декілька вузлів, які ви надаєте, або які автоматично передбачені для вас хмарним посиланням, заснований кластер Hadoop


1
так, так, це так. "Не використовуйте Hadoop - ваші дані не такі великі" . Це дуже проста проблема потокового вводу-виводу.
sourcejedi

0

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

1) Якщо я правильно зрозумів ваш конкретний випадок, чи допустимо було б замінити на якусь іншу рядок довжини SAME?

2а) Чи є кілька випадків? 2б) Якщо так, то чи знаєте ви, скільки?

Я впевнений, що ви вже вирішили цю проблему плюс рік, і я хотів би знати, яке рішення ви використовували.

Я б запропонував рішення (швидше за все, на C), яке б читало БЛОКИ файлу, що шукає кожну рядок з урахуванням можливого перетину блоку. Щойно знайдено, замініть рядок на чергування однакової довжини та запишіть лише той BLOCK. Продовження відомої кількості подій або до кінця файлу. Для цього знадобиться не менше запису числа, а щонайбільше вдвічі (якщо кожне явище було розділене на два блоки). Це не потребує додаткового місця!


-1

Якщо ми маємо мінімальну суму <unk>(як цього передбачає закон Зіпфа),

awk -v RS="<unk>" -v ORS="<raw_unk>" 1

1
Ні. sedЧитає за часом рядки в пам'ять незалежно. Він не зможе відповідати цій лінії.
Кусалаланда

1
Я не можу знайти жодної документації, яка б не сказала нічого, крім того, що GNU sedне буде виконувати буферизацію вводу / виводу при використанні цього прапора. Я не бачу, що він буде читати часткові рядки.
Кусалаланда
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.