파일로부터 한줄 씩 읽기

파이썬

가장 일반적으로 파일을 한 줄씩 읽어들이는 방법은 다음과 같다.

with open('filename.txt', 'r') as f:
    for line in f:
        pass # line은 읽어들인 각 줄

하지만 이 방법은 파일을 한 번에 메모리로 읽어들인 다음, 한 줄씩 스캔한 결과를 돌려주기 때문에 수 기가짜리 로그 파일을 이런 식으로 처리하면 문제가 생긴다.

좀 더 안전한 방법

import fileinput
for line in fileinput.input(['myfile']):
    pass # line 은 파일의 각 줄

fileinput 모듈은 하나의 파일 및 연속열로 주어진 일련의 파일에 대해서 빠르게 한줄씩 읽는 처리를 할 수 있다.

Objective-C

한 번에 읽어서 나누기

읽어들인 문자열을 줄 바꿈문자를 사용해서 쪼개면 되는데….

NSFileManager *fm = [NSFileManager defaultManager];
NSString *filePath = [[fn currentDirectoryPath] stringByAppendingPathComponent:@"filename.txt"];
NSString *fileContents = [NSString stringWithContentsOfFile:filePath];
NSArray *lines = [fileContents componentsSeparatedWithCharactersInSet:[NSCharacterSet newlineCharacterSet]];

역시 한 번에 메모리로 다 읽어들여야 한다. 이를 극복하려면 NSInputStream을 사용해서 입력 소스로 파일을 설정하고 정해진 바이트만큼씩 읽어들여서 문자열로 변환한다. 물론 정해진 바이트수만큼씩 읽어들이기 때문에 줄바꿈 문자를 읽었다면 그 이후의 나머지 바이트들은 따로 저장했다가 다음 번 읽는 바이트와 합쳐야 한다. (게다가 UTF8은 한 글자당 1~3 바이트로 구성되기 때문에 처리가 어렵다.)

조금씩 읽기

결국, 정해진 바이트 수 만큼 읽는 것은 같지만, C에는 fscanf라는 걸출한 입력 포맷 파서가 있다.

NSString * readLineAsNSString(FILE *file) {
    int charsRead = 4096;
    char buffer[4096];
    NSMutableString *result = [NSMutableString stringWithCapacity:4096];
    while( charsRead == 4096) {
        if (fscanf(file, "%4095[^\r\n]%n%*[\r\n]", buffer, &charsRead) == 1) {
            [result appendFormat:@"%s", buffer];
        } else {
            break;
        }
    }
    return result;
}

위 코드는 fscanf() 함수를 사용한다. 이 때 포맷팅의 몇 가지 묘를 보면

  • %n은 읽어들인 바이트수를 내놓는데, 실제로 파일의 오프셋은 움직이지 않는다. 즉 방금 읽어들인 바이트수를 구한다.
  • %숫자는 정해진 숫자만큼의 바이트를 읽는다.
  • %[^문자]는 정규식과 유사한패턴으로 해당 문자들을 제외하고 읽는다. 해당 문자를 만나면 더 이상 읽는 것을 중지한다.
  • %* *가 붙으면 읽은 양만큼을 무시한다. 포맷 스트링 뒤에 오는 변수에 담지 않는다.

결국 저 포맷팅은

  • 줄바꿈 문자에 사용되는 r, n을 제외하고 최대 4095자까지 읽어들인다.
  • 줄바꿈 문자를 만나거나, 4096바이트를 읽었다면 그만 읽고 읽은 바이트 스트림을 문자열로 변환한다.
  • 이후 이어지는 줄바꿈문자 수만큼 오프셋을 이동한다. (그래야 다음 루프에서 정상적으로 줄바꿈 문자가 아닌 줄을 읽게 된다.)

따라서 로직은

  • 포맷팅에 맞게 값을 읽어들이고,
  • 만약 읽어들인 값이 없다면 루프를 탈출한다.
  • 만약 읽어들인 값이 4096자라면 한 번 더 반복한다.

와 같이 동작하여 큰 파일을 한 줄 씩 읽을 수 있게 한다. 대신, 한줄의 길이의 양을 사전에 제한할 수 있어야 한다. 만약 그마저도 가능하지 않다면, 버퍼 크기만큼 줄을 자르도록 (위 코드에서는 while 코드를 삭제) 하여 처리하는 것도 한가지 방법이다.

하지만 이 방법 역시, 바이트 수를 기준으로 읽기 때문에 4096자 이상의 줄을 만나고, 그 줄이 영어가 아닌 언어로 쓰여있는 경우, 마지막 부분의 문자가 깨질 우려가 있다. (이 경우에는 버퍼 크기보다 3바이트 정도 작게 읽은 다음, 버퍼의 끝부분 몇 바이트를 검사하여 올바른 UTF8 문자 값이 되도록 한 두 바이트를 더 읽도록 해도 된다.)

사용법

FILE *f = fopen("mytext.txt", "r");
while (!feof(f)) {
    NSString *line = readLineAsNSString(f);
    NSLog(@"%@", line);
}

feof() 함수는 파일의 끝에 도달했는지 여부를 체크하는 함수이다. (파일의 끝까지 읽었다면 1을 리턴) 이 함수를 사용하여 루프를 돌면서 파일을 한 줄 씩 처리할 수 있다.