♻️

Python(openpyxl)でExcelをHTMLに変換する試み

2023/08/31に公開

はじめに

今回はExcelで作った文書のレイアウトを、PythonでHTMLに変換するツールを作成してみました。
サンプルで使った文書は、いわゆるExcel方眼紙の稟議書です。

結果

実行結果から書いてしまいますが、Pythonのライブラリ「openpyxl」を使うことで、
Excelのセルの情報を読み取り、それっぽくHTML+CSSに変換できることができました!
ですが、再現性を上げるには装飾の反映や、マージセルの考慮などが大変なことも分かりました。
処理の流れを解説しますが、罫線の種類、背景色、文字色の指定などいろいろ未完成です💦

処理の流れ

【1】メイン処理

エントリーポイントで、以下の処理をおこないます。

  1. 指定パスのExcelファイルとワークシートを開きます。
    wb = openpyxl.load_workbook(excel_path, data_only=True)
    ws = wb.active
  2. Excelファイルのセル構成を読み取ってHTMLに変換し、htmlファイルに出力します。
  3. Excelファイルのセル装飾を読み取ってCSSを生成し、cssファイルに出力します。
main.py
import openpyxl
import generate_css_styles
import excel_to_html

# ファイルパスを定義
excel_file_path = "excel.xlsx"
html_output_path = "output.html"
css_output_path = "styles.css"

def generate_html_and_css_files(excel_path, css_path, html_path):
    wb = openpyxl.load_workbook(excel_path, data_only=True)
    ws = wb.active
    
    generate_html_file(ws, html_path)

    generate_css_file(ws, css_path)

    print("完了しました。")

def generate_html_file(ws, html_path):
    html_content = excel_to_html.convert(ws)
    with open(html_path, "w", encoding="utf-8") as html_file:
        html_file.write(html_content)

    print("HTMLファイルを作成しました。")

def generate_css_file(ws, css_path):
    css_styles = generate_css_styles.generate(ws)
    with open(css_path, "w", encoding="utf-8") as css_file:
        css_file.write(css_styles)

    print("CSSファイルを作成しました。")

if __name__ == "__main__":
    generate_html_and_css_files(excel_file_path, css_output_path, html_output_path)

【2】HTMLへの変換処理

メイン処理から呼ばれて、セル構成から変換したHTML情報を返します。

  1. Excelシートからセル内容を取得し、HTMLのtable形式に変換します。
    ws.iter_rows()
  2. 各tdにはCSSのclass名を付与します。
  3. マージセルにはrowspan、colspanを付与します。
    ws.merged_cells
excel_to_html.py
def convert(ws):
    table_content = convert_to_html_table(ws)
    html_template = generate_html_template(table_content)
    return html_template

def convert_to_html_table(ws):
    table_content = ""
    for row_idx, row in enumerate(ws.iter_rows(), start=1):
        table_content += "<tr>"
        for col_idx, cell in enumerate(row, start=1):
            cell_value = cell.value if cell.value is not None else ""
            cell_value = newline_to_html_breaks(str(cell_value))
            css_class = f"cell-{row_idx}-{col_idx}"
            
            rowspan, colspan = get_col_row_span(ws, cell)
            table_content += f'<td class="{css_class}" rowspan="{rowspan}" colspan="{colspan}">{cell_value}</td>'
        table_content += "</tr>"
    return table_content

def newline_to_html_breaks(text):
    return text.replace('\n', '<br>').replace('\r\n', '<br>')

def get_col_row_span(ws, cell):
    rowspan = 1
    colspan = 1

    if cell.coordinate in ws.merged_cells:
        for merged_range in ws.merged_cells.ranges:
            if cell.coordinate in merged_range:
                rowspan = merged_range.max_row - merged_range.min_row + 1
                colspan = merged_range.max_col - merged_range.min_col + 1
                break

    return rowspan, colspan

def generate_html_template(table_content):
    html_template = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <link rel="stylesheet" type="text/css" href="styles.css">
    </head>
    <body>
        <table class="excel-table">
            {table_content}
        </table>
    </body>
    </html>
    """
    return html_template

【3】CSSの生成処理

メイン処理から呼ばれて、セル装飾から生成したCSS情報を返します。
繰り返しになりますが未完成です💣💥

  1. ExcelからHTMLに変換したtableのCSSを定義します。
  2. セルの罫線、文字のサイズ、太字、配置、セルの幅高さなどから、各tdのCSSを生成します。
    cell.border
    cell.font.size
    cell.font.bold
    cell.alignment
    ws.row_dimensions[cell.row].height
generate_css_styles.py
import openpyxl
from openpyxl.utils import get_column_letter

def generate(ws):
    css_styles = generate_table_styles()

    for row in ws.iter_rows():
        for cell in row:
            css_styles += generate_cell_styles(ws, cell)
            css_styles += generate_merged_cell_styles(ws, cell)

    return css_styles

def generate_table_styles():
    return """
.excel-table {
    border-collapse: collapse;
}
"""

def generate_cell_styles(ws, cell):
    css_properties = []

    css_properties.append(generate_bold_styles(cell))
    css_properties.append(generate_border_styles(cell))
    css_properties.append(generate_width_styles(ws, cell))
    css_properties.append(generate_height_styles(ws, cell))
    css_properties.append(generate_font_styles(cell))
    css_properties.append(generate_alignment_styles(cell))
    css_properties = list(filter(lambda x: x != "", css_properties))

    css_indent = "    "
    css_class = f".cell-{cell.row}-{cell.column} {{\n{css_indent}"
    css_class += f"\n{css_indent}".join(css_properties)
    css_class += "\n}\n"
    return css_class

def generate_bold_styles(cell):
    if cell.font and cell.font.bold:
        return "font-weight: bold;"

    return ""

def generate_border_styles(cell):
    border_sides = ["top", "left", "right", "bottom"]
    border_styles = []

    for border_side in border_sides:
        border = getattr(cell.border, border_side)
        if border.style and border.style != "none":
            border_color = border.color.rgb[2:] if border.color else "000000"  # Default color is black
            border_styles.append(f"border-{border_side}: solid 1px #{border_color};")

    if border_styles:
        return " ".join(border_styles)

    return ""

def generate_width_styles(ws, cell):
    col_width = ws.column_dimensions[get_column_letter(cell.column)].width
    col_width = points_to_pixels(col_width)
    return f"width: {col_width}px;"

def generate_height_styles(ws, cell):
    row_height = ws.row_dimensions[cell.row].height
    return f"height: {row_height}px;"

def generate_font_styles(cell):
    return f"font-size: {cell.font.size}px;"

def generate_alignment_styles(cell):
    horizontal_alignment = cell.alignment.horizontal
    if horizontal_alignment in ["left", "center", "right"]:
        return f"text-align: {horizontal_alignment};"

    return ""

def generate_merged_cell_styles(ws, cell):
    css_styles = ""
    for merged_range in ws.merged_cells.ranges:
        if cell.coordinate not in merged_range:
            continue

        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):
                if row != merged_range.min_row or col != merged_range.min_col:
                    merged_cell_class = f".cell-{row}-{col}"
                    css_styles += f"{merged_cell_class} {{ display: none; }}\n"
    return css_styles

def points_to_pixels(points):
    pixels = points * 1.33
    return round(pixels)

おわりに

https://openpyxl.readthedocs.io/en/stable/

openpyxlはExcelファイルを強力かつ柔軟に操作できるため、データ操作や自動レポート作成などExcel業務の効率化にもいろいろ活用できそうです。
活Excelの際に、ご利用を検討してみてはいかがでしょうか。

HTML変換の際は、当記事が少しでも参考になれば幸いです🫠

コラボスタイル Developers

Discussion