Цель-C: чтение файла построчно - PullRequest
136 голосов
/ 25 июня 2009

Как правильно обращаться с большими текстовыми файлами в Objective-C? Допустим, мне нужно читать каждую строку отдельно и хотеть рассматривать каждую строку как строку NSString. Какой самый эффективный способ сделать это?

Одно решение использует метод NSString:

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

, а затем разделите строки с помощью разделителя новой строки, а затем переберите элементы массива. Однако это кажется довольно неэффективным. Разве нет простого способа обработать файл как поток, перечисляя по каждой строке, вместо того, чтобы просто прочитать все сразу? Вроде как Java java.io.BufferedReader.

Ответы [ 17 ]

94 голосов
/ 17 декабря 2010

Это будет работать для общего чтения 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:@";"]];

Вот, пожалуйста.

63 голосов
/ 25 июня 2009

Это отличный вопрос. Я думаю, что @ Diederik имеет хороший ответ, хотя, к сожалению, у Какао нет механизма для именно того, что вы хотите сделать.

NSInputStream позволяет читать фрагменты по N байтов (очень похоже на java.io.BufferedReader), но вы должны конвертировать его в NSString самостоятельно, а затем сканировать новые строки (или любой другой разделитель) и сохраните все оставшиеся символы для следующего чтения или прочитайте больше символов, если символ новой строки еще не был прочитан. (NSFileHandle позволяет читать NSData, который затем можно преобразовать в NSString, но по сути это тот же процесс.)

У Apple есть Руководство по программированию потока , которое может помочь заполнить детали, и этот вопрос SO также может помочь, если вы собираетесь иметь дело с uint8_t* буферами .

Если вы собираетесь часто читать такие строки (особенно в разных частях вашей программы), было бы неплохо инкапсулировать это поведение в классе, который может обработать детали для вас, или даже создать подклассы NSInputStream (он предназначен для подкласса ) и добавляет методы, которые позволяют вам читать именно то, что вы хотите.

Для справки, я думаю, что это было бы хорошей возможностью для добавления, и я буду подавать запрос на улучшение для чего-то, что делает это возможным. : -)


Редактировать: Оказывается, этот запрос уже существует. Для этого есть радар, датированный 2006 годом (rdar: // 4742914 для сотрудников Apple).

34 голосов
/ 25 июня 2009

Это должно сработать:

#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 символов, она продолжает читать, пока не достигнет новой строки или конца файла.

Примечание : я не проверял этот код. Пожалуйста, проверьте его перед использованием.

12 голосов
/ 04 августа 2010

Mac OS X - Unix, Objective-C - расширенный набор C, так что вы можете просто использовать old-school fopen и fgets из <stdio.h>. Это гарантированно работает.

[NSString stringWithUTF8String:buf] преобразует строку C в NSString. Существуют также методы для создания строк в других кодировках и создания без копирования.

9 голосов
/ 25 июня 2009

Вы можете использовать NSInputStream, который имеет базовую реализацию для файловых потоков. Вы можете прочитать байты в буфер (метод read:maxLength:). Вы должны сканировать буфер на наличие новых строк самостоятельно.

6 голосов
/ 27 июня 2009

Соответствующий способ чтения текстовых файлов в Cocoa / Objective-C описан в руководстве по программированию Apple String. Раздел для чтения и записи файлов должен быть именно тем, что вам нужно. PS: что такое "линия"? Два раздела строки, разделенные "\ n"? Или "г"? Или "\ r \ n"? Или, может быть, вы на самом деле после абзацев? Ранее упомянутое руководство также включает раздел по разбиению строки на строки или абзацы. (Этот раздел называется «Абзацы и разрывы строк» ​​и связан с левым меню на странице, на которую я указал выше. К сожалению, этот сайт не позволяет мне публиковать более одного URL, поскольку я еще не заслуживающий доверия пользователь.)

Перефразируя Кнута: преждевременная оптимизация - корень всего зла. Не просто предполагайте, что «чтение всего файла в память» происходит медленно. Вы оценили это? Вы знаете, что на самом деле считывает весь файл в память? Может быть, он просто возвращает прокси-объект и продолжает читать за кулисами, пока вы используете строку? ( Отказ от ответственности: я понятия не имею, действительно ли NSString делает это. Это вполне возможно. ) Суть в том, чтобы сначала пойти задокументированным способом ведения дел. Затем, если тесты показывают, что это не соответствует желаемой производительности, оптимизируйте.

5 голосов
/ 07 марта 2016

Многие из этих ответов представляют собой длинные фрагменты кода или они читаются во всем файле. Мне нравится использовать методы 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 не сохранит ваш символ новой строки. Кроме того, мы +1 длины строки, потому что мы хотим освободить место для завершения NULL.

3 голосов
/ 21 января 2014

Как уже отвечали другие, 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
3 голосов
/ 28 ноября 2013

Как сказал @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);
}
3 голосов
/ 17 января 2013

Строковое чтение файла (также для очень больших файлов) можно выполнить с помощью следующих функций:

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

Занятие провел Дейв Делонг

...