docling-project / docling-project/docling

Multi header table detection.

Open
#1,983 1 comment 0 reactions 0 assignees View on GitHub
enhancement triage/close-stale
Dominant language
Python
Stars
66.4k
Forks
4.8k
Avg merge
2d 21h
Merged PRs (30d)
84

Description

### Requested feature

I deal with many complex Excel and Word files with complex tables in my work. They often have multiple headers or table titles.

For example:
Image

This example shows a multi-header table with a table title.
However, docling always treats the first row (title) as a header row.

### Alternatives

Currently, I have developed a standalone function that converts complex tables into standard markdown format, supporting merging multiple headers and automatic title detection. However, I am unsure how to incorporate this functionality into the docling project.

```python
def excel_to_markdown(file_path: str,
transpose: bool = False,
header_rows: int | tuple[int, int] | Literal['auto'] = 'auto',
sep: str = ".",
df_postpass: Optional[Callable[[DataFrame], None]] = None) -> str:
"""
:param file_path:excel工作簿路径
:param header_rows: 表头索引,或索引范围
:param transpose: 是否转置原表
:param sep: 多行表头合并后的分隔符
:param df_postpass: 可选DataFrame后期处理函数,比如NaN替换,无返回值
:return: 标准markdown字符串
"""
wb = load_workbook(file_path)
ws = wb.active
if transpose:
rows, cols = ws.max_row, ws.max_column
new_ws = wb.create_sheet("Transposed", 0)
# 1. 复制单元格值到转置位置
for row in range(1, rows + 1):
for col in range(1, cols + 1):
cell_value = ws.cell(row=row, column=col).value
new_ws.cell(row=col, column=row, value=cell_value)
# 2. 转置合并区域(行列互换)
for merged_range in ws.merged_cells.ranges:
# 获取原区域的行列范围
min_row, min_col = merged_range.min_row, merged_range.min_col
max_row, max_col = merged_range.max_row, merged_range.max_col

# 转置后的新区域范围
new_min_row, new_min_col = min_col, min_row
new_max_row, new_max_col = max_col, max_row

# 合并新区域
new_ws.merge_cells(
start_row=new_min_row,
start_column=new_min_col,
end_row=new_max_row,
end_column=new_max_col
)

# 清除非左上角单元格的值
top_left_value = new_ws.cell(row=new_min_row, column=new_min_col).value
for row in range(new_min_row, new_max_row + 1):
for col in range(new_min_col, new_max_col + 1):
if row == new_min_row and col == new_min_col:
continue
new_ws.cell(row=row, column=col, value=None)
# 3. 替换原工作表
wb.remove(ws)
# wb.save('test.xlsx')
ws = new_ws

if header_rows == 'auto':
merged_ranges = list(ws.merged_cells.ranges)
if len(merged_ranges) ==0:
header_rows=0
else:
#筛选只有列合并的区域
col_merged_ranges=[merged_range for merged_range in merged_ranges if merged_range.max_row==merged_range.min_row]
# 1.检测首行是否为全列合并的标题行
title_row = False
for merged_range in col_merged_ranges:
if merged_range.min_row == 1:
if merged_range.min_col == 1 and merged_range.max_col == ws.max_column:
title_row = True
break
# 2.确定表头起始行(跳过标题行)
start_row = 2 if title_row else 1
col_merged_ranges_max_row=max([merged_range.min_row for merged_range in col_merged_ranges])#获取所有列合并的区域行索引并取最大值
end_row = col_merged_ranges_max_row+1#首个无列合并区域就是初级表头
# 转换为 pandas 的 0-based 索引并转换为列表
start_header = start_row - 1
end_header = end_row - 1
header_rows=[i for i in range(start_header, end_header+1)]
print("表头索引范围:", header_rows)

# 处理所有合并区域,拆分并给每个子单元格赋值
for merged_range in list(ws.merged_cells.ranges):
# 获取合并区域左上角的值
top_left_value = ws.cell(
row=merged_range.min_row,
column=merged_range.min_col
).value
# 解除合并并填充值到所有子单元格
ws.unmerge_cells(str(merged_range))
for row in range(merged_range.min_row, merged_range.max_row + 1):
for col in range(merged_range.min_col, merged_range.max_col + 1):
ws.cell(row=row, column=col, value=top_left_value)

# 将初步处理后的数据保存到内存缓冲区
buffer = io.BytesIO()
wb.save(buffer)
buffer.seek(0) # 重置指针到文件开头
df = pd.read_excel(buffer, header=header_rows) # 从内存缓冲区读取Excel数据
# 统一处理列名(兼容单级和多级表头)
new_columns = []
for col in df.columns:
if isinstance(col, str):
new_columns.append(col)
elif isinstance(col, tuple):
parts = [str(item) for item in col if not pd.isna(item)]
new_columns.append(sep.join(parts) if parts else "无标题")
else:
new_columns.append(str(col))
df.columns = new_columns
#处理换行符,防止导出markdown时被拆分
df = df.map(lambda x: x.replace("\n", "
") if isinstance(x, str) else x)

print(df)
if df_postpass:
df_postpass(df)
return df.to_markdown(index=False)
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.