Цель-C: чтение файла построчно
Как правильно обращаться с большими текстовыми файлами в Objective-C? Допустим, мне нужно читать каждую строку отдельно и хотеть рассматривать каждую строку как строку NSString. Каков наиболее эффективный способ сделать это?
Одним из решений является использование метода NSString:
+ (id)stringWithContentsOfFile:(NSString *)path
encoding:(NSStringEncoding)enc
error:(NSError **)error
а затем разбить строки с помощью разделителя новой строки, а затем перебрать элементы массива. Однако это кажется довольно неэффективным. Нет ли простого способа рассматривать файл как поток, перечисляя по каждой строке, вместо того, чтобы просто читать все сразу? Вроде как Java java.io.BufferedReader.
17 ответов
Это отличный вопрос. Я думаю, что у @Diederik есть хороший ответ, хотя, к сожалению, у Cocoa нет механизма для именно того, что вы хотите сделать.
NSInputStream
позволяет читать фрагменты N байтов (очень похоже на java.io.BufferedReader
), но вы должны преобразовать его в NSString
самостоятельно, затем отыщите новые строки (или любой другой разделитель) и сохраните все оставшиеся символы для следующего чтения или прочитайте больше символов, если новая строка еще не была прочитана. (NSFileHandle
позволяет читать NSData
который вы можете затем преобразовать в NSString
, но это по сути тот же процесс.)
У Apple есть Руководство по программированию Stream, которое может помочь заполнить детали, и этот вопрос 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 символов, она продолжает читать, пока не достигнет новой строки или конца файла.
Примечание: я не проверял этот код. Пожалуйста, проверьте его перед использованием.
Mac OS X - это Unix, Objective-C - это суперсет C, так что вы можете просто использовать old-school fopen
а также fgets
от <stdio.h>
, Это гарантированно работает.
[NSString stringWithUTF8String:buf]
преобразует строку C в NSString
, Существуют также методы для создания строк в других кодировках и создания без копирования.
Ты можешь использовать NSInputStream
которая имеет базовую реализацию для файловых потоков. Вы можете прочитать байты в буфер (read:maxLength:
метод). Вы должны сканировать буфер для новых строк самостоятельно.
Соответствующий способ чтения текстовых файлов в Cocoa/Objective-C описан в руководстве по программированию Apple String. Раздел для чтения и записи файлов должен быть именно тем, что вам нужно. PS: что такое "линия"? Два раздела строки, разделенные "\n"? Или "г"? Или "\r\n"? Или, может быть, вы на самом деле после абзацев? Ранее упомянутое руководство также включает раздел по разбиению строки на строки или абзацы. (Этот раздел называется "Абзацы и разрывы строк" и связан с левым меню на странице, на которую я указал выше. К сожалению, этот сайт не позволяет мне публиковать более одного URL, поскольку я еще не заслуживающий доверия пользователь.)
Перефразируя Кнута: преждевременная оптимизация - корень всего зла. Не просто предполагайте, что "чтение всего файла в память" происходит медленно. Вы оценили это? Знаете ли вы, что он фактически читает весь файл в память? Может быть, он просто возвращает прокси-объект и продолжает читать за кулисами, пока вы используете строку? (Отказ от ответственности: я понятия не имею, действительно ли NSString делает это. Это возможно.) Суть в том, чтобы: сначала придерживайтесь документированного способа делать вещи. Затем, если тесты показывают, что это не соответствует желаемой производительности, оптимизируйте.
Многие из этих ответов представляют собой длинные фрагменты кода или они читаются во всем файле. Мне нравится использовать методы 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 длина строки, потому что мы хотим освободить место для завершения NULL.
Как сказал @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);
}
Прочитать файл построчно (также для очень больших файлов) можно с помощью следующих функций:
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
Класс был сделан Dave DeLong
Как и другие, ответили, что 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
Я вижу, что многие из этих ответов полагаются на чтение всего текстового файла в память вместо того, чтобы брать его по частям. Вот мое решение в красивом современном 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)"
}
Этот ответ не ObjC, а C.
Поскольку ObjC основан на C, почему бы не использовать fgets?
И да, я уверен, что у ObjC есть свой метод - я просто недостаточно опытен, чтобы знать, что это такое:)
Я добавляю это, потому что все другие ответы, которые я пробовал, терпели неудачу так или иначе. Следующий метод может обрабатывать большие файлы, произвольные длинные строки, а также пустые строки. Он был протестирован с реальным содержимым и удалит символ новой строки из вывода.
- (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;
}
Кредит идет на @ Адам Розенфилд и @sooop
Использование категории или расширения, чтобы сделать нашу жизнь немного проще.
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 и код Dave DeLong. Я искал решение этой проблемы, но мне нужно было проанализировать большие файлы \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
Из ответа @ Адама Розенфилда, строка форматирования fscanf
будет изменено, как показано ниже:
"%4095[^\r\n]%n%*[\n\r]"
он будет работать в окончаниях строк OSX, Linux, Windows.
Вот хорошее простое решение, которое я использую для небольших файлов:
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);