You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS开发:RTF文件粗体文本检测失败问题求助

解决iOS中RTF试题粗体答案检测失败的问题

我在iOS应用中需要处理数千行试题,生成property list(plist)文件,其中正确答案以粗体标记,例如:

  1. 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;
}

修复要点

  1. 全程保留富文本属性:直接对原NSAttributedString进行段落和行拆分,不转换为纯文本,确保字体属性不丢失。
  2. 正确检测粗体:通过枚举富文本的属性,检查字体的UIFontDescriptorTraitBold特征,判断是否为粗体。
  3. 属性保留的拆分逻辑:实现了针对富文本的段落(\n\n)和行(\n)拆分方法,完整保留每个题目和选项的格式信息。

内容的提问来源于stack exchange,提问作者user717452

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 16:42:00