IT박스

목표 -C : 파일을 한 줄씩 읽기

itboxs 2020. 6. 24. 07:51
반응형

목표 -C : 파일을 한 줄씩 읽기


Objective-C에서 큰 텍스트 파일을 처리하는 적절한 방법은 무엇입니까? 각 줄을 개별적으로 읽고 각 줄을 NSString으로 취급하려고한다고 가정 해 봅시다. 가장 효율적인 방법은 무엇입니까?

한 가지 해결책은 NSString 방법을 사용하는 것입니다.

+ (id)stringWithContentsOfFile:(NSString *)path 
      encoding:(NSStringEncoding)enc 
      error:(NSError **)error 

그런 다음 줄 바꿈 구분 기호로 줄을 분할 한 다음 배열의 요소를 반복합니다. 그러나 이것은 상당히 비효율적입니다. 파일을 한 번에 모두 읽는 대신 파일을 스트림으로 취급하여 각 줄을 열거하는 쉬운 방법이 있습니까? Java의 java.io.BufferedReader를 좋아하십시오.


좋은 질문입니다. @Diederik 은 좋은 대답을 가지고 있다고 생각 하지만, Cocoa가 정확히 당신이하고 싶은 일에 대한 메커니즘을 가지고 있지 않다는 것은 불행합니다.

NSInputStreamN 바이트 청크를 읽을 java.io.BufferedReader수는 있지만 (와 매우 유사 ) 직접 바이트 로 변환 NSString한 다음 줄 바꿈 (또는 다른 구분 기호)을 스캔하고 다음 읽기를 위해 나머지 문자를 저장하거나 더 많은 문자를 읽으십시오 개행을 아직 읽지 않은 경우. ( NSFileHandle를 읽고 NSData로 변환 할 수는 NSString있지만 본질적으로 동일한 프로세스입니다.)

Apple은 세부 사항을 작성하는 데 도움 이되는 스트림 프로그래밍 안내서가지고 있으며 , 이 SO 질문uint8_t*버퍼를 다룰 때 도움이 될 수 있습니다 .

당신이 (특히 프로그램의 다른 부분에) 자주 같은 문자열을 읽기 위하여려고하는 경우 당신에 대한 세부 정보를 처리, 또는 서브 클래스 수있는 클래스에서이 동작을 캡슐화하는 좋은 아이디어가 될 것입니다 NSInputStream(이 있어요 수 있도록 설계 서브 클래스 )와 정확히 당신이 원하는 것을 읽을 수 있도록 방법을 추가.

레코드의 경우이 기능을 추가하는 것이 좋을 것으로 생각되며이를 가능하게하는 개선 요청을 제출할 것입니다. :-)


편집 : 이 요청이 이미 존재합니다. 이를 위해 2006 년부터 데이트 한 레이더가있다 (애플 내부 사람들을위한 rdar : // 4742914).


이것은 일반적으로 읽기 A의 작동 String에서 Text. 더 긴 텍스트 (큰 텍스트) 를 읽으려면 버퍼링 (메모리 공간에 텍스트 크기를 예약) 과 같은 다른 사람들이 언급 한 방법을 사용하십시오 .

텍스트 파일을 읽었다 고 가정하십시오.

NSString* filePath = @""//file path...
NSString* fileRoot = [[NSBundle mainBundle] 
               pathForResource:filePath ofType:@"txt"];

새 줄을 제거하고 싶습니다.

// read everything from text
NSString* fileContents = 
      [NSString stringWithContentsOfFile:fileRoot 
       encoding:NSUTF8StringEncoding error:nil];

// first, separate by new line
NSArray* allLinedStrings = 
      [fileContents componentsSeparatedByCharactersInSet:
      [NSCharacterSet newlineCharacterSet]];

// then break down even further 
NSString* strsInOneLine = 
      [allLinedStrings objectAtIndex:0];

// choose whatever input identity you have decided. in this case ;
NSArray* singleStrs = 
      [currentPointString componentsSeparatedByCharactersInSet:
      [NSCharacterSet characterSetWithCharactersInString:@";"]];

거기 있어요


트릭을 수행해야합니다.

#include <stdio.h>

NSString *readLineAsNSString(FILE *file)
{
    char buffer[4096];

    // tune this capacity to your liking -- larger buffer sizes will be faster, but
    // use more memory
    NSMutableString *result = [NSMutableString stringWithCapacity:256];

    // Read up to 4095 non-newline characters, then read and discard the newline
    int charsRead;
    do
    {
        if(fscanf(file, "%4095[^\n]%n%*c", buffer, &charsRead) == 1)
            [result appendFormat:@"%s", buffer];
        else
            break;
    } while(charsRead == 4095);

    return result;
}

다음과 같이 사용하십시오 :

FILE *file = fopen("myfile", "r");
// check for NULL
while(!feof(file))
{
    NSString *line = readLineAsNSString(file);
    // do stuff with line; line is autoreleased, so you should NOT release it (unless you also retain it beforehand)
}
fclose(file);

이 코드는 파일에서 개행 문자를 한 번에 최대 4095 개까지 읽습니다. 4095자를 초과하는 줄이 있으면 줄 바꿈이나 파일 끝이 될 때까지 계속 읽습니다.

참고 :이 코드는 테스트하지 않았습니다. 사용하기 전에 테스트하십시오.


Mac OS X은 Unix이고 Objective-C는 C 수퍼 셋이므로 구식 fopenfgets에서 사용할 수 있습니다 <stdio.h>. 작동합니다.

[NSString stringWithUTF8String:buf]C 문자열을로 변환합니다 NSString. 다른 인코딩으로 문자열을 작성하고 복사하지 않고 작성하는 방법도 있습니다.


NSInputStream파일 스트림에 대한 기본 구현이있는 것을 사용할 수 있습니다 . 바이트를 버퍼 ( read:maxLength:메서드) 로 읽을 수 있습니다 . 개행을 위해 버퍼를 직접 스캔해야합니다.


Cocoa / Objective-C에서 텍스트 파일을 읽는 적절한 방법은 Apple의 String 프로그래밍 안내서에 설명되어 있습니다. 파일읽고 쓰는 부분은 당신이 추구하는 것이어야합니다. PS : "라인"이란 무엇입니까? "\ n"으로 구분 된 문자열의 두 섹션? 아니면 "\ r"? 아니면 "\ r \ n"? 아니면 실제로 단락 뒤에 있습니까? 앞에서 언급 한 안내서에는 문자열을 줄이나 단락으로 나누는 섹션도 포함되어 있습니다. (이 섹션은 "단락 및 줄 바꿈"이라고하며 위에서 지적한 페이지의 왼쪽 메뉴에 링크되어 있습니다. 불행히도이 사이트에서는 내가 URL을 두 개 이상 게시 할 수 없습니다. 신뢰할 수있는 사용자가 아닙니다.)

크 누스의 말을 인용하자면 : 조기 최적화는 모든 악의 근원입니다. 단순히 "전체 파일을 메모리로 읽는"속도가 느리다고 가정하지 마십시오. 벤치마킹 했습니까? 실제로 전체 파일을 메모리로 읽는다는 것을 알고 있습니까? 어쩌면 단순히 프록시 객체를 반환하고 문자열을 소비하면서 장면 뒤에서 계속 읽는가? ( 면책 조항 : NSString이 실제로이 작업을 수행하는지 알 수 없습니다. 아마도 가능합니다. ) 요점은 : 먼저 문서화 된 방식으로 작업을 수행하는 것입니다. 그런 다음 벤치 마크에서 원하는 성능이없는 것으로 나타나면 최적화하십시오.


이러한 많은 답변은 긴 코드 덩어리이거나 전체 파일을 읽습니다. 이 작업에 c 메소드를 사용하고 싶습니다.

FILE* file = fopen("path to my file", "r");

size_t length;
char *cLine = fgetln(file,&length);

while (length>0) {
    char str[length+1];
    strncpy(str, cLine, length);
    str[length] = '\0';

    NSString *line = [NSString stringWithFormat:@"%s",str];        
    % Do what you want here.

    cLine = fgetln(file,&length);
}

fgetln은 개행 문자를 유지하지 않습니다. 또한 NULL 종료를위한 공간을 만들고 싶기 때문에 str의 길이를 +1합니다.


파일을 한 줄씩 읽으려면 다음과 같은 기능을 수행 할 수 있습니다.

DDFileReader * reader = [[DDFileReader alloc] initWithFilePath:pathToMyFile];
NSString * line = nil;
while ((line = [reader readLine])) {
  NSLog(@"read line: %@", line);
}
[reader release];

또는:

DDFileReader * reader = [[DDFileReader alloc] initWithFilePath:pathToMyFile];
[reader enumerateLinesUsingBlock:^(NSString * line, BOOL * stop) {
  NSLog(@"read line: %@", line);
}];
[reader release];

이를 가능하게하는 DDFileReader 클래스는 다음과 같습니다.

인터페이스 파일 (.h) :

@interface DDFileReader : NSObject {
    NSString * filePath;

    NSFileHandle * fileHandle;
    unsigned long long currentOffset;
    unsigned long long totalFileLength;

    NSString * lineDelimiter;
    NSUInteger chunkSize;
}

@property (nonatomic, copy) NSString * lineDelimiter;
@property (nonatomic) NSUInteger chunkSize;

- (id) initWithFilePath:(NSString *)aPath;

- (NSString *) readLine;
- (NSString *) readTrimmedLine;

#if NS_BLOCKS_AVAILABLE
- (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL *))block;
#endif

@end

구현 (.m)

#import "DDFileReader.h"

@interface NSData (DDAdditions)

- (NSRange) rangeOfData_dd:(NSData *)dataToFind;

@end

@implementation NSData (DDAdditions)

- (NSRange) rangeOfData_dd:(NSData *)dataToFind {

    const void * bytes = [self bytes];
    NSUInteger length = [self length];

    const void * searchBytes = [dataToFind bytes];
    NSUInteger searchLength = [dataToFind length];
    NSUInteger searchIndex = 0;

    NSRange foundRange = {NSNotFound, searchLength};
    for (NSUInteger index = 0; index < length; index++) {
        if (((char *)bytes)[index] == ((char *)searchBytes)[searchIndex]) {
            //the current character matches
            if (foundRange.location == NSNotFound) {
                foundRange.location = index;
            }
            searchIndex++;
            if (searchIndex >= searchLength) { return foundRange; }
        } else {
            searchIndex = 0;
            foundRange.location = NSNotFound;
        }
    }
    return foundRange;
}

@end

@implementation DDFileReader
@synthesize lineDelimiter, chunkSize;

- (id) initWithFilePath:(NSString *)aPath {
    if (self = [super init]) {
        fileHandle = [NSFileHandle fileHandleForReadingAtPath:aPath];
        if (fileHandle == nil) {
            [self release]; return nil;
        }

        lineDelimiter = [[NSString alloc] initWithString:@"\n"];
        [fileHandle retain];
        filePath = [aPath retain];
        currentOffset = 0ULL;
        chunkSize = 10;
        [fileHandle seekToEndOfFile];
        totalFileLength = [fileHandle offsetInFile];
        //we don't need to seek back, since readLine will do that.
    }
    return self;
}

- (void) dealloc {
    [fileHandle closeFile];
    [fileHandle release], fileHandle = nil;
    [filePath release], filePath = nil;
    [lineDelimiter release], lineDelimiter = nil;
    currentOffset = 0ULL;
    [super dealloc];
}

- (NSString *) readLine {
    if (currentOffset >= totalFileLength) { return nil; }

    NSData * newLineData = [lineDelimiter dataUsingEncoding:NSUTF8StringEncoding];
    [fileHandle seekToFileOffset:currentOffset];
    NSMutableData * currentData = [[NSMutableData alloc] init];
    BOOL shouldReadMore = YES;

    NSAutoreleasePool * readPool = [[NSAutoreleasePool alloc] init];
    while (shouldReadMore) {
        if (currentOffset >= totalFileLength) { break; }
        NSData * chunk = [fileHandle readDataOfLength:chunkSize];
        NSRange newLineRange = [chunk rangeOfData_dd:newLineData];
        if (newLineRange.location != NSNotFound) {

            //include the length so we can include the delimiter in the string
            chunk = [chunk subdataWithRange:NSMakeRange(0, newLineRange.location+[newLineData length])];
            shouldReadMore = NO;
        }
        [currentData appendData:chunk];
        currentOffset += [chunk length];
    }
    [readPool release];

    NSString * line = [[NSString alloc] initWithData:currentData encoding:NSUTF8StringEncoding];
    [currentData release];
    return [line autorelease];
}

- (NSString *) readTrimmedLine {
    return [[self readLine] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}

#if NS_BLOCKS_AVAILABLE
- (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL*))block {
  NSString * line = nil;
  BOOL stop = NO;
  while (stop == NO && (line = [self readLine])) {
    block(line, &stop);
  }
}
#endif

@end

수업은 Dave DeLong 이 수행했습니다.


@porneL이 말했듯이 C api는 매우 편리합니다.

NSString* fileRoot = [[NSBundle mainBundle] pathForResource:@"record" ofType:@"txt"];
FILE *file = fopen([fileRoot UTF8String], "r");
char buffer[256];
while (fgets(buffer, 256, file) != NULL){
    NSString* result = [NSString stringWithUTF8String:buffer];
    NSLog(@"%@",result);
}

다른 사람들이 NSInputStream과 NSFileHandle 둘 다 대답했듯이 훌륭한 옵션이지만 NSData 및 메모리 매핑을 사용하여 상당히 간단한 방법으로 수행 할 수도 있습니다.

BRLineReader.h

#import <Foundation/Foundation.h>

@interface BRLineReader : NSObject

@property (readonly, nonatomic) NSData *data;
@property (readonly, nonatomic) NSUInteger linesRead;
@property (strong, nonatomic) NSCharacterSet *lineTrimCharacters;
@property (readonly, nonatomic) NSStringEncoding stringEncoding;

- (instancetype)initWithFile:(NSString *)filePath encoding:(NSStringEncoding)encoding;
- (instancetype)initWithData:(NSData *)data encoding:(NSStringEncoding)encoding;
- (NSString *)readLine;
- (NSString *)readTrimmedLine;
- (void)setLineSearchPosition:(NSUInteger)position;

@end

BRLineReader.m

#import "BRLineReader.h"

static unsigned char const BRLineReaderDelimiter = '\n';

@implementation BRLineReader
{
    NSRange _lastRange;
}

- (instancetype)initWithFile:(NSString *)filePath encoding:(NSStringEncoding)encoding
{
    self = [super init];
    if (self) {
        NSError *error = nil;
        _data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedAlways error:&error];
        if (!_data) {
            NSLog(@"%@", [error localizedDescription]);
        }
        _stringEncoding = encoding;
        _lineTrimCharacters = [NSCharacterSet whitespaceAndNewlineCharacterSet];
    }

    return self;
}

- (instancetype)initWithData:(NSData *)data encoding:(NSStringEncoding)encoding
{
    self = [super init];
    if (self) {
        _data = data;
        _stringEncoding = encoding;
        _lineTrimCharacters = [NSCharacterSet whitespaceAndNewlineCharacterSet];
    }

    return self;
}

- (NSString *)readLine
{
    NSUInteger dataLength = [_data length];
    NSUInteger beginPos = _lastRange.location + _lastRange.length;
    NSUInteger endPos = 0;
    if (beginPos == dataLength) {
        // End of file
        return nil;
    }

    unsigned char *buffer = (unsigned char *)[_data bytes];
    for (NSUInteger i = beginPos; i < dataLength; i++) {
        endPos = i;
        if (buffer[i] == BRLineReaderDelimiter) break;
    }

    // End of line found
    _lastRange = NSMakeRange(beginPos, endPos - beginPos + 1);
    NSData *lineData = [_data subdataWithRange:_lastRange];
    NSString *line = [[NSString alloc] initWithData:lineData encoding:_stringEncoding];
    _linesRead++;

    return line;
}

- (NSString *)readTrimmedLine
{
    return [[self readLine] stringByTrimmingCharactersInSet:_lineTrimCharacters];
}

- (void)setLineSearchPosition:(NSUInteger)position
{
    _lastRange = NSMakeRange(position, 0);
    _linesRead = 0;
}

@end

이 답변은 ObjC가 아니라 C입니다.

ObjC는 'C'기반이므로 fget을 사용하지 않는 이유는 무엇입니까?

그리고 네, ObjC는 독자적인 방법을 가지고 있다고 확신합니다-나는 그것이 무엇인지 아직 알기에 충분하지 않습니다 :)


@Adam Rosenfield의 답변에서 형식 문자열은 fscanf다음과 같이 변경됩니다.

"%4095[^\r\n]%n%*[\n\r]"

그것은 osx, linux, windows 줄 끝에서 작동합니다.


우리의 인생을 조금 더 쉽게 만들기 위해 카테고리 또는 확장을 사용합니다.

extension String {

    func lines() -> [String] {
        var lines = [String]()
        self.enumerateLines { (line, stop) -> () in
            lines.append(line)
        }
        return lines
    }

}

// then
for line in string.lines() {
    // do the right thing
}

@lukaswelte의 답변과 Dave DeLong의 코드가 매우 도움이되었습니다. 나는이 문제에 대한 해결책을 찾고 있었지만 \r\n단지 큰 파일을 파싱해야했습니다 \n.

작성된 코드는 둘 이상의 문자로 구문 분석하는 경우 버그를 포함합니다. 아래와 같이 코드를 변경했습니다.

.h 파일 :

#import <Foundation/Foundation.h>

@interface FileChunkReader : NSObject {
    NSString * filePath;

    NSFileHandle * fileHandle;
    unsigned long long currentOffset;
    unsigned long long totalFileLength;

    NSString * lineDelimiter;
    NSUInteger chunkSize;
}

@property (nonatomic, copy) NSString * lineDelimiter;
@property (nonatomic) NSUInteger chunkSize;

- (id) initWithFilePath:(NSString *)aPath;

- (NSString *) readLine;
- (NSString *) readTrimmedLine;

#if NS_BLOCKS_AVAILABLE
- (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL *))block;
#endif

@end

.m 파일 :

#import "FileChunkReader.h"

@interface NSData (DDAdditions)

- (NSRange) rangeOfData_dd:(NSData *)dataToFind;

@end

@implementation NSData (DDAdditions)

- (NSRange) rangeOfData_dd:(NSData *)dataToFind {

    const void * bytes = [self bytes];
    NSUInteger length = [self length];

    const void * searchBytes = [dataToFind bytes];
    NSUInteger searchLength = [dataToFind length];
    NSUInteger searchIndex = 0;

    NSRange foundRange = {NSNotFound, searchLength};
    for (NSUInteger index = 0; index < length; index++) {
        if (((char *)bytes)[index] == ((char *)searchBytes)[searchIndex]) {
            //the current character matches
            if (foundRange.location == NSNotFound) {
                foundRange.location = index;
            }
            searchIndex++;
            if (searchIndex >= searchLength)
            {
                return foundRange;
            }
        } else {
            searchIndex = 0;
            foundRange.location = NSNotFound;
        }
    }

    if (foundRange.location != NSNotFound
        && length < foundRange.location + foundRange.length )
    {
        // if the dataToFind is partially found at the end of [self bytes],
        // then the loop above would end, and indicate the dataToFind is found
        // when it only partially was.
        foundRange.location = NSNotFound;
    }

    return foundRange;
}

@end

@implementation FileChunkReader

@synthesize lineDelimiter, chunkSize;

- (id) initWithFilePath:(NSString *)aPath {
    if (self = [super init]) {
        fileHandle = [NSFileHandle fileHandleForReadingAtPath:aPath];
        if (fileHandle == nil) {
            return nil;
        }

        lineDelimiter = @"\n";
        currentOffset = 0ULL; // ???
        chunkSize = 128;
        [fileHandle seekToEndOfFile];
        totalFileLength = [fileHandle offsetInFile];
        //we don't need to seek back, since readLine will do that.
    }
    return self;
}

- (void) dealloc {
    [fileHandle closeFile];
    currentOffset = 0ULL;

}

- (NSString *) readLine {
    if (currentOffset >= totalFileLength)
    {
        return nil;
    }

    @autoreleasepool {

        NSData * newLineData = [lineDelimiter dataUsingEncoding:NSUTF8StringEncoding];
        [fileHandle seekToFileOffset:currentOffset];
        unsigned long long originalOffset = currentOffset;
        NSMutableData *currentData = [[NSMutableData alloc] init];
        NSData *currentLine = [[NSData alloc] init];
        BOOL shouldReadMore = YES;


        while (shouldReadMore) {
            if (currentOffset >= totalFileLength)
            {
                break;
            }

            NSData * chunk = [fileHandle readDataOfLength:chunkSize];
            [currentData appendData:chunk];

            NSRange newLineRange = [currentData rangeOfData_dd:newLineData];

            if (newLineRange.location != NSNotFound) {

                currentOffset = originalOffset + newLineRange.location + newLineData.length;
                currentLine = [currentData subdataWithRange:NSMakeRange(0, newLineRange.location)];

                shouldReadMore = NO;
            }else{
                currentOffset += [chunk length];
            }
        }

        if (currentLine.length == 0 && currentData.length > 0)
        {
            currentLine = currentData;
        }

        return [[NSString alloc] initWithData:currentLine encoding:NSUTF8StringEncoding];
    }
}

- (NSString *) readTrimmedLine {
    return [[self readLine] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}

#if NS_BLOCKS_AVAILABLE
- (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL*))block {
    NSString * line = nil;
    BOOL stop = NO;
    while (stop == NO && (line = [self readLine])) {
        block(line, &stop);
    }
}
#endif

@end

내가 시도한 다른 모든 대답이 어쨌든 부족했기 때문에 이것을 추가하고 있습니다. 다음 방법은 큰 파일, 임의의 긴 줄 및 빈 줄을 처리 할 수 ​​있습니다. 실제 내용으로 테스트되었으며 출력에서 ​​줄 바꿈 문자를 제거합니다.

- (NSString*)readLineFromFile:(FILE *)file
{
    char buffer[4096];
    NSMutableString *result = [NSMutableString stringWithCapacity:1000];

    int charsRead;
    do {
        if(fscanf(file, "%4095[^\r\n]%n%*[\n\r]", buffer, &charsRead) == 1) {
            [result appendFormat:@"%s", buffer];
        }
        else {
            break;
        }
    } while(charsRead == 4095);

    return result.length ? result : nil;
}

크레딧은 @Adam Rosenfield와 @sooop로갑니다.


작은 파일에 사용하는 멋진 간단한 솔루션이 있습니다.

NSString *path = [[NSBundle mainBundle] pathForResource:@"Terrain1" ofType:@"txt"];
NSString *contents = [NSString stringWithContentsOfFile:path encoding:NSASCIIStringEncoding error:nil];
NSArray *lines = [contents componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\r\n"]];
for (NSString* line in lines) {
    if (line.length) {
        NSLog(@"line: %@", line);
    }
}

이 스크립트를 사용하면 효과적입니다.

NSString *path = @"/Users/xxx/Desktop/names.txt";
NSError *error;
NSString *stringFromFileAtPath = [NSString stringWithContentsOfFile: path
                                                           encoding: NSUTF8StringEncoding
                                                              error: &error];
if (stringFromFileAtPath == nil) {
    NSLog(@"Error reading file at %@\n%@", path, [error localizedFailureReason]);
}
NSLog(@"Contents:%@", stringFromFileAtPath);

참고 URL : https://stackoverflow.com/questions/1044334/objective-c-reading-a-file-line-by-line

반응형