如何从HTML/RTF生成的NSAttributedString中提取表格?
刚好做过类似的需求!当你从HTML/RTF解析出NSAttributedString后,完全可以提取里面的表格结构——不管是拿到NSTextTableBlock、NSTextTable这类原生对象,还是获取单元格的NSRange,都有可行的方案。下面就用Swift 4给你详细拆解:
核心原理
NSAttributedString解析HTML/RTF表格时,会用NSTextTable代表整个表格,NSTextTableBlock代表每个单元格,这些对象会以属性的形式绑定在对应文本的范围上。我们只需要遍历字符串的属性,就能把这些结构化信息提取出来。
方案一:提取完整的表格层级结构
如果你想实现类似嵌套遍历表格→行→单元格的操作,可以先定义一个结构体来规整数据,然后遍历属性组装结构:
// 用来规整表格结构的自定义结构体 struct ParsedTable { let textTable: NSTextTable var rows: [[NSAttributedString]] = [] } // 解析NSAttributedString中的表格 func parseTables(from attributedString: NSAttributedString) -> [ParsedTable] { var tables: [ParsedTable] = [] var currentTable: ParsedTable? var currentRow: [NSAttributedString] = [] attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { attrs, range, stop in // 检查当前文本范围是否属于表格单元格 guard let textBlock = attrs[.textBlock] as? NSTextTableBlock else { // 不在表格里时,收尾未完成的行和表格 if !currentRow.isEmpty { currentTable?.rows.append(currentRow) currentRow = [] } if let unfinishedTable = currentTable { tables.append(unfinishedTable) currentTable = nil } return } // 获取单元格所属的表格 let textTable = textBlock.textTable if currentTable == nil { currentTable = ParsedTable(textTable: textTable) } else if currentTable?.textTable != textTable { // 遇到新表格,先把之前的表格存入结果 tables.append(currentTable!) currentTable = ParsedTable(textTable: textTable) currentRow = [] } // 提取单元格的富文本内容 let cellContent = attributedString.attributedSubstring(from: range) currentRow.append(cellContent) // 判断是否是当前行的最后一个单元格,是的话就收尾该行 if textBlock.isLastBlockInRow { currentTable?.rows.append(currentRow) currentRow = [] } } // 处理最后一个未收尾的表格 if !currentRow.isEmpty { currentTable?.rows.append(currentRow) } if let finalTable = currentTable { tables.append(finalTable) } return tables }
使用示例
就像你期望的那样,解析后可以直接嵌套遍历:
let html = """ <table style="height: 51px;" width="147"> <tbody> <tr> <td style="width: 65.5px;">a</td> <td style="width: 65.5px;">b</td> </tr> <tr> <td style="width: 65.5px;">c</td> <td style="width: 65.5px;">d</td> </tr> </tbody> </table> """ // 解析HTML为NSAttributedString guard let htmlData = html.data(using: .utf8), let attributedStr = try? NSAttributedString(data: htmlData, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else { fatalError("Failed to parse HTML content") } // 提取表格 let parsedTables = parseTables(from: attributedStr) // 遍历表格、行、单元格 for table in parsedTables { print("发现一个表格,包含 \(table.rows.count) 行") for row in table.rows { let cellTexts = row.map { $0.string } print("当前行内容:\(cellTexts.joined(separator: " | "))") for cell in row { // 在这里处理单元格内容,比如获取文本、样式等 print("单元格文本:\(cell.string)") } } }
方案二:仅获取表格/单元格的NSRange
如果不需要完整的内容结构,只需要获取表格和单元格的范围,可以用这个简化版的方法:
func getTableAndCellRanges(from attributedString: NSAttributedString) -> [(tableRange: NSRange, cellRanges: [NSRange])] { var tableRangeInfos: [(tableRange: NSRange, cellRanges: [NSRange])] = [] var currentCellRanges: [NSRange] = [] var currentTableStartLocation: Int? attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { attrs, range, stop in if let _ = attrs[.textBlock] as? NSTextTableBlock { // 标记表格的起始位置 if currentTableStartLocation == nil { currentTableStartLocation = range.location } currentCellRanges.append(range) } else { // 离开表格范围时,计算整个表格的范围并存入 if let startLoc = currentTableStartLocation { let tableRange = NSRange(location: startLoc, length: range.location - startLoc) tableRangeInfos.append((tableRange: tableRange, cellRanges: currentCellRanges)) currentTableStartLocation = nil currentCellRanges = [] } } } // 处理最后一个未收尾的表格 if let startLoc = currentTableStartLocation { let tableRange = NSRange(location: startLoc, length: attributedString.length - startLoc) tableRangeInfos.append((tableRange: tableRange, cellRanges: currentCellRanges)) } return tableRangeInfos }
使用示例
let rangeInfos = getTableAndCellRanges(from: attributedStr) for info in rangeInfos { print("表格范围:\(info.tableRange)") print("包含的单元格范围:\(info.cellRanges)") }
一些注意事项
- HTML解析成NSAttributedString时,可能会自动插入空白段落或换行,遍历的时候可以通过判断文本是否为空来过滤这些无关内容;
NSTextTableBlock的isLastBlockInRow属性是判断行结束的关键,能帮你正确把单元格分组为行;- 这个逻辑对RTF文件完全适用,因为RTF表格在NSAttributedString中的标记方式和HTML一致。
内容的提问来源于stack exchange,提问作者alagris
相关产品推荐
相关产品推荐

