Objective-C: Membaca file baris demi baris

140

Apa cara yang tepat untuk menangani file teks besar di Objective-C? Katakanlah saya perlu membaca setiap baris secara terpisah dan ingin memperlakukan setiap baris sebagai NSString. Apa cara paling efisien untuk melakukan ini?

Salah satu solusinya adalah menggunakan metode NSString:

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

dan kemudian membaginya dengan pemisah baris baru, dan kemudian iterate atas elemen dalam array. Namun, ini tampaknya tidak efisien. Apakah tidak ada cara mudah untuk memperlakukan file sebagai stream, menghitung setiap baris, alih-alih hanya membacanya sekaligus? Agak seperti java.io.BufferedReader Java.

Quinn Taylor
sumber
1
Agak terlambat, tetapi periksa [NSScanner scanUpToString: @ "\ n" keString: & baca], dengan asumsi Anda ingin membaca setiap baris ke dalam string 'baca'.
hauntsaninja
Silakan lihat pertanyaan serupa ini . Saya membuat proyek yang berkaitan dengan membaca file baris demi baris .
JJD

Jawaban:

63

Itu pertanyaan yang bagus. Saya pikir @Diederik memiliki jawaban yang bagus, meskipun sangat disayangkan bahwa Kakao tidak memiliki mekanisme untuk apa yang ingin Anda lakukan.

NSInputStreammemungkinkan Anda membaca potongan N byte (sangat mirip dengan java.io.BufferedReader), tetapi Anda harus mengonversinya menjadi NSStringAnda sendiri, kemudian memindai baris baru (atau pembatas lainnya) dan menyimpan karakter yang tersisa untuk dibaca selanjutnya, atau membaca lebih banyak karakter jika baris baru belum dibaca. ( NSFileHandlememungkinkan Anda membaca NSDatayang kemudian dapat dikonversi menjadi NSString, tetapi pada dasarnya prosesnya sama.)

Apple memiliki Panduan Pemrograman Stream yang dapat membantu mengisi detail, dan pertanyaan SO ini dapat membantu juga jika Anda akan berurusan dengan uint8_t*buffer.

Jika Anda akan sering membaca string seperti ini (terutama di berbagai bagian program Anda) adalah ide yang baik untuk merangkum perilaku ini di kelas yang dapat menangani detail untuk Anda, atau bahkan subklasifikasi NSInputStream(ini dirancang untuk menjadi subclass ) dan menambahkan metode yang memungkinkan Anda untuk membaca apa yang Anda inginkan.

Sebagai catatan, saya pikir ini akan menjadi fitur yang bagus untuk ditambahkan, dan saya akan mengajukan permintaan tambahan untuk sesuatu yang memungkinkan ini terjadi. :-)


Sunting: Ternyata permintaan ini sudah ada. Ada Radar yang berasal dari tahun 2006 untuk ini (rdar: // 4742914 untuk orang-orang internal Apple).

Quinn Taylor
sumber
10
Lihat pendekatan komprehensif Dave DeLong untuk masalah ini di sini: stackoverflow.com/questions/3707427#3711079
Quinn Taylor
Dimungkinkan juga untuk menggunakan NSData dan pemetaan memori. Saya telah membuat jawaban dengan kode contoh yang memiliki API yang sama dengan implementasi NSFileHandle Dave DeLong: stackoverflow.com/a/21267461/267043
Bjørn Olav Ruud
95

Ini akan berfungsi untuk membaca secara umum Stringdari Text. Jika Anda ingin membaca teks yang lebih panjang ( teks berukuran besar) , maka gunakan metode yang disebut orang lain di sini seperti buffered (cadangan ukuran teks dalam ruang memori) .

Katakanlah Anda membaca File Teks.

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

Anda ingin menyingkirkan baris baru.

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

Itu dia.

Yoon Lee
sumber
17
saya punya file 70 mb, menggunakan kode ini untuk membaca file tidak hep saya meningkatkan memori secara linear. ada yang bisa bantu saya?
GameLoading
37
Ini bukan jawaban untuk pertanyaan itu. Pertanyaannya adalah membaca file baris demi baris untuk mengurangi penggunaan memori
doozMen
34

Ini harus melakukan trik:

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

Gunakan sebagai berikut:

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

Kode ini membaca karakter non-baris baru dari file, hingga 4095 sekaligus. Jika Anda memiliki garis yang lebih panjang dari 4095 karakter, ia terus membaca hingga mencapai baris baru atau akhir file.

Catatan : Saya belum menguji kode ini. Silakan mengujinya sebelum menggunakannya.

Adam Rosenfield
sumber
1
cukup ubah [result appendFormat: "% s", buffer]; ke [result appendFormat: @ "% s", buffer];
Codezy
1
bagaimana Anda mengubah format untuk menerima baris kosong, atau lebih tepatnya baris yang terdiri dari satu karakter baris baru?
jakev
Ini berhenti lebih awal untuk saya setelah 812 baris. Baris 812 adalah "... 3 lagi", dan itu membuat string pembaca kosong.
sudo
1
Saya menambahkan tanda centang untuk melewati baris kosong: int fscanResult = fscanf (file, "% 4095 [^ \ n]% n% * c", buffer, & charsRead); if (fscanResult == 1) {[result appendFormat: @ "% s", buffer]; } else {if (feof (file)) {break; } lain jika (ferror (file)! = 0) {break; } fscanf (file, "\ n", nil, & charsRead); istirahat; }
Go Rose-Hulman
1
Jika saya membaca dokumentasi fscanf dengan benar, "%4095[^\n]%n%*c"diam-diam akan mengkonsumsi dan membuang satu karakter dengan masing-masing buffer membaca. Sepertinya format ini mengasumsikan bahwa garis akan lebih pendek dari panjang buffer.
Blago
12

Mac OS X adalah Unix, Objective-C adalah superset C, jadi Anda bisa menggunakan old-school fopendan fgetsdari <stdio.h>. Dijamin bekerja.

[NSString stringWithUTF8String:buf]akan mengonversi string C menjadi NSString. Ada juga metode untuk membuat string dalam pengkodean lain dan membuat tanpa menyalin.

Kornel
sumber
[menyalin komentar anonim] fgetsakan menyertakan '\n'karakter, jadi Anda mungkin ingin menghapusnya sebelum mengonversi string.
Kornel
9

Anda dapat menggunakan NSInputStreamyang memiliki implementasi dasar untuk aliran file. Anda dapat membaca byte ke buffer ( read:maxLength:metode). Anda harus memindai sendiri buffer untuk baris baru.

diederikh
sumber
6

Cara yang tepat untuk membaca file teks dalam Cocoa / Objective-C didokumentasikan dalam panduan pemrograman String Apple. Bagian untuk membaca dan menulis file harus sesuai keinginan Anda. PS: Apa itu "garis"? Dua bagian string yang dipisahkan oleh "\ n"? Atau "\ r"? Atau "\ r \ n"? Atau mungkin Anda benar-benar setelah paragraf? Panduan yang disebutkan sebelumnya juga termasuk bagian tentang pemisahan string menjadi garis atau paragraf. (Bagian ini disebut "Paragraph dan Line Breaks", dan terhubung dengan di menu sebelah kiri dari halaman yang saya tunjukkan di atas. Sayangnya situs ini tidak memungkinkan saya untuk mengirim lebih dari satu URL karena saya belum menjadi pengguna yang dapat dipercaya.)

Mengutip Knuth: optimisasi prematur adalah akar dari semua kejahatan. Jangan hanya berasumsi bahwa "membaca seluruh file ke dalam memori" lambat. Sudahkah Anda membandingkannya? Apakah Anda tahu itu benar - benar membaca seluruh file ke dalam memori? Mungkin itu hanya mengembalikan objek proxy dan terus membaca di belakang layar saat Anda mengkonsumsi string? ( Penafian: Saya tidak tahu apakah NSString benar-benar melakukan ini. Bisa dibayangkan. ) Intinya adalah: pertama-tama lakukanlah dengan cara terdokumentasi dalam melakukan sesuatu. Kemudian, jika tolok ukur menunjukkan bahwa ini tidak memiliki kinerja yang Anda inginkan, optimalkan.

Stig Brautaset
sumber
Karena Anda menyebutkan akhir baris CRLF (Windows): Itu sebenarnya adalah kasus yang memecah cara Objective-C dalam melakukan sesuatu. Jika Anda menggunakan salah satu -stringWithContentsOf*metode yang diikuti oleh -componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet], itu akan melihat \rdan \nsecara terpisah dan menambahkan baris kosong setelah setiap baris.
Siobhan
Yang mengatakan, solusi f gagal gagal pada file CR-only. Tapi itu (secara teoritis) jarang terjadi saat ini, dan FTC berfungsi baik untuk LF dan CRLF.
Siobhan
6

Banyak dari jawaban ini adalah potongan kode yang panjang atau mereka baca di seluruh file. Saya suka menggunakan metode c untuk tugas ini.

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

Perhatikan bahwa fgetln tidak akan menyimpan karakter baris baru Anda. Juga, Kami memberi +1 panjang str karena kami ingin memberikan ruang untuk penghentian NULL.

DCurro
sumber
4

Untuk membaca file baris demi baris (juga untuk file besar yang ekstrem) dapat dilakukan dengan fungsi-fungsi berikut:

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

Atau:

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

Kelas DDFileReader yang memungkinkan ini adalah sebagai berikut:

File Antarmuka (.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

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

Kelas dilakukan oleh Dave DeLong

lukaswelte
sumber
4

Seperti yang dikatakan @porneL, api C sangat berguna.

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
sumber
4

Seperti yang lain telah menjawab baik NSInputStream dan NSFileHandle adalah opsi yang baik, tetapi juga dapat dilakukan dengan cara yang cukup kompak dengan NSData dan pemetaan memori:

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
sumber
1

Jawaban ini BUKAN ObjC tetapi C.

Karena ObjC berbasis 'C', mengapa tidak menggunakan fgets?

Dan ya, saya yakin ObjC memiliki metode sendiri - Saya belum cukup mahir untuk mengetahui apa itu :)

KevinDTimm
sumber
5
Jika Anda tidak tahu bagaimana melakukannya di Objective-C, lalu mengapa mengatakan itu bukan jawabannya? Ada banyak alasan untuk tidak drop down ke straight C jika Anda bisa melakukannya sebaliknya. Misalnya, fungsi C menangani char * tetapi dibutuhkan lebih banyak pekerjaan untuk membaca sesuatu yang lain, seperti pengkodean yang berbeda. Juga, dia menginginkan objek NSString. Semua mengatakan, menggulung ini sendiri tidak hanya lebih banyak kode, tetapi juga rawan kesalahan.
Quinn Taylor
3
Saya setuju dengan Anda 100%, tetapi saya telah menemukan bahwa (kadang-kadang) lebih baik untuk mendapatkan jawaban yang bekerja dengan cepat, mengimplementasikannya dan kemudian ketika alternatif yang lebih benar muncul, manfaatkan itu. Ini sangat penting saat membuat prototipe, memberikan kesempatan untuk mendapatkan sesuatu untuk bekerja dan kemudian berkembang dari sana.
KevinDTimm
3
Saya baru sadar bahwa itu dimulai "Jawaban ini" bukan "Jawabannya". Doh! Saya setuju, pasti lebih baik memiliki peretasan yang berfungsi daripada kode elegan yang tidak. Saya tidak menurunkan suara Anda, tetapi melemparkan dugaan tanpa mengetahui apa yang mungkin dimiliki Objective-C mungkin juga tidak membantu. Meski begitu, membuat upaya selalu lebih baik daripada seseorang yang tahu dan tidak membantu ... ;-)
Quinn Taylor
Ini tidak memberikan jawaban untuk pertanyaan itu. Untuk mengkritik atau meminta klarifikasi dari penulis, tinggalkan komentar di bawah posting mereka.
Robotic Cat
1
@KevinDTimm: Saya setuju; Saya hanya menyesal bahwa saya tidak menemukan jawaban 5 tahun. Mungkin ini metapertanyaan; haruskah pertanyaan yang sangat lama dari pengguna biasa dapat ditandai untuk ditinjau?
Robotic Cat
0

dari jawaban @Adam Rosenfield, string pemformatan fscanfakan diubah seperti di bawah ini:

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

ini akan bekerja di osx, linux, ujung baris windows.

sooop
sumber
0

Menggunakan kategori atau ekstensi untuk membuat hidup kita sedikit lebih mudah.

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
sumber
0

Saya menemukan respons oleh @lukaswelte dan kode dari Dave DeLong sangat membantu. Saya sedang mencari solusi untuk masalah ini tetapi perlu mengurai file besar dengan \r\ntidak hanya \n.

Kode seperti tertulis berisi bug jika parsing oleh lebih dari satu karakter. Saya telah mengubah kode seperti di bawah ini.

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

file .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
hovey
sumber
0

Saya menambahkan ini karena semua jawaban lain yang saya coba gagal. Metode berikut ini dapat menangani file besar, garis panjang sembarang, serta baris kosong. Ini telah diuji dengan konten aktual dan akan menghapus karakter baris baru dari 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;
}

Kredit jatuh ke @Adam Rosenfield dan @sooop

Blago
sumber
0

Saya melihat banyak jawaban ini bergantung pada membaca seluruh file teks ke dalam memori daripada mengambilnya satu per satu. Inilah solusi saya di Swift modern yang bagus, menggunakan FileHandle untuk menjaga dampak memori rendah:

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
    }

}

Perhatikan bahwa ini menjaga carriage return di akhir baris, jadi tergantung pada kebutuhan Anda, Anda mungkin ingin menyesuaikan kode untuk menghapusnya.

Penggunaan: cukup buka pegangan file ke file teks target Anda dan panggil readLinedengan panjang maksimum yang sesuai - 1024 adalah standar untuk teks biasa, tetapi saya membiarkannya terbuka jika Anda tahu itu akan lebih pendek. Perhatikan bahwa perintah tidak akan meluap akhir file, jadi Anda mungkin harus memeriksa secara manual bahwa Anda belum mencapainya jika Anda bermaksud menguraikan semuanya. Berikut ini beberapa contoh kode yang menunjukkan cara membuka file di myFileURLdan membacanya baris demi baris hingga akhir.

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)"
}
Abu
sumber
-2

Inilah solusi sederhana yang bagus yang saya gunakan untuk file yang lebih kecil:

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
sumber
Dia bertanya tentang cara membaca satu baris pada satu waktu sehingga tidak membaca seluruh isi ke dalam memori. Solusi Anda membuat string dengan seluruh konten kemudian membaginya menjadi beberapa baris.
David
-7

Gunakan skrip ini, ini berfungsi dengan baik:

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
sumber
1
Apa yang dikatakan @fisninear adalah bahwa ini tidak mengatasi keinginan OP untuk mengurangi penggunaan memori. OP tidak bertanya bagaimana menggunakan metode ini (yang memuat seluruh file ke dalam memori), ia meminta alternatif ramah-memori untuk file teks besar. Sangat mungkin untuk memiliki file teks multi-gigabyte, yang jelas menciptakan masalah memori.
Joshua Nozzi