Cel-C: Czytanie pliku wiersz po wierszu


140

Jaki jest właściwy sposób postępowania z dużymi plikami tekstowymi w Objective-C? Powiedzmy, że muszę przeczytać każdą linię osobno i chcę traktować każdą linię jako NSString. Jaki jest najefektywniejszy sposób zrobienia tego?

Jednym z rozwiązań jest użycie metody NSString:

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

a następnie podziel linie za pomocą separatora nowej linii, a następnie wykonaj iterację po elementach tablicy. Wydaje się to jednak dość nieefektywne. Czy nie ma łatwego sposobu na traktowanie pliku jako strumienia, wyliczanie w każdym wierszu, zamiast po prostu wczytywać go od razu? Coś jak java.io.BufferedReader w Javie.


1
Trochę późno, ale sprawdź [NSScanner scanUpToString: @ "\ n" intoString: & read], zakładając, że chcesz wczytać każdy wiersz do ciągu 'read'.
hauntsaninja

Proszę spojrzeć na to podobne pytanie . Założyłem projekt, który zajmuje się czytaniem plików wiersz po wierszu .
JJD

Odpowiedzi:


63

To świetne pytanie. Myślę, że @Diederik ma dobrą odpowiedź, chociaż szkoda, że ​​Cocoa nie ma mechanizmu dokładnie tego, co chcesz zrobić.

NSInputStreampozwala czytać fragmenty N bajtów (bardzo podobne do java.io.BufferedReader), ale musisz samodzielnie przekonwertować je na a NSString, a następnie zeskanować w poszukiwaniu nowych linii (lub jakiegokolwiek innego separatora) i zapisać wszelkie pozostałe znaki do następnego odczytu lub przeczytać więcej znaków jeśli nowa linia nie została jeszcze odczytana. ( NSFileHandlepozwala przeczytać plik, NSDataktóry można następnie przekonwertować na plik NSString, ale jest to zasadniczo ten sam proces).

Firma Apple ma przewodnik programowania strumieniowego, który może pomóc wypełnić szczegóły, a to pytanie SO może również pomóc, jeśli masz do czynienia z uint8_t*buforami.

Jeśli masz zamiar często czytać napisy takie jak ten (szczególnie w różnych częściach programu), dobrym pomysłem byłoby umieszczenie tego zachowania w klasie, która może obsługiwać szczegóły za Ciebie lub nawet podklasę NSInputStream(jest zaprojektowana tak, aby była podklasy ) i dodawanie metod, które pozwalają czytać dokładnie to, co chcesz.

Dla przypomnienia, myślę, że byłoby to fajna funkcja do dodania i złożę prośbę o ulepszenie czegoś, co to umożliwia. :-)


Edycja: okazuje się, że to żądanie już istnieje. Jest do tego Radar pochodzący z 2006 roku (rdar: // 4742914 dla pracowników Apple).


10
Zobacz kompleksowe podejście Dave'a DeLonga do tego problemu tutaj: stackoverflow.com/questions/3707427#3711079
Quinn Taylor

Możliwe jest również użycie zwykłego NSData i mapowania pamięci. Stworzyłem odpowiedź z przykładowym kodem, który ma to samo API, co implementacja NSFileHandle Dave'a DeLonga: stackoverflow.com/a/21267461/267043
Bjørn Olav Ruud

95

To zadziała dla ogólnego czytania Stringz Text. Jeśli chcesz przeczytać dłuższy tekst (duży rozmiar tekstu) , użyj metody, o której wspomniały inne osoby, np. Buforowana (zarezerwuj rozmiar tekstu w pamięci) .

Powiedzmy, że czytasz plik tekstowy.

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

Chcesz pozbyć się nowej linii.

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

Masz to.


17
Mam plik 70 MB, używanie tego kodu do odczytu pliku nie pomaga mi, zwiększa pamięć liniowo. czy ktoś może mi pomóc?
GameLoading

37
To nie jest odpowiedź na pytanie.
Chodziło o

34

To powinno załatwić sprawę:

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

Użyj w następujący sposób:

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

Ten kod odczytuje z pliku znaki inne niż znaki nowej linii, do 4095 na raz. Jeśli masz linię dłuższą niż 4095 znaków, odczytuje ona dalej, aż trafi w nową linię lub koniec pliku.

Uwaga : nie testowałem tego kodu. Przetestuj go przed użyciem.


1
po prostu zmień [wynik appendFormat: "% s", bufor]; to [result appendFormat: @ "% s", buffer];
Codezy,

1
jak zmodyfikowałbyś format, aby akceptował puste wiersze, czy raczej wiersze składające się z pojedynczego znaku nowej linii?
jakev

To kończy się dla mnie wcześnie po 812 liniach. 812 wiersz to „... 3 więcej”, co powoduje, że czytnik wyświetla puste łańcuchy.
sudo

1
Dodałem czek, aby przejść przez puste wiersze: int fscanResult = fscanf (plik, "% 4095 [^ \ n]% n% * c", buffer, & charsRead); if (fscanResult == 1) {[result appendFormat: @ "% s", buffer]; } else {if (feof (plik)) {przerwa; } else if (ferror (plik)! = 0) {przerwa; } fscanf (plik, "\ n", nil, & charsRead); przerwa; }
Go Rose-Hulman

1
Jeśli dobrze czytam dokumentację fscanf, "%4095[^\n]%n%*c"po cichu pochłonie i wyrzuci jeden znak przy każdym odczytanym buforze. Wygląda na to, że ten format zakłada, że ​​linie będą krótsze niż długość bufora.
Blago

12

Mac OS X to Unix, Objective-C to superset w C, więc możesz po prostu używać old-school fopeni fgetsfrom <stdio.h>. Gwarantujemy, że zadziała.

[NSString stringWithUTF8String:buf]przekonwertuje ciąg C na NSString. Istnieją również metody tworzenia łańcuchów w innych kodowaniach i tworzenia bez kopiowania.


[kopiowanie anonimowego komentarza] fgetsbędzie zawierało '\n'znak, więc możesz chcieć go usunąć przed konwersją ciągu.
Kornel

9

Możesz użyć, NSInputStreamktóry ma podstawową implementację dla strumieni plików. Możesz wczytać bajty do bufora ( read:maxLength:metoda). Musisz samodzielnie przeskanować bufor w poszukiwaniu nowych linii.


6

Odpowiedni sposób czytania plików tekstowych w Cocoa / Objective-C jest udokumentowany w przewodniku programowania ciągów znaków firmy Apple. Sekcja dotycząca czytania i pisania plików powinna być właśnie tym, czego szukasz. PS: Co to jest „linia”? Dwie sekcje ciągu oddzielone znakiem „\ n”? Lub „\ r”? Lub „\ r \ n”? A może faktycznie po akapitach? Wspomniany przewodnik zawiera również sekcję dotyczącą dzielenia ciągu znaków na wiersze lub akapity. (Ta sekcja nazywa się „Akapity i podziały wierszy” i jest połączona linkiem w menu po lewej stronie wskazanej powyżej strony. Niestety ta witryna nie pozwala mi na umieszczenie więcej niż jednego adresu URL, ponieważ nie jest jeszcze godnym zaufania użytkownikiem).

Parafrazując Knutha: przedwczesna optymalizacja jest źródłem wszelkiego zła. Nie zakładaj po prostu, że „wczytywanie całego pliku do pamięci” jest powolne. Czy sprawdziłeś to? Czy wiesz, że faktycznie czyta cały plik do pamięci? Może po prostu zwraca obiekt proxy i czyta za kulisami, gdy konsumujesz ciąg? ( Zastrzeżenie: nie mam pojęcia, czy NSString faktycznie to robi. Możliwe, że mógłby. ) Chodzi o to: najpierw idź z udokumentowanym sposobem robienia rzeczy. Następnie, jeśli testy porównawcze pokazują, że nie zapewnia to oczekiwanej wydajności, zoptymalizuj.


Ponieważ wspomniałeś o zakończeniach linii CRLF (Windows): to właściwie przypadek, który łamie sposób działania Objective-C. Jeśli użyjesz jednej z -stringWithContentsOf*metod, po których następuje -componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet], widzi ona \ri \noddzielnie i dodaje pusty wiersz po każdym wierszu.
Siobhán

To powiedziawszy, rozwiązanie fgets nie działa w przypadku plików tylko CR. Ale są to (teoretycznie) obecnie rzadkie, a fgets działa zarówno dla LF, jak i CRLF.
Siobhán

6

Wiele z tych odpowiedzi to długie fragmenty kodu lub czytane są w całym pliku. Lubię używać metod c do tego właśnie zadania.

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

Zauważ, że fgetln nie zachowa twojego znaku nowej linii. Ponadto dajemy +1 długości str, ponieważ chcemy zrobić miejsce na zakończenie NULL.


4

Aby odczytać plik wiersz po wierszu (również w przypadku bardzo dużych plików), można skorzystać z następujących funkcji:

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

Lub:

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

Klasa DDFileReader, która to umożliwia, jest następująca:

Plik interfejsu (.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

Wdrożenie (.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

Klasa została wykonana przez Dave DeLong


4

Tak jak powiedział @porneL, interfejs API C jest bardzo przydatny.

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

4

Jak inni odpowiedzieli, zarówno NSInputStream, jak i NSFileHandle są dobrymi opcjami, ale można to również zrobić w dość zwarty sposób za pomocą NSData i mapowania pamięci:

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

1

Ta odpowiedź NIE JEST ObjC, ale C.

Skoro ObjC jest oparty na „C”, dlaczego nie użyć fgets?

I tak, jestem pewien, że ObjC ma swoją własną metodę - po prostu nie jestem jeszcze wystarczająco biegły, aby wiedzieć, co to jest :)


5
Jeśli nie wiesz, jak to zrobić w Objective-C, to dlaczego mówisz, że to nie jest odpowiedź? Jest wiele powodów, aby nie schodzić do prostego C, jeśli możesz to zrobić w inny sposób. Na przykład funkcje w C obsługują char *, ale odczytanie czegoś innego wymaga dużo więcej pracy, na przykład różnych kodowań. Chce też obiekty NSString. Podsumowując, samodzielne zrobienie tego to nie tylko więcej kodu, ale także podatne na błędy.
Quinn Taylor

3
Zgadzam się z tobą w 100%, ale odkryłem, że (czasami) lepiej jest uzyskać odpowiedź, która działa szybko, wdrożyć ją, a kiedy pojawi się bardziej poprawna alternatywa, wykorzystać ją. Jest to szczególnie ważne podczas tworzenia prototypów, dając możliwość uzyskania czegoś do pracy, a następnie postępu.
KevinDTimm

3
Właśnie zdałem sobie sprawę, że zaczynało się „Ta odpowiedź”, a nie „Odpowiedź”. No! Zgadzam się, zdecydowanie lepiej mieć hack, który działa, niż elegancki kod, który nie działa. Nie przegłosowałem cię, ale wyrzucanie domysłów bez wiedzy, co może mieć Objective-C, prawdopodobnie też nie jest zbyt pomocne. Mimo to wysiłek jest zawsze lepszy niż ktoś, kto wie i nie pomaga ... ;-)
Quinn Taylor

To nie daje odpowiedzi na pytanie. Aby skrytykować lub poprosić autora o wyjaśnienie, zostaw komentarz pod jego postem.
Robotic Cat,

1
@KevinDTimm: Zgadzam się; Przykro mi tylko, że nie zauważyłem, że to odpowiedź od 5 lat. Może to jest metapytanie; czy bardzo stare pytania od zwykłych użytkowników można oznaczyć do przejrzenia?
Robotic Cat,

0

z odpowiedzi @Adama Rosenfielda, ciąg formatujący fscanfzostałby zmieniony jak poniżej:

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

będzie działać na końcówkach linii OSX, Linux, Windows.


0

Korzystanie z kategorii lub rozszerzenia, aby ułatwić sobie życie.

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
}

0

Odpowiedź od @lukaswelte i kod od Dave'a DeLonga okazały się bardzo pomocne. Szukałem rozwiązania tego problemu, ale musiałem analizować duże pliki \r\nnie tylko \n.

Napisany kod zawiera błąd w przypadku analizowania przez więcej niż jeden znak. Zmieniłem kod jak poniżej.

plik .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

plik .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

0

Dodam to, ponieważ wszystkie inne odpowiedzi, których próbowałem, były w ten czy inny sposób krótkie. Poniższa metoda może obsługiwać duże pliki, dowolne długie wiersze, a także puste wiersze. Został przetestowany z rzeczywistą zawartością i usunie znak nowej linii z wyniku.

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

Podziękowania dla @Adam Rosenfield i @sooop


0

Widzę, że wiele z tych odpowiedzi polega na wczytywaniu całego pliku tekstowego do pamięci, zamiast brać go po jednym kawałku na raz. Oto moje rozwiązanie w ładnym, nowoczesnym języku Swift, przy użyciu FileHandle, aby utrzymać niski wpływ pamięci:

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
    }

}

Zauważ, że zachowuje to powrót karetki na końcu wiersza, więc w zależności od potrzeb możesz dostosować kod, aby go usunąć.

Sposób użycia: po prostu otwórz uchwyt pliku do docelowego pliku tekstowego i wywołaj readLinez odpowiednią maksymalną długością - 1024 jest standardem dla zwykłego tekstu, ale zostawiłem go otwartego na wypadek, gdybyś wiedział, że będzie krótszy. Zwróć uwagę, że polecenie nie spowoduje przepełnienia końca pliku, więc jeśli zamierzasz przeanalizować całość, może być konieczne ręczne sprawdzenie, czy go nie osiągnąłeś. Oto przykładowy kod, który pokazuje, jak otworzyć plik pod adresem myFileURLi czytać go wiersz po wierszu aż do końca.

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

-2

Oto ładne, proste rozwiązanie, którego używam do mniejszych plików:

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

Pytał, jak czytać wiersz na raz, aby nie wczytywał całej zawartości do pamięci. Twoje rozwiązanie tworzy ciąg z całą zawartością, a następnie dzieli go na linie.
David,

-7

Użyj tego skryptu, działa świetnie:

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

1
@Fisninear mówi, że nie dotyczy to dążenia OP do zmniejszenia zużycia pamięci. OP nie pytał, jak użyć metody (która ładuje cały plik do pamięci), prosił o przyjazne dla pamięci alternatywy dla dużych plików tekstowych. Całkiem możliwe jest posiadanie wielogigabajtowych plików tekstowych, co oczywiście stwarza problem z pamięcią.
Joshua Nozzi
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.