상당히 큰 파일 (35Gb)이 있고이 파일을 제자리에서 필터링하고 싶습니다 (예 : 다른 파일을위한 충분한 디스크 공간이 없음). 특히 grep하고 일부 패턴을 무시하고 싶습니다. 다른 파일을 사용하지 않고이 작업을 수행합니까?
foo:
예를 들어 포함하는 모든 줄을 필터링하고 싶다고 가정 해 봅시다 .
상당히 큰 파일 (35Gb)이 있고이 파일을 제자리에서 필터링하고 싶습니다 (예 : 다른 파일을위한 충분한 디스크 공간이 없음). 특히 grep하고 일부 패턴을 무시하고 싶습니다. 다른 파일을 사용하지 않고이 작업을 수행합니까?
foo:
예를 들어 포함하는 모든 줄을 필터링하고 싶다고 가정 해 봅시다 .
답변:
시스템 호출 레벨에서 가능해야합니다. 프로그램은 대상 파일을 자르지 않고 쓰기 위해 열고 stdin에서 읽은 내용을 쓰기 시작할 수 있습니다. EOF를 읽을 때 출력 파일이 잘릴 수 있습니다.
입력에서 행을 필터링하므로 출력 파일 쓰기 위치는 항상 읽기 위치보다 작아야합니다. 이것은 새로운 출력으로 입력을 손상시키지 않아야한다는 것을 의미합니다.
그러나이를 수행하는 프로그램을 찾는 것이 문제입니다. 출력 파일을 열 때 자르지 않는 dd(1)
옵션 conv=notrunc
이 있지만 마지막 부분도 자르지 않고 grep 내용 뒤에 원래 파일 내용을 남겨 둡니다 (와 같은 명령 사용 grep pattern bigfile | dd of=bigfile conv=notrunc
)
시스템 호출 관점에서 매우 간단하기 때문에 작은 프로그램을 작성하여 작은 (1MiB) 전체 루프백 파일 시스템에서 테스트했습니다. 원하는 것을 수행했지만 실제로 다른 파일로 먼저 테스트하려고합니다. 항상 파일을 덮어 쓸 위험이 있습니다.
overwrite.c
/* This code is placed in the public domain by camh */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main(int argc, char **argv)
{
int outfd;
char buf[1024];
int nread;
off_t file_length;
if (argc != 2) {
fprintf(stderr, "usage: %s <output_file>\n", argv[0]);
exit(1);
}
if ((outfd = open(argv[1], O_WRONLY)) == -1) {
perror("Could not open output file");
exit(2);
}
while ((nread = read(0, buf, sizeof(buf))) > 0) {
if (write(outfd, buf, nread) == -1) {
perror("Could not write to output file");
exit(4);
}
}
if (nread == -1) {
perror("Could not read from stdin");
exit(3);
}
if ((file_length = lseek(outfd, 0, SEEK_CUR)) == (off_t)-1) {
perror("Could not get file position");
exit(5);
}
if (ftruncate(outfd, file_length) == -1) {
perror("Could not truncate file");
exit(6);
}
close(outfd);
exit(0);
}
당신은 그것을 다음과 같이 사용할 것입니다 :
grep pattern bigfile | overwrite bigfile
나는 주로 당신이 그것을 시도하기 전에 다른 사람들이 의견을 남길 수 있도록 이것을 게시하고 있습니다. 아마도 다른 누군가가 더 많은 테스트를 거친 비슷한 프로그램을 알고있을 것입니다.
grep
이 읽고 출력 할 수 없습니다 더 많은 데이터를보다 쓰기 위치는 항상 읽기 위치 뒤에해야합니다. 독서와 같은 속도로 글을 쓰더라도 여전히 괜찮습니다. grep 대신 이것을 사용하여 rot13을 시도한 다음 다시 시도하십시오. 이전과 이후에 md5sum을 보시면 똑같이 보입니다.
dd
,하지만 성가신입니다.
sed
파일을 편집 하는 데 사용할 수 있습니다 (그러나 중간 임시 파일이 생성됨).
다음을 포함하는 모든 줄을 제거하려면 foo
:
sed -i '/foo/d' myfile
다음을 포함하는 모든 줄을 유지하려면 foo
:
sed -i '/foo/!d' myfile
$HOME
할 것이다 쓸 수 있지만 /tmp
됩니다 읽기 전용 (기본적으로). 예를 들어, Ubuntu가 있고 복구 콘솔로 부팅 한 경우가 일반적입니다. 또한, 여기-문서 운영자 <<<
가 필요로 하나가 작동하지 않습니다 /tmp
로 R / W 그것뿐만 아니라 거기에 임시 파일을 쓰기 때문입니다. (cf. 이 질문 은 strace
'd 출력을 포함합니다. )
필자는 필터 명령이 접두사 축소 필터 라고 부르는 것으로 가정합니다.이 필터 는 출력의 바이트 N이 N 바이트 이상의 입력을 읽기 전에 절대 쓰지 않는 속성을 갖습니다. grep
이 속성을 가지고 있습니다 (만 필터링하고 일치하는 줄 번호 추가와 같은 다른 일을하지 않는 한). 이러한 필터를 사용하면 입력을 덮어 쓸 수 있습니다. 물론 파일의 시작 부분에서 덮어 쓴 부분이 영구적으로 손실되므로 실수를하지 않아야합니다.
대부분의 유닉스 도구는 파일을 덮어 쓰지 않고 파일에 추가하거나자를 수 있습니다. 표준 도구 상자의 한 가지 예외 dd
는 출력 파일을 자르지 않도록 지시 할 수 있습니다. 따라서 계획은로 명령을 필터링하는 것 dd conv=notrunc
입니다. 파일 크기는 변경되지 않으므로 새 내용의 길이를 잡고 파일을 해당 길이로 자릅니다 (와 함께 dd
). 이 작업은 본질적으로 강력하지 않습니다. 오류가 발생하면 사용자가 스스로해야합니다.
export LC_ALL=C
n=$({ grep -v foo <big_file |
tee /dev/fd/3 |
dd of=big_file conv=notrunc; } 3>&1 | wc -c)
dd if=/dev/null of=big_file bs=1 seek=$n
견고하게 동등한 Perl을 작성할 수 있습니다. 효율적이지 않은 빠른 구현은 다음과 같습니다. 물론 해당 언어로 직접 초기 필터링을 수행 할 수도 있습니다.
grep -v foo <big_file | perl -e '
close STDOUT;
open STDOUT, "+<", $ARGV[0] or die;
while (<STDIN>) {print}
truncate STDOUT, tell STDOUT or die
' big_file
Bourne과 같은 쉘 :
{
cat < bigfile | grep -v to-exclude
perl -e 'truncate STDOUT, tell STDOUT'
} 1<> bigfile
어떤 이유로, 사람들은 40 살 ¹과 표준 읽기 + 쓰기 리디렉션 연산자 를 잊는 경향이 있습니다.
우리는 열 bigfile
읽기 + 쓰기 모드와에 잘림없이 (어떤 일이 대부분 여기에 중요한) stdout
동안은 bigfile
에 공개 (별도)이다 cat
의 stdin
. grep
종료 된 후 일부 라인이 제거 된 경우 stdout
이제 내부 어딘가를 bigfile
가리켜 서이 시점 이후의 항목을 제거해야합니다. 따라서 현재 위치에서 perl
파일 ( truncate STDOUT
) 을 자르는 명령 (에서 반환 tell STDOUT
)
( stdin과 stdout이 동일한 파일을 가리키는 경우 그렇지 않으면 cat
GNU에 대한 것 grep
입니다).
¹ 음, <>
70 년대 후반부터 Bourne 쉘에 있었지만 처음에는 문서화되지 않았으며 제대로 구현되지 않았습니다 . 그것은 ash
1989 년부터 원래 구현되지 않았으며 POSIX sh
리디렉션 연산자 (POSIX sh
가 ksh88
항상 가지고 있었던 90 년대 초 이후 ) 였지만 sh
2000 년까지 FreeBSD 에 추가되지 않았기 때문에 15 년이되었습니다. 오래된 것이 더 정확할 것입니다. 또한 지정되지 않은 경우 기본 파일 디스크립터 는 2010 년 ksh93t +에서 0에서 1로 변경 <>
되었다는 점을 제외하고는 모든 쉘에 ksh93
있습니다 (이전 호환성 및 POSIX 준수 중단).
perl -e 'truncate STDOUT, tell STDOUT'
? 그것은 그것을 포함하지 않고 저에게 효과적입니다. Perl을 사용하지 않고 동일한 것을 달성 할 수있는 방법이 있습니까?
redirection "<>" fixed and documented (used in /etc/inittab f.i.).
. 하나의 힌트입니다.
이것은 오래된 질문이지만, 그것은 영원한 질문이며, 지금까지 제안 된 것보다 더 일반적이고 명확한 해결책을 사용할 수 있습니다. 신용이 필요한 신용 : Stéphane Chazelas의 <>
업데이트 연산자에 대한 언급을 고려하지 않고 신용 카드를 사용했을 것으로 확신하지 않습니다 .
Bourne 쉘에서 업데이트 할 파일 을 여는 것은 유틸리티가 제한적입니다. 쉘은 파일을 탐색 할 수있는 방법과 새로운 길이를 설정하는 방법을 제공하지 않습니다 (이전 길이보다 짧은 경우). 그러나 그것은 쉽게 해결되었으므로의 표준 유틸리티가 아니라는 것에 놀랐습니다 /usr/bin
.
이것은 작동합니다 :
$ grep -n foo T
8:foo
$ (exec 4<>T; grep foo T >&4 && ftruncate 4) && nl T;
1 foo
이와 마찬가지로 (Stéphane에게 팁) :
$ { grep foo T && ftruncate; } 1<>T && nl T;
1 foo
(GNU grep을 사용하고 있습니다. 그가 답변을 작성한 이후에 변경된 것이있을 수 있습니다.)
단, / usr / bin / ftruncate 가 없습니다 . 수십 줄의 C에 대해서는 아래를 참조하십시오. 이 ftruncate 유틸리티는 임의의 파일 설명자를 임의의 길이로 자르며 기본값은 표준 출력과 현재 위치입니다.
위의 명령 (제 1 예)
T
업데이트 를 위해 파일 설명자 4를 엽니 다 . open (2)과 마찬가지로이 방법으로 파일을 열면 현재 오프셋이 0에 배치됩니다. T
정상적으로 처리 되고 셸은 출력을 T
설명자 4 를 통해 리디렉션합니다 .서브 쉘이 종료되고 설명자 4가 닫힙니다 . 다음은 ftruncate입니다 .
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int
main( int argc, char *argv[] ) {
off_t i, fd=1, len=0;
off_t *addrs[2] = { &fd, &len };
for( i=0; i < argc-1; i++ ) {
if( sscanf(argv[i+1], "%lu", addrs[i]) < 1 ) {
err(EXIT_FAILURE, "could not parse %s as number", argv[i+1]);
}
}
if( argc < 3 && (len = lseek(fd, 0, SEEK_CUR)) == -1 ) {
err(EXIT_FAILURE, "could not ftell fd %d as number", (int)fd);
}
if( 0 != ftruncate((int)fd, len) ) {
err(EXIT_FAILURE, argc > 1? argv[1] : "stdout");
}
return EXIT_SUCCESS;
}
NB, ftruncate (2)는 이런 식으로 사용하면 이식 할 수 없습니다. 절대적인 일반성을 위해 마지막으로 쓴 바이트를 읽고 O_WRONLY 파일을 다시 열고 찾은 다음 바이트를 쓰고 닫습니다.
질문이 5 살이라는 점을 감안할 때이 솔루션은 명백하지 않습니다. 이는 활용 간부 새로운 디스크립터 및 개방하는 <>
비전있는 둘 연산자. 파일 디스크립터로 inode를 조작하는 표준 유틸리티를 생각할 수 없습니다. (구문은 일 수 ftruncate >&4
있지만 개선이 확실하지 않습니다.) 그것은 camh의 유능하고 탐구적인 답변보다 상당히 짧습니다. 내가 Perl을 더 좋아하지 않는다면 Stéphane 's, IMO보다 조금 더 명확합니다. 누군가가 유용하다고 생각합니다.
동일한 작업을 수행하는 다른 방법은 현재 오프셋을보고하는 lseek (2)의 실행 버전입니다. 출력은 일부 Linuxi가 제공하는 / usr / bin / truncate에 사용될 수 있습니다 .
ed
적절한 위치에서 파일을 편집 할 수있는 올바른 선택 일 수 있습니다.
ed my_big_file << END_OF_ED_COMMANDS
g/foo:/d
w
q
END_OF_ED_COMMANDS
ed
버전이 다르게 행동 하지 않는다면 man ed
... (GNU Ed 1.4)에서 온 것입니다.If invoked with a file argument, then a copy of file is read into the editor's buffer. Changes are made to this copy and not directly to file itself.
ed
파일을 버퍼로 읽어되기 때문에 35기가바이트 파일을 편집하기위한 GOOL 솔루션이 아닙니다.
!
), 그래서 그것의 소매를 몇 가지 더 재미있는 트릭을 할 수 있습니다.
ed
파일 의 쓰기 작업 이 파일을 자르고 다시 작성한다고 확신합니다. 따라서 OP가 원하는대로 디스크의 데이터를 제자리에서 변경하지 않습니다. 또한 파일이 너무 커서 메모리에로드 할 수없는 경우 작동하지 않습니다.
당신은 (에 현장을 덮어 쓰기합니다), 다음 파일을 엽니 다 읽기 / 쓰기 파일 설명 떠들썩한 파티를 사용 sed
하고 truncate
...하지만 물론, 지금까지 데이터가 지금까지 읽을의 변경 사항이 금액보다 큰 것을 허용하지 않습니다 .
다음은 스크립트입니다 (bash 변수 $ BASHPID 사용).
# Create a test file
echo "going abc" >junk
echo "going def" >>junk
echo "# ORIGINAL file";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )
#
# Assign file to fd 3, and open it r/w
exec 3<> junk
#
# Choose a unique filename to hold the new file size and the pid
# of the semi-asynchrounous process to which 'tee' streams the new file..
[[ ! -d "/tmp/$USER" ]] && mkdir "/tmp/$USER"
f_pid_size="/tmp/$USER/pid_size.$(date '+%N')" # %N is a GNU extension: nanoseconds
[[ -f "$f_pid_size" ]] && { echo "ERROR: Work file already exists: '$f_pid_size'" ;exit 1 ; }
#
# run 'sed' output to 'tee' ...
# to modify the file in-situ, and to count the bytes
<junk sed -e "s/going //" |tee >(echo -n "$BASHPID " >"$f_pid_size" ;wc -c >>"$f_pid_size") >&3
#
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
# The byte-counting process is not a child-process,
# so 'wait' doesn't work... but wait we must...
pid_size=($(cat "$f_pid_size")) ;pid=${pid_size[0]}
# $f_pid_size may initially contain only the pid...
# get the size when pid termination is assured
while [[ "$pid" != "" ]] ; do
if ! kill -0 "$pid" 2>/dev/null; then
pid="" # pid has terminated. get the byte count
pid_size=($(cat "$f_pid_size")) ;size=${pid_size[1]}
fi
done
rm "$f_pid_size"
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
#
exec 3>&- # close fd 3.
newsize=$(cat newsize)
echo "# MODIFIED file (before truncating)";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo ) cat junk
#
truncate -s $newsize junk
echo "# NEW (truncated) file";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo ) cat junk
#
exit
테스트 출력은 다음과 같습니다
# ORIGINAL file
going abc
going def
# 2 lines, 20 bytes
# MODIFIED file (before truncating)
abc
def
c
going def
# 4 lines, 20 bytes
# NEW (truncated) file
abc
def
# 2 lines, 8 bytes
현장에서 정확하게 는 아니지만 유사한 상황에서 사용될 수 있습니다.
디스크 공간이 문제인 경우 파일을 먼저 압축 한 다음 (텍스트이므로 크게 줄어 듭니다) 압축 해제 / 압축 파이프 라인 중간에 일반적인 방식으로 sed (또는 grep 등)를 사용하십시오.
# Reduce size from ~35Gb to ~6Gb
$ gzip MyFile
# Edit file, creating another ~6Gb file
$ gzip -dc <MyFile.gz | sed -e '/foo/d' | gzip -c >MyEditedFile.gz
sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
이 질문에 인터넷 검색 사람의 이익을 위해, 정답은 중단 이 패턴의 어떤 변화를 사용하는 대신 무시할 성능 향상을위한 파일을 손상 위험이 모호한 쉘 기능을 찾고, 그리고 :
grep "foo" file > file.new && mv file.new file
매우 드문 경우이지만 이것이 불가능한 이유 일 경우 에만 이 페이지의 다른 답변을 진지하게 고려해야합니다 (확실히 읽는 것이 흥미롭지 만). 두 번째 파일을 만들 디스크 공간이 없다는 OP의 수수께끼가 바로 그런 상황이라는 것을 인정합니다. 그럼에도 불구하고 @Ed Randall 및 @Basile Starynkevitch와 같은 다른 옵션도 있습니다.