Domanda

Qual è il modo appropriato di trattare file di testo di grandi dimensioni in Objective-C? Diciamo che ho bisogno di leggere ogni riga separatamente e voglio trattare ogni riga come una NSString. Qual è il modo più efficace per farlo?

Una soluzione sta usando il metodo NSString:

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

e quindi dividere le linee con un separatore di nuova riga, quindi scorrere gli elementi nell'array. Tuttavia, questo sembra abbastanza inefficiente. Non esiste un modo semplice per trattare il file come un flusso, enumerandolo su ogni riga, invece di leggerlo tutto in una volta? Un po 'come Java java.io.BufferedReader.

È stato utile?

Soluzione

Questa è un'ottima domanda. Penso che @Diederik abbia una buona risposta, anche se è un peccato che Cocoa non abbia un meccanismo esattamente per quello che vuoi fare.

NSInputStream ti permette di leggere blocchi di N byte (molto simili a java.io.BufferedReader), ma devi convertirlo in un NSString da solo, quindi cercare nuovi caratteri (o qualsiasi altro delimitatore) e salvare eventuali caratteri rimanenti per la lettura successiva o leggere più caratteri se una nuova riga non è stata ancora letta. ( NSFileHandle ti permette di leggere un NSData che puoi quindi convertire in uint8_t*, ma essenzialmente è lo stesso processo.)

Apple ha una Guida alla programmazione di stream che può aiutarti a riempire nei dettagli e questa domanda SO può essere d'aiuto se hai a che fare con <=> buffer.

Se stai leggendo stringhe come questa frequentemente (specialmente in diverse parti del tuo programma), sarebbe una buona idea incapsulare questo comportamento in una classe che può gestire i dettagli per te, o anche sottoclassare <= > (è progettato per essere suddiviso in sottoclassi ) e con l'aggiunta di metodi che ti consentono di leggere esattamente ciò che desideri.

Per la cronaca, penso che questa sarebbe una bella caratteristica da aggiungere, e presenterò una richiesta di miglioramento per qualcosa che lo rende possibile. : -)


Modifica: Risulta che questa richiesta esiste già. C'è un radar risalente al 2006 per questo (rdar: // 4742914 per le persone interne ad Apple).

Altri suggerimenti

Questo funzionerà per la lettura generale di String da Text. Se desideri leggere un testo più lungo (grande dimensione del testo) , usa il metodo che è stato menzionato qui altre persone come buffer (riserva la dimensione del testo nello spazio di memoria) .

Supponi di leggere un file di testo.

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

Vuoi sbarazzarti della nuova linea.

// 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:@";"]];

Eccolo qui.

Questo dovrebbe fare il trucco:

#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;
}

Utilizzare come segue:

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);

Questo codice legge i caratteri non newline dal file, fino a 4095 alla volta. Se hai una linea più lunga di 4095 caratteri, continua a leggere fino a quando non raggiunge una nuova riga o fine del file.

Nota : non ho testato questo codice. Provalo prima di usarlo.

Mac OS X è Unix, Objective-C è superset C, quindi puoi semplicemente usare la vecchia scuola fopen e fgets da <stdio.h>. È garantito per funzionare.

[NSString stringWithUTF8String:buf] convertirà la stringa C in NSString. Esistono anche metodi per creare stringhe in altre codifiche e creare senza copiare.

Puoi usare NSInputStream che ha un'implementazione di base per i flussi di file. È possibile leggere i byte in un buffer (metodo read:maxLength:). Devi scansionare tu stesso il buffer per le nuove righe.

Il modo appropriato di leggere i file di testo in Cocoa / Objective-C è documentato nella guida alla programmazione delle stringhe di Apple. La sezione per leggere e scrivere file dovrebbe essere proprio ciò che stai cercando. PS: Che cos'è un & Quot; linea & Quot ;? Due sezioni di una stringa separate da & Quot; \ n & Quot ;? Oppure & Quot; \ r & Quot ;? Oppure & Quot; \ r \ n & Quot ;? O forse stai effettivamente seguendo i paragrafi? La guida precedentemente menzionata include anche una sezione sulla divisione di una stringa in righe o paragrafi. (Questa sezione è chiamata & Quot; Paragrafi e interruzioni di riga & Quot ;, ed è collegata nel menu di sinistra della pagina che ho indicato sopra. Purtroppo questo sito non mi consente di pubblicare più di un URL in quanto non sono ancora un utente affidabile.)

Per parafrasare Knuth: l'ottimizzazione prematura è la radice di tutti i mali. Non dare per scontato che & Quot; leggere l'intero file in memoria & Quot; è lento. L'hai confrontato? Sai che in realtà legge l'intero file in memoria? Forse restituisce semplicemente un oggetto proxy e continua a leggere dietro le quinte mentre consumi la stringa? ( Dichiarazione di non responsabilità: non ho idea se NSString lo faccia davvero. Probabilmente potrebbe. ) Il punto è: prima di tutto segui il modo documentato di fare le cose. Quindi, se i benchmark mostrano che questo non ha le prestazioni che desideri, ottimizza.

Molte di queste risposte sono lunghe porzioni di codice o vengono lette nell'intero file. Mi piace usare i metodi c proprio per questo compito.

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);
}

Nota che fgetln non manterrà il tuo personaggio newline. Inoltre, facciamo +1 sulla lunghezza della str perché vogliamo fare spazio per la terminazione NULL.

Per leggere un file riga per riga (anche per file di dimensioni estreme) è possibile eseguire le seguenti funzioni:

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

o

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

La classe DDFileReader che abilita ciò è la seguente:

File di interfaccia (.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

Implementazione (.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

La lezione è stata tenuta da Dave DeLong

Proprio come ha detto @porneL, l'API C è molto utile.

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);
}

Come altri hanno risposto, sia NSInputStream che NSFileHandle sono ottime opzioni, ma può anche essere fatto in modo abbastanza compatto con NSData e mappatura della memoria:

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

Questa risposta NON è ObjC ma C.

Poiché ObjC è basato su "C", perché non utilizzare i budget?

E sì, sono sicuro che ObjC ha il suo metodo - non sono ancora abbastanza esperto per sapere di cosa si tratta :)

dalla risposta di @Adam Rosenfield, la stringa di formattazione di fscanf verrebbe modificata come di seguito:

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

funzionerà in osx, linux, terminazioni di linea di Windows.

Uso della categoria o dell'estensione per semplificarci un po 'la vita.

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
}

Ho trovato molto utile la risposta di @lukaswelte e il codice di Dave DeLong . Stavo cercando una soluzione a questo problema, ma avevo bisogno di analizzare file di grandi dimensioni \r\n non solo \n.

Il codice come scritto contiene un bug se viene analizzato da più di un carattere. Ho modificato il codice come di seguito.

.h file:

#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 file:

#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

Lo sto aggiungendo perché tutte le altre risposte che ho provato sono state insufficienti in un modo o nell'altro. Il seguente metodo può gestire file di grandi dimensioni, righe lunghe arbitrarie e righe vuote. È stato testato con contenuti reali e eliminerà il carattere di nuova riga dall'output.

- (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;
}

Il merito va a @Adam Rosenfield e @sooop

Ecco una bella soluzione semplice che uso per file più piccoli:

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);
    }
}

Usa questo script, funziona benissimo:

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);
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top