Ziel-C: Zeile für Zeile eine Datei lesen

140

Was ist der geeignete Umgang mit großen Textdateien in Objective-C? Angenommen, ich muss jede Zeile einzeln lesen und möchte jede Zeile als NSString behandeln. Was ist der effizienteste Weg, dies zu tun?

Eine Lösung ist die Verwendung der NSString-Methode:

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

Teilen Sie dann die Zeilen mit einem Zeilenumbruchtrennzeichen und iterieren Sie dann über die Elemente im Array. Dies scheint jedoch ziemlich ineffizient zu sein. Gibt es keine einfache Möglichkeit, die Datei als Stream zu behandeln, der über jede Zeile aufgezählt wird, anstatt alles auf einmal einzulesen? Ein bisschen wie Javas java.io.BufferedReader.

Quinn Taylor
quelle
1
Ein bisschen spät, aber sehen Sie sich [NSScanner scanUpToString: @ "\ n" intoString: & read] an, vorausgesetzt, Sie möchten jede Zeile in die Zeichenfolge 'read' einlesen.
Hauntsaninja
Bitte schauen Sie sich diese ähnliche Frage an . Ich habe ein Projekt eingerichtet, das sich mit dem zeilenweisen Lesen von Dateien befasst .
JJD

Antworten:

63

Das ist eine gute Frage. Ich denke, @Diederik hat eine gute Antwort, obwohl es bedauerlich ist, dass Cocoa keinen Mechanismus für genau das hat, was Sie tun möchten.

NSInputStreamErmöglicht das Lesen von Teilen von N Bytes (sehr ähnlich zu java.io.BufferedReader), aber Sie müssen diese selbst in eine konvertieren NSString, dann nach Zeilenumbrüchen (oder einem anderen Trennzeichen) suchen und alle verbleibenden Zeichen für den nächsten Lesevorgang speichern oder weitere Zeichen lesen wenn noch kein Zeilenumbruch gelesen wurde. ( NSFileHandleSie können eine lesen, NSDatadie Sie dann in eine konvertieren können NSString, aber es ist im Wesentlichen der gleiche Vorgang.)

Apple verfügt über ein Stream-Programmierhandbuch , mit dessen Hilfe Sie die Details ergänzen können. Diese SO-Frage kann auch hilfreich sein, wenn Sie sich mit uint8_t*Puffern befassen .

Wenn Sie solche Zeichenfolgen häufig lesen (insbesondere in verschiedenen Teilen Ihres Programms), ist es eine gute Idee, dieses Verhalten in einer Klasse zu kapseln, die die Details für Sie verarbeiten kann, oder sogar in Unterklassen NSInputStream(wie es entworfen wurde) Unterklasse ) und Hinzufügen von Methoden, mit denen Sie genau lesen können, was Sie wollen.

Für die Aufzeichnung denke ich, dass dies eine nette Funktion wäre, um hinzuzufügen, und ich werde eine Verbesserungsanfrage für etwas einreichen, das dies möglich macht. :-)


Bearbeiten: Es stellt sich heraus, dass diese Anforderung bereits vorhanden ist. Hierfür gibt es ein Radar aus dem Jahr 2006 (rdar: // 4742914 für Apple-interne Personen).

Quinn Taylor
quelle
10
Sehen Sie Dave DeLongs umfassende Herangehensweise an dieses Problem hier: stackoverflow.com/questions/3707427#3711079
Quinn Taylor
Es ist auch möglich, einfache NSData- und Speicherzuordnungen zu verwenden. Ich habe eine Antwort mit Beispielcode erstellt, die dieselbe API wie die NSFileHandle-Implementierung von Dave DeLong hat: stackoverflow.com/a/21267461/267043
Bjørn Olav Ruud
95

Dies funktioniert für das allgemeine Lesen eines Stringvon Text. Wenn Sie längeren Text lesen möchten (große Textgröße) , verwenden Sie die Methode, die andere Personen hier erwähnt haben, z. B. gepuffert (reservieren Sie die Größe des Textes im Speicherplatz) .

Angenommen, Sie lesen eine Textdatei.

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

Sie möchten neue Linie loswerden.

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

Hier hast du es.

Yoon Lee
quelle
17
Ich habe eine 70-MB-Datei. Wenn ich diesen Code zum Lesen einer Datei verwende, kann ich den Speicher nicht linear erhöhen. Kann mir jemand helfen?
GameLoading
37
Dies ist keine Antwort auf die Frage. Die Frage war, eine Datei Zeile für Zeile zu lesen, um die Speichernutzung zu reduzieren
doozMen
34

Dies sollte den Trick tun:

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

Verwenden Sie wie folgt:

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

Dieser Code liest Nicht-Zeilenumbrüche aus der Datei, bis zu 4095 gleichzeitig. Wenn Sie eine Zeile haben, die länger als 4095 Zeichen ist, wird sie so lange gelesen, bis sie auf eine neue Zeile oder ein Dateiende trifft.

Hinweis : Ich habe diesen Code nicht getestet. Bitte testen Sie es, bevor Sie es verwenden.

Adam Rosenfield
quelle
1
ändern Sie einfach [Ergebnis appendFormat: "% s", Puffer]; zu [Ergebnis appendFormat: @ "% s", Puffer];
Codezy
1
Wie würden Sie das Format ändern, um leere Zeilen oder Zeilen, die aus einem einzelnen Zeilenumbruchzeichen bestehen, zu akzeptieren?
Jakev
Dies hört für mich nach 812 Zeilen früh auf. Die 812. Zeile lautet "... 3 weitere", wodurch der Leser leere Zeichenfolgen ausgibt.
Sudo
1
Ich habe eine Prüfung hinzugefügt, um die leeren Zeilen zu überwinden: int fscanResult = fscanf (Datei "% 4095 [^ \ n]% n% * c", Puffer & charsRead); if (fscanResult == 1) {[Ergebnis appendFormat: @ "% s", Puffer]; } else {if (feof (Datei)) {break; } else if (ferror (file)! = 0) {break; } fscanf (Datei, "\ n", nil & charsRead); brechen; }
Gehen Sie Rose-Hulman
1
Wenn ich die fscanf-Dokumentation richtig lese, "%4095[^\n]%n%*c"wird bei jedem gelesenen Puffer stillschweigend ein Zeichen verbraucht und weggeworfen . Es sieht so aus, als würde dieses Format davon ausgehen, dass die Zeilen kürzer als die Pufferlänge sind.
Blago
12

Mac OS X ist Unix, Objective-C ist C-Obermenge, sodass Sie nur Old-School fopenund fgetsvon verwenden können <stdio.h>. Es wird garantiert funktionieren.

[NSString stringWithUTF8String:buf]konvertiert C-String in NSString. Es gibt auch Methoden zum Erstellen von Zeichenfolgen in anderen Codierungen und zum Erstellen ohne Kopieren.

Kornel
quelle
[Kopieren eines anonymen Kommentars] fgetsenthält das '\n'Zeichen. Sie können es daher entfernen, bevor Sie die Zeichenfolge konvertieren.
Kornel
9

Sie können NSInputStreameine grundlegende Implementierung für Dateistreams verwenden. Sie können Bytes in einen Puffer ( read:maxLength:Methode) einlesen . Sie müssen den Puffer selbst nach Zeilenumbrüchen durchsuchen.

diederikh
quelle
6

Die geeignete Methode zum Lesen von Textdateien in Cocoa / Objective-C ist im Apple-Programmierhandbuch für Zeichenfolgen dokumentiert. Der Abschnitt zum Lesen und Schreiben von Dateien sollte genau das sein, wonach Sie suchen. PS: Was ist eine "Linie"? Zwei durch "\ n" getrennte Abschnitte einer Zeichenfolge? Oder "\ r"? Oder "\ r \ n"? Oder vielleicht bist du tatsächlich hinter Absätzen her? Die zuvor erwähnte Anleitung enthält auch einen Abschnitt zum Aufteilen einer Zeichenfolge in Zeilen oder Absätze. (Dieser Abschnitt heißt "Absätze und Zeilenumbrüche" und ist im Menü auf der linken Seite der Seite, auf die ich oben verwiesen habe, verlinkt. Leider kann ich auf dieser Website nicht mehr als eine URL veröffentlichen, wie ich bin noch kein vertrauenswürdiger Benutzer.)

Um Knuth zu paraphrasieren: Vorzeitige Optimierung ist die Wurzel allen Übels. Gehen Sie nicht einfach davon aus, dass das "Einlesen der gesamten Datei in den Speicher" langsam ist. Haben Sie es bewertet? Wissen Sie, dass es tatsächlich die gesamte Datei in den Speicher liest? Vielleicht gibt es einfach ein Proxy-Objekt zurück und liest weiter hinter den Kulissen, während Sie die Zeichenfolge verbrauchen? ( Haftungsausschluss: Ich habe keine Ahnung, ob NSString dies tatsächlich tut. Es ist denkbar, dass dies möglich ist. ) Der Punkt ist: Gehen Sie zuerst mit der dokumentierten Vorgehensweise vor. Wenn Benchmarks zeigen, dass dies nicht die gewünschte Leistung bietet, optimieren Sie.

Stig Brautaset
quelle
Da Sie CRLF (Windows) -Zeilenenden erwähnen: Dies ist tatsächlich ein Fall, der die Objective-C-Vorgehensweise verletzt. Wenn Sie eine der folgenden -stringWithContentsOf*Methoden verwenden -componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet], wird das \rund \nseparat angezeigt und nach jeder Zeile wird eine Leerzeile hinzugefügt.
Siobhán
Die fgets-Lösung schlägt jedoch bei Nur-CR-Dateien fehl. Aber diese sind heutzutage (theoretisch) selten und fgets funktioniert sowohl für LF als auch für CRLF.
Siobhán
6

Viele dieser Antworten sind lange Codestücke oder sie lesen die gesamte Datei ein. Ich verwende gerne die c-Methoden für genau diese Aufgabe.

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

Beachten Sie, dass fgetln Ihren Zeilenumbruch nicht beibehält. Außerdem +1 die Länge der str, weil wir Platz für die NULL-Terminierung schaffen wollen.

DCurro
quelle
4

Das zeilenweise Lesen einer Datei (auch für extrem große Dateien) kann mit den folgenden Funktionen erfolgen:

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

Oder:

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

Die Klasse DDFileReader, die dies ermöglicht, ist die folgende:

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

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

Der Unterricht wurde von Dave DeLong durchgeführt

lukaswelte
quelle
4

Genau wie @porneL sagte, ist die C api sehr praktisch.

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);
}
wdanxna
quelle
4

Wie andere geantwortet haben, sind sowohl NSInputStream als auch NSFileHandle gute Optionen, aber es kann auch auf ziemlich kompakte Weise mit NSData und Speicherzuordnung durchgeführt werden:

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
Bjørn Olav Ruud
quelle
1

Diese Antwort lautet NICHT ObjC, sondern C.

Warum nicht fgets verwenden, da ObjC auf C basiert?

Und ja, ich bin sicher, ObjC hat seine eigene Methode - ich bin einfach noch nicht kompetent genug, um zu wissen, was es ist :)

KevinDTimm
quelle
5
Wenn Sie nicht wissen, wie es in Objective-C geht, warum dann sagen Sie, dass es nicht die Antwort ist? Es gibt viele Gründe, nicht auf Straight C zu fallen, wenn Sie es anders machen können. Zum Beispiel verarbeiten C-Funktionen char *, aber es erfordert viel mehr Arbeit, etwas anderes zu lesen, z. B. verschiedene Codierungen. Außerdem möchte er NSString-Objekte. Alles in allem ist das Rollen selbst nicht nur mehr Code, sondern auch fehleranfällig.
Quinn Taylor
3
Ich stimme Ihnen zu 100% zu, aber ich habe festgestellt, dass es (manchmal) besser ist, eine Antwort zu erhalten, die schnell funktioniert, sie umzusetzen und diese dann zu nutzen, wenn eine korrektere Alternative erscheint. Dies ist besonders wichtig beim Prototyping, um die Möglichkeit zu geben, etwas zum Laufen zu bringen und von dort aus weiterzukommen.
KevinDTimm
3
Ich habe gerade festgestellt, dass es "Diese Antwort" begann, nicht "Die Antwort". Doh! Ich stimme zu, es ist definitiv besser, einen Hack zu haben, der funktioniert, als eleganten Code, der nicht funktioniert. Ich habe Sie nicht abgelehnt, aber eine Vermutung abzugeben, ohne zu wissen, was Objective-C möglicherweise hat, ist auch nicht sehr hilfreich. Trotzdem ist es immer besser, sich anzustrengen als jemand, der weiß und nicht hilft ... ;-)
Quinn Taylor
Dies gibt keine Antwort auf die Frage. Um einen Autor zu kritisieren oder um Klärung zu bitten, hinterlassen Sie einen Kommentar unter seinem Beitrag.
Roboter-Katze
1
@ KevinDTimm: Ich stimme zu; Es tut mir nur leid, dass ich nicht bemerkt habe, dass es eine 5 Jahre alte Antwort war. Vielleicht ist das eine metaFrage; Sollten sehr alte Fragen von regulären Benutzern zur Überprüfung markiert werden können?
Robotic Cat
0

Aus der Antwort von @Adam Rosenfield geht hervor, dass die Formatierungszeichenfolge von fscanfwie folgt geändert wird:

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

Es funktioniert in OSX-, Linux- und Windows-Zeilenenden.

sooop
quelle
0

Verwenden von Kategorien oder Erweiterungen, um unser Leben ein bisschen einfacher zu machen.

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
}
Kaz Yoshikawa
quelle
0

Ich fand die Antwort von @lukaswelte und den Code von Dave DeLong sehr hilfreich. Ich suchte nach einer Lösung für dieses Problem, musste aber \r\nnicht nur große Dateien analysieren \n.

Der geschriebene Code enthält einen Fehler, wenn mehr als ein Zeichen analysiert wird. Ich habe den Code wie folgt geändert.

.h Datei:

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

#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
hovey
quelle
0

Ich füge dies hinzu, weil alle anderen Antworten, die ich versucht habe, auf die eine oder andere Weise fehlgeschlagen sind. Die folgende Methode kann große Dateien, beliebig lange Zeilen sowie leere Zeilen verarbeiten. Es wurde mit tatsächlichem Inhalt getestet und entfernt Zeilenumbrüche aus der Ausgabe.

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

Gutschrift geht an @Adam Rosenfield und @sooop

Blago
quelle
0

Ich sehe, dass viele dieser Antworten darauf beruhen, die gesamte Textdatei in den Speicher zu lesen, anstatt sie einzeln zu belegen. Hier ist meine Lösung in nettem modernem Swift, bei der FileHandle verwendet wird, um die Auswirkungen auf den Speicher gering zu halten:

enum MyError {
    case invalidTextFormat
}

extension FileHandle {

    func readLine(maxLength: Int) throws -> String {

        // Read in a string of up to the maximum length
        let offset = offsetInFile
        let data = readData(ofLength: maxLength)
        guard let string = String(data: data, encoding: .utf8) else {
            throw MyError.invalidTextFormat
        }

        // Check for carriage returns; if none, this is the whole string
        let substring: String
        if let subindex = string.firstIndex(of: "\n") {
            substring = String(string[string.startIndex ... subindex])
        } else {
            substring = string
        }

        // Wind back to the correct offset so that we don't miss any lines
        guard let dataCount = substring.data(using: .utf8, allowLossyConversion: false)?.count else {
            throw MyError.invalidTextFormat
        }
        try seek(toOffset: offset + UInt64(dataCount))
        return substring
    }

}

Beachten Sie, dass dadurch der Wagenrücklauf am Ende der Zeile erhalten bleibt. Abhängig von Ihren Anforderungen möchten Sie möglicherweise den Code anpassen, um ihn zu entfernen.

Verwendung: Öffnen Sie einfach ein Dateihandle für Ihre Zieltextdatei und rufen Sie readLinemit einer geeigneten maximalen Länge auf - 1024 ist Standard für Klartext, aber ich habe es offen gelassen, falls Sie wissen, dass es kürzer sein wird. Beachten Sie, dass der Befehl das Ende der Datei nicht überläuft. Daher müssen Sie möglicherweise manuell überprüfen, ob Sie ihn nicht erreicht haben, wenn Sie das gesamte Objekt analysieren möchten. Hier ist ein Beispielcode, der zeigt, wie Sie eine Datei unter öffnen myFileURLund bis zum Ende Zeile für Zeile lesen können.

do {
    let handle = try FileHandle(forReadingFrom: myFileURL)
    try handle.seekToEndOfFile()
    let eof = handle.offsetInFile
    try handle.seek(toFileOffset: 0)

    while handle.offsetInFile < eof {
        let line = try handle.readLine(maxLength: 1024)
        // Do something with the string here
    }
    try handle.close()
catch let error {
    print("Error reading file: \(error.localizedDescription)"
}
Asche
quelle
-2

Hier ist eine schöne einfache Lösung, die ich für kleinere Dateien verwende:

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);
    }
}
Chris
quelle
Er fragte, wie man eine Zeile gleichzeitig liest, damit nicht der gesamte Inhalt in den Speicher eingelesen wird. Ihre Lösung erstellt eine Zeichenfolge mit dem gesamten Inhalt und teilt sie dann in Zeilen auf.
David
-7

Verwenden Sie dieses Skript, es funktioniert großartig:

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);
abhi
quelle
1
Was @fisninear sagt, ist, dass dies nicht dem Wunsch des OP entspricht, die Speichernutzung zu reduzieren. Das OP fragte nicht nach der Verwendung der Methode (die die gesamte Datei in den Speicher lädt), sondern nach speicherfreundlichen Alternativen für große Textdateien. Es ist durchaus möglich, Textdateien mit mehreren Gigabyte zu haben, was offensichtlich zu einem Speicherproblem führt.
Joshua Nozzi