Cum pot elimina prima linie a unui fișier text folosind scriptul bash/sed? (Programare, Bash, Scripting, Sed)

Brent a intrebat.

Am nevoie să elimin în mod repetat prima linie dintr-un fișier text imens folosind un script bash.

În acest moment folosesc sed -i -e "1d" $FILE – dar durează aproximativ un minut pentru a face ștergerea.

Există o modalitate mai eficientă de a realiza acest lucru?

Comentarii

  • ce înseamnă -i? –  > Por cikatomo.
  • @cikatomo: înseamnă inline edit – editează fișierul cu ceea ce ai generat. –  > Por drewrockshard.
  • tail este MULT MAI LENT decât sed. tail are nevoie de 13,5s, sed are nevoie de 0,85s. Fișierul meu are ~1M de linii, ~100MB. MacBook Air 2013 cu SSD. –  > Por jcsahnwaldt Reinstaurați-o pe Monica.
16 răspunsuri
Aaron Digulla

Încercați coadă:

tail -n +2 "$FILE"

-n x: Doar imprimați ultima x linii. tail -n 5 ar da ultimele 5 linii de intrare. + inversează oarecum argumentul și face ca tail să tipărească orice altceva în afară de primele linii x-1 linii. tail -n +1 ar imprima întregul fișier, tail -n +2 totul în afară de prima linie, etc.

GNU tail este mult mai rapid decât sed. tail este disponibil și pe BSD și pe -n +2 este consecvent în ambele instrumente. Verificați FreeBSD sau OS X pentru mai multe informații.

Versiunea BSD poate fi mult mai lentă decât sed, , totuși. Mă întreb cum au reușit să facă asta; tail ar trebui doar să citească un fișier linie cu linie, în timp ce sed face operații destul de complexe care implică interpretarea unui script, aplicarea expresiilor regulate și altele asemenea.

Notă: S-ar putea să fiți tentat să folosiți

# THIS WILL GIVE YOU AN EMPTY FILE!
tail -n +2 "$FILE" > "$FILE"

dar acest lucru vă va da un fișier gol. Motivul este că redirecționarea (>) are loc înainte de tail să fie invocată de către shell:

  1. Shell trunchiază fișierul $FILE
  2. Shell creează un nou proces pentru tail
  3. Shell redirecționează stdout al tail procesului către $FILE
  4. tail citește din fișierul acum gol $FILE

Dacă doriți să eliminați prima linie din interiorul fișierului, ar trebui să utilizați:

tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"

&& se va asigura că fișierul nu va fi suprascris atunci când există o problemă.

Comentarii

  • În conformitate cu acest lucru ss64.com/bash/tail.html buffer-ul tipic este implicit de 32k atunci când se utilizează BSD ‘tail’ cu ajutorul comenzii -r opțiune. Poate că există o setare a buffer-ului undeva în sistem? Sau -n este un număr semnat pe 32 de biți? –  > Por Yzmir Ramirez.
  • 42

  • @Eddie: user869097 a spus că nu funcționează atunci când un singur linie este de 15Mb sau mai mult. Atâta timp cât liniile sunt mai scurte, tail va funcționa pentru orice dimensiune de fișier. –  > Por Aaron Digulla.
  • ați putea explica aceste argumente ? –  > Por Dreampuf.
  • 18

  • @Dreampuf – din pagina de manual: -n N means output the last N lines, instead of the last 10; or use +N to output lines starting with the Nth –  > Por Will Sheppard.
  • Aveam de gând să fiu de acord cu @JonaChristopherSahnwaldt – tail este mult, mult mai lent decât varianta sed, cu un ordin de mărime. Îl testez pe un fișier de 500.000K linii (nu mai mult de 50 de caractere pe linie). Cu toate acestea, mi-am dat seama apoi că foloseam versiunea FreeBSD a tail (care vine implicit cu OS X). Când am trecut la GNU tail, apelul tail a fost de 10 ori mai rapid decât apelul sed (și apelul GNU sed, de asemenea). AaronDigulla are dreptate aici, dacă folosiți GNU. –  > Por dancow.
amit

Puteți utiliza -i pentru a actualiza fișierul fără a utiliza operatorul ‘>’. Următoarea comandă va șterge prima linie din fișier și o va salva în fișier.

sed -i '1d' filename

Comentarii

  • Primesc o eroare: unterminated transform source string –  > Por Daniel Kobe.
  • acest lucru funcționează de fiecare dată și ar trebui să fie într-adevăr răspunsul de top! –  > Por xtheking.
  • Doar pentru a vă aminti, Mac necesită furnizarea unui sufix atunci când se utilizează sed cu ediții in-place. Deci, rulați cele de mai sus cu -i.bak –  > Por mjp.
  • Doar o notă – pentru a elimina mai multe linii, utilizați sed -i '1,2d' filename –  > Por Nașul.
  • Această versiune este într-adevăr mult mai ușor de citit și mai universală decât tail -n +2. Nu sunt sigur de ce nu este răspunsul de top. –  > Por Luke Davis.
Nasri Najib

Pentru cei care sunt pe SunOS, care nu este GNU, următorul cod vă va ajuta:

sed '1d' test.dat > tmp.dat 

Comentarii

    29

  • Date demografice interesante –  > Por căpitan.
Ingo Baab

Puteți face acest lucru cu ușurință cu:

cat filename | sed 1d > filename_without_first_line

pe linia de comandă; sau pentru a elimina permanent primul rând dintr-un fișier, utilizați modul in-place al sed cu ajutorul comenzii -i flag:

sed -i 1d <filename>

Comentarii

  • -i primește, din punct de vedere tehnic, un argument care specifică sufixul de fișier care trebuie utilizat atunci când se face o copie de rezervă a fișierului (de exemplu sed -I .bak 1d filename creează o copie numită filename.bak a fișierului original cu prima linie intactă). În timp ce GNU sed vă permite să specificați -i fără un argument pentru a sări peste copia de siguranță, BSD sed, așa cum se găsește pe macOS, necesită un argument de tip șir gol ca un cuvânt shell separat (de ex. sed -i '' ...). –  > Por Mark Reed.
paxdiablo

Nu, asta este cât se poate de eficient. Ai putea scrie un program C care ar putea face treaba puțin mai repede (mai puțin timp de pornire și de procesare a argumentelor), dar probabil că va tinde spre aceeași viteză ca și sed pe măsură ce fișierele devin mari (și presupun că sunt mari dacă durează un minut).

Dar întrebarea dvs. suferă de aceeași problemă ca multe altele, în sensul că pre-supune soluția. Dacă ar fi să ne spuneți în detaliu ce încercați să faceți, mai degrabă decât să cum, am putea fi în măsură să vă sugerăm o opțiune mai bună.

De exemplu, dacă este vorba de un fișier A pe care îl procesează un alt program B, o soluție ar fi să nu se elimine prima linie, ci să se modifice programul B pentru a-l procesa diferit.

Să spunem că toate programele dvs. adaugă la acest fișier A, iar programul B citește și procesează în prezent prima linie înainte de a o șterge.

Ați putea reproiecta programul B astfel încât să nu încerce să șteargă prima linie, ci să mențină un decalaj persistent (probabil bazat pe fișier) în fișierul A, astfel încât, data viitoare când se execută, să poată căuta acel decalaj, să proceseze linia acolo și să actualizeze decalajul.

Apoi, la o oră liniștită (miezul nopții?), ar putea efectua o procesare specială a fișierului A pentru a șterge toate liniile procesate în prezent și pentru a seta decalajul înapoi la 0.

Cu siguranță va fi mai rapid pentru un program să deschidă și să caute un fișier decât să deschidă și să rescrie. Această discuție presupune că aveți control asupra programului B, bineînțeles. Nu știu dacă acesta este cazul, dar ar putea exista și alte soluții posibile dacă furnizați informații suplimentare.

Comentarii

  • Cred că OP încearcă să realizeze ceea ce m-a făcut să găsesc această întrebare. Am 10 fișiere CSV cu 500k linii în fiecare. Fiecare fișier are același rând de antet ca și prima linie. Sunt cat:ing aceste fișiere într-un singur fișier și apoi le import într-un DB lăsând DB-ul să creeze nume de coloane de la prima linie. Evident, nu vreau ca acea linie să se repete în fișierul 2-10. –  > Por d-b.
  • @d-b În acest caz, awk FNR-1 *.csv este probabil mai rapid. –  > Por jinawee.
Robert Gamble

Așa cum a spus Pax, probabil că nu veți obține mai rapid decât atât. Motivul este că nu există aproape niciun sistem de fișiere care să suporte trunchierea de la începutul fișierului, așa că acest lucru va fi un O(n) în care n este dimensiunea fișierului. Ce puteți face mult mai rapid este să suprascrieți prima linie cu același număr de octeți (poate cu spații sau cu un comentariu), ceea ce ar putea funcționa în funcție de ceea ce încercați să faceți (apropo, ce este asta?).

Comentarii

  • Re „…aproape niciun sistem de fișiere care acceptă trunchierea…”: este interesant; vă rugăm să luați în considerare includerea unei note între paranteze care să numească un astfel de sistem de fișiere. –  > Por agc.
  • @agc: irelevant acum, dar primul meu loc de muncă în anii ’70 a fost la Quadex, un mic start-up (acum dispărut și fără legătură cu cele două companii care folosesc acum acest nume). Aveau un sistem de fișiere care permitea adăugarea de sau eliminarea la începutul sau la sfârșitul unui fișier, folosit mai ales pentru a implementa editarea în mai puțin de 3KB prin punerea deasupra ferestrei și sub fereastră în fișiere. Nu avea un nume propriu, era doar o parte din QMOS, Quadex Multiuser Operating System. („Multi” era de obicei 2-3 pe un LSI-11/02 cu mai puțin de 64KB RAM și de obicei câteva dischete de 8″ de tip RX01 de 250KB fiecare) 🙂 –  > Por dave_thompson_085.
alexis

Tu poate editați fișierele pe loc: Folosiți doar funcția perl -i de perl, astfel:

perl -ni -e 'print unless $. == 1' filename.txt

Acest lucru face ca prima linie să dispară, așa cum ați cerut. Perl va trebui să citească și să copieze întregul fișier, dar face în așa fel încât rezultatul să fie salvat sub numele fișierului original.

agc

Adresa sponge util evită necesitatea de a jongla cu un fișier temporar:

tail -n +2 "$FILE" | sponge "$FILE"

Comentarii

  • sponge este într-adevăr mult mai curat și mai robust decât soluția acceptată (tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE") –  > Por Jealie.
  • Ar trebui să fie clar că „sponge” necesită instalarea pachetului „moreutils”. –  > Por FedFranz.
  • Aceasta este singura soluție care a funcționat pentru mine pentru a schimba un fișier de sistem (pe o imagine docker Debian). Alte soluții au eșuat din cauza erorii „Device or resource busy” (Dispozitiv sau resursă ocupată) la încercarea de a scrie fișierul. –  > Por FedFranz.
  • Dar nu sponge bufferizează întregul fișier în memorie? Asta nu va funcționa dacă are sute de GB. –  > Por OrangeDog.
  • @OrangeDog, Atâta timp cât sistemul de fișiere îl poate stoca, sponge îl va absorbi, deoarece folosește un sistem de stocare de tip /tmp ca o etapă intermediară, care este apoi folosit pentru a înlocui originalul după aceea. –  > Por agc.
Mark Reed

Dacă doriți să modificați fișierul pe loc, puteți folosi întotdeauna fișierul original ed în locul fișierului său streaming succesor sed:

ed "$FILE" <<<$'1d
wq
'

The ed a fost editorul de text original al UNIX, înainte de a exista terminale cu ecran complet și cu atât mai puțin stații de lucru grafice. ex editor, cunoscut cel mai bine ca fiind cel pe care îl folosiți atunci când tastați la promptul de două puncte în vi, , este un exversiune exersată a ed, , astfel încât multe dintre aceleași comenzi funcționează. În timp ce ed este menit să fie utilizat în mod interactiv, acesta poate fi utilizat și în modul batch, trimițându-i un șir de comenzi, ceea ce face această soluție.

Secvența <<<$'1d
wq
'
profită de suportul lui Bash pentru șirurile de caractere here-strings (<<<) și a ghilimelelor POSIX ($'') pentru a introduce date de intrare în ed constând din două linii: 1d, , care dșterge linia 1, , și apoi wq, , care wscrie fișierul înapoi pe disc și apoi quită sesiunea de editare.

Comentarii

  • Dar trebuie să citiți întregul fișier în memorie, ceea ce nu va funcționa dacă acesta are sute de GB. –  > Por OrangeDog.
serup

ar trebui să afișeze liniile cu excepția primei linii :

cat textfile.txt | tail -n +2

Comentarii

  • – ar trebui să faceți „tail -n +2 textfile.txt” –  > Por niglesias.
  • @niglesiais Nu sunt de acord cu „utilizarea inutilă a lui cat”, pentru că este clar că această soluție este ok pentru conținutul canalizat și nu doar pentru fișiere. –  > Por Titou.
Hongbo Liu

S-ar putea folosi vim pentru a face acest lucru:

vim -u NONE +'1d' +'wq!' /tmp/test.txt

Acest lucru ar trebui să fie mai rapid, deoarece vim nu va citi întregul fișier la procesare.

Comentarii

  • Poate fi necesar să se citeze +wq! dacă shell-ul dvs. este bash. Probabil că nu, deoarece ! nu se află la începutul unui cuvânt, dar obișnuirea de a cita lucrurile este, probabil, un lucru bun în general. (Și dacă doriți să obțineți o super-eficiență prin faptul că nu citați inutil, nu aveți nevoie de ghilimele în jurul lui 1d fie.) –  > Por Mark Reed.
  • vim face nevoie să citească întregul fișier. De fapt, dacă fișierul este mai mare decât memoria, așa cum se cere în această întrebare, vim citește întregul fișier și îl scrie (sau cea mai mare parte din el) într-un fișier temporar, iar după editare scrie totul înapoi (în fișierul permanent). Nu știu cum credeți că ar putea funcționa acest lucru fără acest lucru. –  > Por dave_thompson_085.
crydo

Ce-ar fi să folosiți csplit?

man csplit
csplit -k file 1 '{1}'

Comentarii

  • Această sintaxă ar funcționa, de asemenea, dar ar genera doar două fișiere de ieșire în loc de trei: csplit file /^.*$/1. Sau mai simplu: csplit file //1. Sau și mai simplu: csplit file 2. –  > Por Marco Roy.
Brent

Din moment ce se pare că nu pot accelera ștergerea, cred că o abordare bună ar fi să procesez fișierul în loturi, astfel:

While file1 not empty
  file2 = head -n1000 file1
  process file2
  sed -i -e "1000d" file1
end

Inconvenientul acestei metode este că, dacă programul este omorât la mijloc (sau dacă există un fișier sql prost – ceea ce face ca partea de „procesare” să moară sau să se blocheze), vor exista linii care fie sunt sărite, fie sunt procesate de două ori.

(fișierul1 conține linii de cod sql)

Comentarii

  • Ce conține prima linie? Puteți să o suprascrieți cu un comentariu sql, așa cum am sugerat în postarea mea? –  > Por Robert Gamble.
Tim

Dacă ceea ce vrei să faci este să recuperezi după un eșec, ai putea doar să construiești un fișier care să conțină ceea ce ai făcut până acum.

if [[ -f $tmpf ]] ; then
    rm -f $tmpf
fi
cat $srcf |
    while read line ; do
        # process line
        echo "$line" >> $tmpf
    done

egors

Acest one liner va fi suficient:

echo "$(tail -n +2 "$FILE")" > "$FILE"

Funcționează, deoarece tail este executat înainte de echo și apoi fișierul este deblocat, prin urmare nu este nevoie de un fișier temporar.

EvilTeach

Ar putea folosi tail pe N-1 linii și direcționarea acesteia într-un fișier, urmată de eliminarea vechiului fișier și redenumirea noului fișier cu numele vechi să facă treaba?

Dacă aș face acest lucru în mod programatic, aș citi prin fișier și aș reține offset-ul fișierului, după ce am citit fiecare linie, astfel încât să pot căuta înapoi în acea poziție pentru a citi fișierul cu o linie mai puțin în el.

Comentarii

  • Prima soluție este în esență identică cu cea pe care o face Brent acum. Nu înțeleg abordarea dvs. programatică, doar prima linie trebuie ștearsă, ar trebui doar să citiți și să aruncați prima linie și să copiați restul într-un alt fișier, ceea ce este, din nou, același lucru ca și în cazul abordărilor sed și tail. –  > Por Robert Gamble.
  • A doua soluție implică faptul că fișierul nu este micșorat de fiecare dată cu prima linie. Programul pur și simplu îl procesează, ca și cum ar fi fost micșorat, dar începând de fiecare dată de la următoarea linie –  > Por EvilTeach.
  • Tot nu înțeleg care este cea de-a doua soluție a ta. –  > Por Robert Gamble.