Как лучше всего работать с большими текстовыми файлами в Objective-C? Скажем, мне нужно читать каждую строку отдельно и рассматривать каждую строку как NSString. Как это сделать наиболее эффективно?
Одно из решений - использовать метод NSString:
+ (id)stringWithContentsOfFile:(NSString *)path
encoding:(NSStringEncoding)enc
error:(NSError **)error
а затем разделите строки разделителем новой строки, а затем выполните итерацию по элементам в массиве. Однако это кажется довольно неэффективным. Нет ли простого способа рассматривать файл как поток, перечисляя по каждой строке, вместо того, чтобы просто читать все сразу? Вроде как Java java.io.BufferedReader.
Ответы:
Отличный вопрос. Я думаю, что у @Diederik есть хороший ответ, хотя, к сожалению, у Какао нет механизма для того, что вы хотите делать.
NSInputStream
позволяет вам читать фрагменты из N байтов (очень похоже наjava.io.BufferedReader
), но вы должны преобразовать его вNSString
самостоятельно, затем сканировать новые строки (или любой другой разделитель) и сохранять любые оставшиеся символы для следующего чтения или читать больше символов если новая строка еще не была прочитана. (NSFileHandle
позволяет читать,NSData
который затем можно преобразовать вNSString
, но по сути это тот же процесс.)У Apple есть Руководство по потоковому программированию, которое может помочь в заполнении деталей, и этот вопрос SO также может помочь, если вы собираетесь иметь дело с
uint8_t*
буферами.Если вы собираетесь часто читать такие строки (особенно в разных частях вашей программы), было бы неплохо инкапсулировать это поведение в классе, который может обрабатывать детали за вас, или даже создать подкласс
NSInputStream
(он предназначен для подклассы ) и добавление методов, позволяющих читать именно то, что вы хотите.Для протокола, я думаю, что было бы неплохо добавить эту функцию, и я буду подавать запрос на улучшение чего-то, что делает это возможным. :-)
Изменить: оказывается, этот запрос уже существует. Для этого есть радар 2006 года (rdar: // 4742914 для сотрудников Apple).
источник
Это будет работать для общего чтения
String
изText
. Если вы хотите прочитать более длинный текст (большой размер текста) , используйте метод, упомянутый здесь другими людьми, например, буферизованный (зарезервируйте размер текста в памяти) .Скажем, вы читаете текстовый файл.
NSString* filePath = @""//file path... NSString* fileRoot = [[NSBundle mainBundle] pathForResource:filePath ofType:@"txt"];
Вы хотите избавиться от новой строки.
// read everything from text NSString* fileContents = [NSString stringWithContentsOfFile:fileRoot encoding:NSUTF8StringEncoding error:nil]; // first, separate by new line NSArray* allLinedStrings = [fileContents componentsSeparatedByCharactersInSet: [NSCharacterSet newlineCharacterSet]]; // then break down even further NSString* strsInOneLine = [allLinedStrings objectAtIndex:0]; // choose whatever input identity you have decided. in this case ; NSArray* singleStrs = [currentPointString componentsSeparatedByCharactersInSet: [NSCharacterSet characterSetWithCharactersInString:@";"]];
Вот и все.
источник
Это должно помочь:
#include <stdio.h> NSString *readLineAsNSString(FILE *file) { char buffer[4096]; // tune this capacity to your liking -- larger buffer sizes will be faster, but // use more memory NSMutableString *result = [NSMutableString stringWithCapacity:256]; // Read up to 4095 non-newline characters, then read and discard the newline int charsRead; do { if(fscanf(file, "%4095[^\n]%n%*c", buffer, &charsRead) == 1) [result appendFormat:@"%s", buffer]; else break; } while(charsRead == 4095); return result; }
Используйте следующим образом:
FILE *file = fopen("myfile", "r"); // check for NULL while(!feof(file)) { NSString *line = readLineAsNSString(file); // do stuff with line; line is autoreleased, so you should NOT release it (unless you also retain it beforehand) } fclose(file);
Этот код считывает из файла до 4095 символов, не являющихся символами новой строки. Если у вас есть строка длиной более 4095 символов, она будет продолжать чтение до тех пор, пока не достигнет новой строки или конца файла.
Примечание : я не тестировал этот код. Пожалуйста, проверьте его перед использованием.
источник
"%4095[^\n]%n%*c"
будет молча потреблять и отбрасывать один символ при каждом чтении буфера. Похоже, этот формат предполагает, что строки будут короче, чем длина буфера.Mac OS X - это Unix, Objective-C - это надмножество C, поэтому вы можете просто использовать старую школу
fopen
иfgets
оттуда<stdio.h>
. Гарантированно сработает.[NSString stringWithUTF8String:buf]
преобразует строку C вNSString
. Также существуют методы создания строк в других кодировках и создания без копирования.источник
fgets
будет включать'\n'
символ, поэтому вы можете удалить его перед преобразованием строки.Вы можете использовать то,
NSInputStream
что имеет базовую реализацию для файловых потоков. Вы можете читать байты в буфер (read:maxLength:
метод). Вы должны сами сканировать буфер на предмет новых строк.источник
Соответствующий способ чтения текстовых файлов в Cocoa / Objective-C задокументирован в руководстве Apple по программированию String. Раздел для чтения и записи файлов должен быть именно тем, что вам нужно. PS: Что такое «линия»? Две части строки, разделенные "\ n"? Или "\ r"? Или "\ r \ n"? Или, может быть, вы действительно после абзацев? Ранее упомянутое руководство также включает раздел о разделении строки на строки или абзацы. (Этот раздел называется «Абзацы и разрывы строк», и ссылка на него находится в левом меню страницы, на которую я указал выше. К сожалению, этот сайт не позволяет мне размещать более одного URL-адреса, поскольку я еще не заслуживающий доверия пользователь.)
Перефразируя Кнута: преждевременная оптимизация - это корень всех зол. Не думайте, что «чтение всего файла в память» происходит медленно. Вы проверили это? Вы знаете, что он на самом деле считывает весь файл в память? Может быть, он просто возвращает прокси-объект и продолжает читать за кулисами, пока вы потребляете строку? ( Отказ от ответственности: я понятия не имею, действительно ли это делает NSString. Возможно, это возможно. ) Суть в следующем: сначала используйте задокументированный способ выполнения действий. Затем, если тесты покажут, что это не дает желаемой производительности, оптимизируйте.
источник
-stringWithContentsOf*
методов-componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]
, он видит\r
и\n
отдельно и добавляет пустую строку после каждой строки.Многие из этих ответов представляют собой длинные фрагменты кода или читаются во всем файле. Мне нравится использовать методы c именно для этой задачи.
FILE* file = fopen("path to my file", "r"); size_t length; char *cLine = fgetln(file,&length); while (length>0) { char str[length+1]; strncpy(str, cLine, length); str[length] = '\0'; NSString *line = [NSString stringWithFormat:@"%s",str]; % Do what you want here. cLine = fgetln(file,&length); }
Обратите внимание, что fgetln не сохранит ваш символ новой строки. Кроме того, мы +1 к длине str, потому что хотим освободить место для завершения NULL.
источник
Прочитать файл построчно (также для очень больших файлов) можно с помощью следующих функций:
DDFileReader * reader = [[DDFileReader alloc] initWithFilePath:pathToMyFile]; NSString * line = nil; while ((line = [reader readLine])) { NSLog(@"read line: %@", line); } [reader release];
Или:
DDFileReader * reader = [[DDFileReader alloc] initWithFilePath:pathToMyFile]; [reader enumerateLinesUsingBlock:^(NSString * line, BOOL * stop) { NSLog(@"read line: %@", line); }]; [reader release];
Класс DDFileReader, обеспечивающий это, следующий:
Файл интерфейса (.h):
@interface DDFileReader : NSObject { NSString * filePath; NSFileHandle * fileHandle; unsigned long long currentOffset; unsigned long long totalFileLength; NSString * lineDelimiter; NSUInteger chunkSize; } @property (nonatomic, copy) NSString * lineDelimiter; @property (nonatomic) NSUInteger chunkSize; - (id) initWithFilePath:(NSString *)aPath; - (NSString *) readLine; - (NSString *) readTrimmedLine; #if NS_BLOCKS_AVAILABLE - (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL *))block; #endif @end
Реализация (.m)
#import "DDFileReader.h" @interface NSData (DDAdditions) - (NSRange) rangeOfData_dd:(NSData *)dataToFind; @end @implementation NSData (DDAdditions) - (NSRange) rangeOfData_dd:(NSData *)dataToFind { const void * bytes = [self bytes]; NSUInteger length = [self length]; const void * searchBytes = [dataToFind bytes]; NSUInteger searchLength = [dataToFind length]; NSUInteger searchIndex = 0; NSRange foundRange = {NSNotFound, searchLength}; for (NSUInteger index = 0; index < length; index++) { if (((char *)bytes)[index] == ((char *)searchBytes)[searchIndex]) { //the current character matches if (foundRange.location == NSNotFound) { foundRange.location = index; } searchIndex++; if (searchIndex >= searchLength) { return foundRange; } } else { searchIndex = 0; foundRange.location = NSNotFound; } } return foundRange; } @end @implementation DDFileReader @synthesize lineDelimiter, chunkSize; - (id) initWithFilePath:(NSString *)aPath { if (self = [super init]) { fileHandle = [NSFileHandle fileHandleForReadingAtPath:aPath]; if (fileHandle == nil) { [self release]; return nil; } lineDelimiter = [[NSString alloc] initWithString:@"\n"]; [fileHandle retain]; filePath = [aPath retain]; currentOffset = 0ULL; chunkSize = 10; [fileHandle seekToEndOfFile]; totalFileLength = [fileHandle offsetInFile]; //we don't need to seek back, since readLine will do that. } return self; } - (void) dealloc { [fileHandle closeFile]; [fileHandle release], fileHandle = nil; [filePath release], filePath = nil; [lineDelimiter release], lineDelimiter = nil; currentOffset = 0ULL; [super dealloc]; } - (NSString *) readLine { if (currentOffset >= totalFileLength) { return nil; } NSData * newLineData = [lineDelimiter dataUsingEncoding:NSUTF8StringEncoding]; [fileHandle seekToFileOffset:currentOffset]; NSMutableData * currentData = [[NSMutableData alloc] init]; BOOL shouldReadMore = YES; NSAutoreleasePool * readPool = [[NSAutoreleasePool alloc] init]; while (shouldReadMore) { if (currentOffset >= totalFileLength) { break; } NSData * chunk = [fileHandle readDataOfLength:chunkSize]; NSRange newLineRange = [chunk rangeOfData_dd:newLineData]; if (newLineRange.location != NSNotFound) { //include the length so we can include the delimiter in the string chunk = [chunk subdataWithRange:NSMakeRange(0, newLineRange.location+[newLineData length])]; shouldReadMore = NO; } [currentData appendData:chunk]; currentOffset += [chunk length]; } [readPool release]; NSString * line = [[NSString alloc] initWithData:currentData encoding:NSUTF8StringEncoding]; [currentData release]; return [line autorelease]; } - (NSString *) readTrimmedLine { return [[self readLine] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; } #if NS_BLOCKS_AVAILABLE - (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL*))block { NSString * line = nil; BOOL stop = NO; while (stop == NO && (line = [self readLine])) { block(line, &stop); } } #endif @end
Занятие провел Дэйв Делонг.
источник
Как сказал @porneL, C api очень удобен.
NSString* fileRoot = [[NSBundle mainBundle] pathForResource:@"record" ofType:@"txt"]; FILE *file = fopen([fileRoot UTF8String], "r"); char buffer[256]; while (fgets(buffer, 256, file) != NULL){ NSString* result = [NSString stringWithUTF8String:buffer]; NSLog(@"%@",result); }
источник
Как ответили другие, NSInputStream и NSFileHandle - прекрасные варианты, но это также можно сделать довольно компактным способом с помощью NSData и сопоставления памяти:
BRLineReader.h
#import <Foundation/Foundation.h> @interface BRLineReader : NSObject @property (readonly, nonatomic) NSData *data; @property (readonly, nonatomic) NSUInteger linesRead; @property (strong, nonatomic) NSCharacterSet *lineTrimCharacters; @property (readonly, nonatomic) NSStringEncoding stringEncoding; - (instancetype)initWithFile:(NSString *)filePath encoding:(NSStringEncoding)encoding; - (instancetype)initWithData:(NSData *)data encoding:(NSStringEncoding)encoding; - (NSString *)readLine; - (NSString *)readTrimmedLine; - (void)setLineSearchPosition:(NSUInteger)position; @end
BRLineReader.m
#import "BRLineReader.h" static unsigned char const BRLineReaderDelimiter = '\n'; @implementation BRLineReader { NSRange _lastRange; } - (instancetype)initWithFile:(NSString *)filePath encoding:(NSStringEncoding)encoding { self = [super init]; if (self) { NSError *error = nil; _data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedAlways error:&error]; if (!_data) { NSLog(@"%@", [error localizedDescription]); } _stringEncoding = encoding; _lineTrimCharacters = [NSCharacterSet whitespaceAndNewlineCharacterSet]; } return self; } - (instancetype)initWithData:(NSData *)data encoding:(NSStringEncoding)encoding { self = [super init]; if (self) { _data = data; _stringEncoding = encoding; _lineTrimCharacters = [NSCharacterSet whitespaceAndNewlineCharacterSet]; } return self; } - (NSString *)readLine { NSUInteger dataLength = [_data length]; NSUInteger beginPos = _lastRange.location + _lastRange.length; NSUInteger endPos = 0; if (beginPos == dataLength) { // End of file return nil; } unsigned char *buffer = (unsigned char *)[_data bytes]; for (NSUInteger i = beginPos; i < dataLength; i++) { endPos = i; if (buffer[i] == BRLineReaderDelimiter) break; } // End of line found _lastRange = NSMakeRange(beginPos, endPos - beginPos + 1); NSData *lineData = [_data subdataWithRange:_lastRange]; NSString *line = [[NSString alloc] initWithData:lineData encoding:_stringEncoding]; _linesRead++; return line; } - (NSString *)readTrimmedLine { return [[self readLine] stringByTrimmingCharactersInSet:_lineTrimCharacters]; } - (void)setLineSearchPosition:(NSUInteger)position { _lastRange = NSMakeRange(position, 0); _linesRead = 0; } @end
источник
Это НЕ ObjC, а C.
Поскольку ObjC основан на языке C, почему бы не использовать fgets?
И да, я уверен, что у ObjC есть свой метод - я просто еще недостаточно разбираюсь в том, что это такое :)
источник
meta
вопрос; следует ли помечать для проверки очень старые вопросы обычных пользователей?из ответа @Adam Rosenfield строка форматирования
fscanf
будет изменена, как показано ниже:"%4095[^\r\n]%n%*[\n\r]"
он будет работать в osx, linux, окончании строк Windows.
источник
Использование категории или расширения, чтобы сделать нашу жизнь немного проще.
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 }
источник
Я нашел ответ @lukaswelte и код Дэйва Делонга очень полезными. Я искал решение этой проблемы, но мне нужно было разбирать
\r\n
не только большие файлы\n
.Написанный код содержит ошибку при синтаксическом анализе более чем одним символом. Я изменил код, как показано ниже.
.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
.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
источник
Я добавляю это, потому что все остальные ответы, которые я пробовал, так или иначе не оправдали себя. Следующий метод может обрабатывать большие файлы, произвольные длинные строки, а также пустые строки. Он был протестирован с реальным контентом и будет вырезать символ новой строки из вывода.
- (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; }
Кредит принадлежит @Adam Rosenfield и @sooop
источник
Я вижу, что многие из этих ответов полагаются на чтение всего текстового файла в память вместо того, чтобы брать его по частям. Вот мое решение в красивом современном Swift, использующее FileHandle для снижения воздействия на память:
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 } }
Обратите внимание, что это сохраняет возврат каретки в конце строки, поэтому в зависимости от ваших потребностей вы можете изменить код, чтобы удалить его.
Использование: просто откройте дескриптор целевого текстового файла и вызовите
readLine
подходящую максимальную длину - 1024 является стандартным для обычного текста, но я оставил его открытым на случай, если вы знаете, что он будет короче. Обратите внимание, что команда не переполняет конец файла, поэтому вам, возможно, придется вручную проверить, что вы не достигли его, если вы собираетесь анализировать все это. Вот пример кода, который показывает, как открыть файлmyFileURL
и прочитать его построчно до конца.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)" }
источник
Вот хорошее простое решение, которое я использую для небольших файлов:
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); } }
источник
Используйте этот скрипт, он отлично работает:
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);
источник