iOS开发:RTF文件粗体文本检测失败问题求助
解决iOS中RTF试题粗体答案检测失败的问题
我在iOS应用中需要处理数千行试题,生成property list(plist)文件,其中正确答案以粗体标记,例如:
- What is 1+1?
A. 2(粗体)
B. 3
C. 4
D. 5
此例中A.2为粗体,但我无法检测这些粗体文本以提取正确答案,当前代码的粗体检测部分完全失效,无法返回预期结果。我已将RTF文件拖入Xcode侧边栏确保可访问,代码其余部分运行正常。
初始失效代码
尝试用正则匹配HTML标签检测粗体,但RTF格式不适用此逻辑:
-(void)editTextFile { NSError *error = nil; NSString *filepath = [[NSBundle mainBundle] pathForResource:@"2024ROMANSEDITED" ofType:@"rtf"]; NSAttributedString *fileContent = [[NSAttributedString alloc] initWithFileURL:[NSURL fileURLWithPath:filepath] options:@{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType} documentAttributes:nil error:&error]; NSArray *questionItems = [fileContent.string componentsSeparatedByString:@"\n\n"]; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsPath = paths.firstObject; NSString *plistPath = [documentsPath stringByAppendingPathComponent:@"chatgptoptions.plist"]; NSMutableArray *questions = [NSMutableArray array]; for (NSString *questionItem in questionItems) { NSArray *items = [questionItem componentsSeparatedByString:@"\n"]; NSString *theQuestion = items.firstObject; NSArray *answerOptions = [items subarrayWithRange:NSMakeRange(1, items.count - 1)]; NSString *correctAnswerIndex = @""; for (NSUInteger i = 0; i < answerOptions.count; i++) { NSString *answerOption = answerOptions[i]; NSRegularExpression *boldRegex = [NSRegularExpression regularExpressionWithPattern:@"<b>(.*?)</b>" options:NSRegularExpressionCaseInsensitive error:nil]; NSArray *boldMatches = [boldRegex matchesInString:answerOption options:0 range:NSMakeRange(0, answerOption.length)]; if (boldMatches.count > 0) { correctAnswerIndex = [NSString stringWithFormat:@"%lu", i]; break; } } NSDictionary *innerDict = @{ @"Answer": correctAnswerIndex, @"duration_in_seconds": @"30", @"negative_points": @"5", @"options": answerOptions, @"points": @"20", @"question": theQuestion, @"question_type": @"1" }; [questions addObject:innerDict]; } NSDictionary *plistDictionary = @{ @"Questions": questions }; BOOL success = [plistDictionary writeToFile:plistPath atomically:YES]; if (success) { NSLog(@"PLIST file generated successfully at path: %@", plistPath); } else { NSLog(@"Failed to generate PLIST file."); } }
问题说明:RTF文件的粗体并非HTML的<b>标签,正则匹配完全无效,导致correctAnswerIndex始终为空。
更新后仍失效代码
尝试保留富文本格式,但拆分时丢失了属性信息:
-(void)editTextFile { NSString *filepath = [[NSBundle mainBundle] pathForResource:@"2024ROMANSEDITED" ofType:@"rtf"]; NSError *error = nil; NSAttributedString *fileContent = [[NSAttributedString alloc] initWithFileURL:[NSURL fileURLWithPath:filepath] options:@{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType} documentAttributes:nil error:&error]; NSArray *questionItems = [self extractQuestionItemsFromRTFFileContent:fileContent]; NSMutableArray *questions = [NSMutableArray array]; for (NSAttributedString *questionItem in questionItems) { NSString *theQuestion = [self extractQuestionFromQuestionItem:questionItem]; NSArray *answerOptions = [self extractAnswerOptionsFromQuestionItem:questionItem]; NSString *correctAnswerIndex = [self extractCorrectAnswerIndexFromAnswerOptions:answerOptions]; NSDictionary *innerDict = @{ @"Answer": correctAnswerIndex, @"duration_in_seconds": @"30", @"negative_points": @"5", @"options": answerOptions, @"points": @"20", @"question": theQuestion, @"question_type": @"1" }; [questions addObject:innerDict]; } NSDictionary *plistDictionary = @{ @"Questions": questions }; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsPath = paths.firstObject; NSString *plistPath = [documentsPath stringByAppendingPathComponent:@"chatgptoptions.plist"]; BOOL success = [plistDictionary writeToFile:plistPath atomically:YES]; if (success) { NSLog(@"PLIST file generated successfully at path: %@", plistPath); } else { NSLog(@"Failed to generate PLIST file."); } } - (NSArray *)extractQuestionItemsFromRTFFileContent:(NSAttributedString *)fileContent { NSMutableArray *questionItems = [NSMutableArray array]; NSArray *paragraphs = [fileContent.string componentsSeparatedByString:@"\n\n"]; for (NSString *paragraph in paragraphs) { NSAttributedString *attributedParagraph = [[NSAttributedString alloc] initWithString:paragraph]; [questionItems addObject:attributedParagraph]; } return questionItems; } - (NSString *)extractQuestionFromQuestionItem:(NSAttributedString *)questionItem { return questionItem.string; } - (NSArray *)extractAnswerOptionsFromQuestionItem:(NSAttributedString *)questionItem { NSString *questionText = questionItem.string; NSArray *lines = [questionText componentsSeparatedByString:@"\n"]; return [lines subarrayWithRange:NSMakeRange(1, lines.count - 1)]; } - (NSString *)extractCorrectAnswerIndexFromAnswerOptions:(NSArray *)answerOptions { for (NSUInteger i = 0; i < answerOptions.count; i++) { NSString *answerOption = answerOptions[i]; if ([self isAnswerOptionBold:answerOption]) { return [NSString stringWithFormat:@"%lu", i]; } } return @""; } - (BOOL)isAnswerOptionBold:(NSString *)answerOption { NSAttributedString *attributedOption = [[NSAttributedString alloc] initWithString:answerOption]; NSRange effectiveRange; NSDictionary *attributes = [attributedOption attributesAtIndex:0 effectiveRange:&effectiveRange]; UIFont *font = attributes[NSFontAttributeName]; NSDictionary *fontAttributes = [font.fontDescriptor fontAttributes]; NSString *fontFamilyName = fontAttributes[UIFontDescriptorFamilyAttribute]; UIFontDescriptorSymbolicTraits traits = font.fontDescriptor.symbolicTraits; BOOL isFontBold = (traits & UIFontDescriptorTraitBold) == UIFontDescriptorTraitBold; NSLog(@"Font Family: %@", fontFamilyName); NSLog(@"Is Bold: %@", isFontBold ? @"YES" : @"NO"); if (font) { NSDictionary *fontAttributes = [font.fontDescriptor fontAttributes]; NSString *fontFamilyName = fontAttributes[UIFontDescriptorFamilyAttribute]; UIFontDescriptorSymbolicTraits traits = font.fontDescriptor.symbolicTraits; BOOL isFontBold = (traits & UIFontDescriptorTraitBold) == UIFontDescriptorTraitBold; NSLog(@"Font Family: %@", fontFamilyName); NSLog(@"Is Bold: %@", isFontBold ? @"YES" : @"NO"); if (isFontBold) { NSLog(@"Bold: %@", answerOption); return YES; } } return NO; }
问题说明:拆分段落时将原NSAttributedString转成NSString后重新创建,完全丢失了RTF中的字体粗体属性,导致检测时字体信息为空。
修复后的完整代码
核心思路:全程保留富文本属性,通过枚举字体特征检测粗体
-(void)editTextFile { NSError *error = nil; NSString *filepath = [[NSBundle mainBundle] pathForResource:@"2024ROMANSEDITED" ofType:@"rtf"]; NSAttributedString *fileContent = [[NSAttributedString alloc] initWithFileURL:[NSURL fileURLWithPath:filepath] options:@{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType} documentAttributes:nil error:&error]; if (!fileContent || error) { NSLog(@"Failed to load RTF file: %@", error.localizedDescription); return; } NSMutableArray *questions = [NSMutableArray array]; // 按段落拆分(保留富文本属性) [self enumerateParagraphsInAttributedString:fileContent usingBlock:^(NSAttributedString *paragraph, BOOL *stop) { if (paragraph.length == 0) return; // 拆分段落内的行(保留每行的富文本属性) NSMutableArray *attributedLines = [self splitAttributedStringIntoLines:paragraph]; if (attributedLines.count < 2) return; // 至少有题目+一个选项 NSAttributedString *questionAttributed = attributedLines.firstObject; NSString *theQuestion = questionAttributed.string; NSArray *answerAttributedOptions = [attributedLines subarrayWithRange:NSMakeRange(1, attributedLines.count - 1)]; NSMutableArray *answerOptionsStrings = [NSMutableArray array]; NSString *correctAnswerIndex = @""; for (NSUInteger i = 0; i < answerAttributedOptions.count; i++) { NSAttributedString *optionAttributed = answerAttributedOptions[i]; NSString *optionString = optionAttributed.string; [answerOptionsStrings addObject:optionString]; // 检测当前选项是否包含粗体文本 if ([self doesAttributedStringContainBoldText:optionAttributed]) { correctAnswerIndex = [NSString stringWithFormat:@"%lu", i]; break; } } NSDictionary *innerDict = @{ @"Answer": correctAnswerIndex, @"duration_in_seconds": @"30", @"negative_points": @"5", @"options": answerOptionsStrings, @"points": @"20", @"question": theQuestion, @"question_type": @"1" }; [questions addObject:innerDict]; }]; NSDictionary *plistDictionary = @{@"Questions": questions}; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsPath = paths.firstObject; NSString *plistPath = [documentsPath stringByAppendingPathComponent:@"chatgptoptions.plist"]; BOOL success = [plistDictionary writeToFile:plistPath atomically:YES]; NSLog(success ? @"PLIST file generated successfully at path: %@" : @"Failed to generate PLIST file.", plistPath); } // 枚举富文本中的段落(按\n\n拆分,保留属性) - (void)enumerateParagraphsInAttributedString:(NSAttributedString *)attributedString usingBlock:(void(^)(NSAttributedString *paragraph, BOOL *stop))block { NSString *fullString = attributedString.string; NSRange currentRange = NSMakeRange(0, 0); BOOL stop = NO; while (currentRange.location + currentRange.length < fullString.length && !stop) { NSRange paragraphRange = [fullString rangeOfString:@"\n\n" options:0 range:NSMakeRange(currentRange.location + currentRange.length, fullString.length - (currentRange.location + currentRange.length))]; if (paragraphRange.location == NSNotFound) { paragraphRange = NSMakeRange(currentRange.location + currentRange.length, fullString.length - (currentRange.location + currentRange.length)); NSAttributedString *paragraph = [attributedString attributedSubstringFromRange:paragraphRange]; block(paragraph, &stop); break; } else { NSRange subRange = NSMakeRange(currentRange.location + currentRange.length, paragraphRange.location - (currentRange.location + currentRange.length)); NSAttributedString *paragraph = [attributedString attributedSubstringFromRange:subRange]; block(paragraph, &stop); currentRange = paragraphRange; } } } // 将富文本段落拆分为单行(按\n拆分,保留属性) - (NSMutableArray *)splitAttributedStringIntoLines:(NSAttributedString *)attributedString { NSMutableArray *lines = [NSMutableArray array]; NSString *string = attributedString.string; NSRange currentRange = NSMakeRange(0, 0); while (currentRange.location + currentRange.length < string.length) { NSRange lineRange = [string rangeOfString:@"\n" options:0 range:NSMakeRange(currentRange.location + currentRange.length, string.length - (currentRange.location + currentRange.length))]; if (lineRange.location == NSNotFound) { lineRange = NSMakeRange(currentRange.location + currentRange.length, string.length - (currentRange.location + currentRange.length)); NSAttributedString *line = [attributedString attributedSubstringFromRange:lineRange]; [lines addObject:line]; break; } else { NSRange subRange = NSMakeRange(currentRange.location + currentRange.length, lineRange.location - (currentRange.location + currentRange.length)); NSAttributedString *line = [attributedString attributedSubstringFromRange:subRange]; [lines addObject:line]; currentRange = lineRange; } } return lines; } // 检测富文本是否包含粗体文本 - (BOOL)doesAttributedStringContainBoldText:(NSAttributedString *)attributedString { __block BOOL containsBold = NO; [attributedString enumerateAttributesInRange:NSMakeRange(0, attributedString.length) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { UIFont *font = attrs[NSFontAttributeName]; if (font) { UIFontDescriptorSymbolicTraits traits = font.fontDescriptor.symbolicTraits; if ((traits & UIFontDescriptorTraitBold) == UIFontDescriptorTraitBold) { containsBold = YES; *stop = YES; } } }]; return containsBold; }
修复要点
- 全程保留富文本属性:直接对原
NSAttributedString进行段落和行拆分,不转换为纯文本,确保字体属性不丢失。 - 正确检测粗体:通过枚举富文本的属性,检查字体的
UIFontDescriptorTraitBold特征,判断是否为粗体。 - 属性保留的拆分逻辑:实现了针对富文本的段落(
\n\n)和行(\n)拆分方法,完整保留每个题目和选项的格式信息。
内容的提问来源于stack exchange,提问作者user717452
相关产品推荐
相关产品推荐

