はじめに
神エクセルとか、ネ申エクセルとか、方眼紙エクセルとか呼ばれる評判の悪い奴がありますよね。
本記事は、そいつをHTML形式のフォームにするシリーズ2回目です。pythonのopenpyxlを使用します。
前回の記事はこちらです。
今回
前回は、エクセルの行と列の幅を取り出すのに、結構ハマったので記事にしたのですが、その後はまりどころはほとんどなく、完成してしまいました。
pythonはそんなに得意というわけでもないので解説記事にするのは躊躇します。どうしようかと考えていましたが、面倒になったので解説なし!でコードを貼り付けておきます。
もし、これはもっと短く書ける!などご指摘がありましたらよろしくお願いいたします。
対応したもの
何もかもにも対応という訳にはいかず、対応したのは以下のみです。
エクセル中の表現 | 対応状況 |
---|---|
セルの結合状態 | 対応 |
セルの枠線 | hair、thin、medium、thickの4種類のみ対応 |
文字のアラインメント(水平方向) | center、left、right、bottom |
文字のアラインメント(鉛直方向) | center、top、None |
文字のフォント | 対応 |
文字のフォントサイズ | 対応 |
文字のフォントカラー | 対応 |
文字(太字) | 対応 |
特殊仕様
エクセルシート中のセルに、ドルマーク($)から始まる文字を書いておくと、
HTMLのフォームにする時に、そこがデータをPOSTメソッドで送るinput cellになります。
例えば、エクセルのセルに、
$data002
と入力しておくと、以下のようにname属性に、$に続く部分(data002)がセットされます。
<input class="defaultInput" type="text" name="data002">
使い方
普通にpythonで実行してください。引数4つが必要です。
usage: excellToHtmlLib.py [-h] P R S U
Converting excell form into HTLM form.
positional arguments:
P path to excell file
R cell range; e.g. A1:Z15
S Sheet name
U URL to send the form data to
optional arguments:
-h, --help show this help message and exit
今後
フォーム入力データのエラーチェック機能が含まれるようにしたいです。
例えば、必須項目が入力されているかのチェックをできるようにすることが考えられます。$*data002のように設定したら必須項目であることにするなどの指定方法が考えられますかね。
また、入力データがメールアドレスであるかや、数字であるかどうかをチェックしたり($に続けて正規表現を指定するなどの方法が考えられます)とかできるようにしたいです。
ただこれを実装するには、pythonからPHPコードを書き出すようにしなくてはならないですね。
またそれをやるとなると、データを受け取ってSQLデータベースにしまう機能を含ませたくなり、さらに、悪意のある攻撃に対してロバストにしたくなってログインして使うような形にしたくなり....ということで時間がかかりそうです(しばらく棚上げ)。
コード
自己責任でご利用ください。私は著作権を主張しませんので、好きにご利用ください。
import openpyxl
from argparse import ArgumentParser
def getOption():
argparser = ArgumentParser(description='Converting excell form into HTLM form.')
argparser.add_argument('path', metavar='P', type=str,help='path to excell file')
argparser.add_argument('range', metavar='R', type=str, help='cell range; e.g. A1:Z15')
argparser.add_argument('sheetName', metavar='S', type=str, default='Sheet1',help='Sheet name')
argparser.add_argument('fileURL', metavar='U', type=str, default='./url/to_Process_Form_Data.html',help='URL to send the form data to')
return argparser.parse_args()
def listColmunWidths(maxC,sheet):
defaultWidth = sheet.sheet_format.defaultColWidth * 6
columnwidth = [defaultWidth for i in range(maxC)]# デフォルトのカラム幅は70ピクセル
# カラムディメンションから、横幅を調べる。
# 連続する同じ幅のカラムが1つのディメンションが幅情報を持っている。
for xxx in sheet.column_dimensions:
cd = sheet.column_dimensions[xxx]
limit = maxC if cd.max > maxC else cd.max
for ind in range(cd.min,limit+1):
if cd.hidden == True:
columnwidth[ind-1] = 0
else:
columnwidth[ind-1] = cd.width*7 #widthの値を7倍したのが、ピクセル単位でのカラムの幅
return columnwidth
def listRowHeights(maxRow,sheet):
rowHeight = [20 for i in range(maxRow)]# デフォルトのカラム高は20ピクセル
rows = sheet.row_dimensions.keys()
for row in range(maxRow):
if row in rows: #rowは1_base
rowHeight[row-1] = sheet.row_dimensions[row].height
return rowHeight
def cellRect(cell,sheet,columnwidths,rowHeights):
if cell.__class__.__name__== "MergedCell":
#MergedCellなので、座標はない
return (-1,-1,-1,-1)
elif cell .__class__.__name__== "Cell":
#マージドセルに含まれているかどうか調べる
for rrr in sheet.merged_cells.ranges:
if rrr.__contains__(cell.coordinate):
#含まれている...マージドセル範囲の左上にあることを意味する
x = sum(columnwidths[0:cell.column-1])
y = sum(rowHeights[0:cell.row-1])
w = sum(columnwidths[rrr.min_col-1:rrr.max_col])
h = sum(rowHeights[rrr.min_row-1:rrr.max_row])
return (x,y,w,h)
#含まれていない
x = sum(columnwidths[0:cell.column-1])
y = sum(rowHeights[0:cell.row-1])
w = columnwidths[cell.column-1]
h = rowHeights[cell.row-1]
return (x,y,w,h)
def isTopCellBottomStyleNone(cell,sheet):#上のセルのボトムスタイルがNoneか調べる。マージドセルである場合も考慮
if cell.row == 1:
return False
topCell = sheet.cell(cell.row -1,cell.column)#一つ上のセル
for range in sheet.merged_cells.ranges:
if range.__contains__(topCell.coordinate):
topLeftCell = sheet.cell(range.min_row,range.min_col)
if topLeftCell.border.bottom.style == None:
return True
else:
return False
if topCell.border.bottom.style == None:
return True
else:
return False
def isLeftCellRightStyleNone(cell,sheet):#左のセルのレフトスタイルが設定されているか?左のセルが、マージドセルである場合も考慮
if cell.column == 1:
return False
leftCell = sheet.cell(cell.row ,cell.column-1)
for range in sheet.merged_cells.ranges:
if range.__contains__(leftCell.coordinate):
topLeftCell = sheet.cell(range.min_row,range.min_col)
if topLeftCell.border.right.style == None:
return True
else:
return False
if leftCell.border.right.style == None:
return True
else:
return False
def style_border(cell,sheet):#セルのボーダーについての設定を、htmlのスタイル指定形式で返す。#枠を共有する隣り合うセルそれぞれで枠を書いてしまうと、枠が太くなりすぎるので、そうならないように処理している。
style_border = ""
thickness = {'hair':'0.25','thin':'0.5','medium':'1.0','thick':'2.0'}
if cell.border.top.style in thickness:
style_border = style_border + "border-top-style: solid; border-top-width:" + thickness[cell.border.top.style] + "px;"
else:#一つ上のセルの、ボトムボーダースタイルがNoneなら、線無し(枠線が太く見えすぎないようにするため)。上のセルが結合されているかもしれないことを考慮している。
if isTopCellBottomStyleNone(cell,sheet):
style_border = style_border + "border-top-style: solid; border-top-width: 0px;"
else:
style_border = style_border + "border-top-style: solid; border-top-width: 0.01px; border-top-color: rgb(224, 224, 224);"
if cell.border.left.style in thickness:
style_border = style_border + "border-left-style: solid; border-left-width:" + thickness[cell.border.left.style] + "px;"
else:#一つ左のセルが、ライトボーダースタイルがNoneなら、線無し(枠線が太く見えすぎないようにするため)。上のセルが結合されているかもしれないことを考慮している。
if isLeftCellRightStyleNone(cell,sheet):
style_border = style_border + "border-left-style: solid; border-left-width: 0px;"
else:
style_border = style_border + "border-left-style: solid; border-left-width: 0.01px; border-left-color: rgb(224, 224, 224);"
if cell.border.right.style in thickness:
style_border = style_border + "border-right-style: solid; border-right-width:" + thickness[cell.border.right.style] + "px;"
else:
style_border = style_border + "border-right-style: solid; border-right-width: 0.01px; border-right-color: rgb(224, 224, 224);"
if cell.border.bottom.style in thickness:
style_border = style_border + "border-bottom-style: solid; border-bottom-width:" + thickness[cell.border.bottom.style] + "px;"
else:
style_border = style_border + "border-bottom-style: solid; border-bottom-width: 0.01px; border-bottom-color: rgb(224, 224, 224);"
return style_border
def style_location(x,y,w,h):
x = str(round(x,2))
y = str(round(y,2))
w = str(round(w,2))
h = str(round(h,2))
style_location = "left: " + x + "px " + ";" + "top: " + y + "px " + ";" + "width: " + w + "px " + ";" "height: " + h + "px " + ";"
return style_location
def style_alignment(cell):
alignment_info = ""
# 横方向
if cell.alignment.horizontal == 'center':
alignment_info = alignment_info + "justify-content: center;"
elif cell.alignment.horizontal == 'left':
alignment_info = alignment_info + "justify-content: left;"
elif cell.alignment.horizontal == 'right':
alignment_info = alignment_info + "justify-content: right;"
elif cell.alignment.horizontal == 'bottom':
alignment_info = alignment_info + "justify-content: bottom;"
# 縦方向
if cell.alignment.vertical == 'center':
alignment_info = alignment_info + "align-items:center;"
elif cell.alignment.vertical == 'top':
alignment_info = alignment_info + "align-items:flex-start;"
elif cell.alignment.vertical == None:##デフォルトで下付?
alignment_info = alignment_info + "align-items:baseline;"
return alignment_info
def style_font(cell):
style_font_info = "font-family: " + "'" + cell.font.name + "';" + "font-size: " + str(cell.font.sz) + "px" + ";"
return style_font_info
def exellToHtml(path,sheetName,exportRange,url):
wb = openpyxl.load_workbook(path,data_only=True)
sh = wb[sheetName]
# 列の幅、行の高さを保存するリストの作成
last_cell = sh[exportRange][-1][-1]
columnwidths = listColmunWidths(last_cell.column,sh)
rowHeights = listRowHeights(last_cell.row,sh)
# 出力が冗長にならないように、「スタイル設定用の文字列」(key)とスタイル名(value)を辞書で管理する。
# 辞書は、ボーダー用と、フォント用の2つ作る。
style_border_dic = {} # 空の辞書
style_border_count = 1;
style_font_dic = {}# 空の辞書
style_font_count = 1;
style_list = []
outputString = ""# 出力する文字列
for row in sh[exportRange]:# ユーザーが指定した範囲のセルを1つ1つ調べる
values = []
for cell in row:
debug = cell.__class__.__name__
if cell.__class__.__name__== "MergedCell":# 結合されたセルは除外
continue
# セルの座標
x,y,w,h = cellRect(cell,sh,columnwidths,rowHeights)
style_location_info = style_location(x,y,w,h)
style_font_info = style_font(cell)
style_border_info = style_border(cell,sh)
alignment_info = style_alignment(cell)
val = str(cell.value) if cell.value != None else ""
# 太字設定
if cell.font.b and val != "" and val[0] != '$':
val = "<b>" + val + "</b>"
# フォントカラーの設定。ハイパーリンクにセットされる色などは未対応
if cell.font.color != None:
argb = cell.font.color.rgb
if argb.__class__.__name__ == 'str':
val = "<font color = \"#" + argb[2:] + "\">" + val + "</font>"
# 辞書で管理。辞書のkeyはボーダー設定用文字列、valueはスタイル名(連番。sb1, sb2 ....)とする。
# divのclass属性にスタイル名を、
# htmlのheadの<style>タグ中にkeyを書き出すようにする。
if style_border_info not in style_border_dic:##ボーダー設定
border_StyleName = "sb" + str(style_border_count)
style_border_dic[style_border_info] = border_StyleName
style_border_count = style_border_count + 1
style_list.append("." + border_StyleName + "{" + style_border_info + "}")
else:
border_StyleName = style_border_dic[style_border_info]
# 辞書でフォント情報管理。valueはスタイル名(連番。sf1, sf2 ....)とする。
if style_font_info not in style_font_dic:##フォント設定
font_StyleName = "sf" + str(style_font_count)
style_font_dic[style_font_info] = font_StyleName
style_font_count = style_font_count + 1
style_list.append("." + font_StyleName + "{" + style_font_info + "}")
else:
font_StyleName = style_font_dic[style_font_info]
if len(val) != 0 and val[0] == '$':
aDiv = " <div class = \"inputCell " + border_StyleName + " " + font_StyleName + "\" style=\"" + style_location_info + alignment_info + "\">" + "<input class=\"defaultInput\" type=\"text\" name=\""+ val[1:] + "\"></div>" + "<!-- " + cell.coordinate + " -->" +"\n"
else:
aDiv = " <div class = \"flexbox " + border_StyleName + " " + font_StyleName + "\" style=\"" + style_location_info + alignment_info + "\">" + val + "</div>" + "<!-- " + cell.coordinate + " -->" +"\n"
outputString = outputString + aDiv
outputString = outputString + "\n"
# 送信ボタン ... 書類の左下に置く。
lastRowFirstCell = sh[exportRange][-1][-1]##右下のセル
last_x,last_y,last_w,last_h = cellRect(lastRowFirstCell,sh,columnwidths,rowHeights)
submit_button = "<div class= \"flexbox\" style=\"left: 0px ;top: " + str(last_y + last_h) + "px ;width: " + str(last_w) + "px ;height: 50px ;\"><input type=\"submit\" value=\"送信!\"></div>"
# html化するのに必要な情報
finalString = """
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>GoodbyeKamiExell</title>
<meta name="description" content="excell2html">
<style>
*, *:before, *:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-o-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
}
.clearfix::after {
content : " ";
display : block;
clear : both;
}
.flexbox{
/*jborder:solid 0.2px rgb(100,100, 100); */
padding: 0px;
display: flex;
align-items: center;/* 縦方向中央揃え */
/*justify-content: center; 横方向中央揃え */
position:absolute;
white-space:nowrap;
}
.inputCell{
border:solid 0.1px rgb(200, 200, 200);
position:absolute;
}
.defaultInput{
height:100%;
width:100%;
padding:5px;
background-color:rgb(236, 255, 234);
/*border: 0px;*/
}
input:focus {
outline: 2.1px solid rgb(68, 96, 255); /* 線幅、線のスタイル、カラー */
outline-offset: -1px; /* 対象の要素からの距離、マイナス(内側)にも対応 */
}
""" + "\n ".join(style_list) + """
</style>
</head>
<body>
<form action=\"""" + url + """\" method=\"POST\">
<div class="clearfix">
"""+ outputString + """
</div>
""" + submit_button + """
</form>
</body></html>
"""
print(finalString)
args = getOption()
exellToHtml(path = args.path,sheetName = args.sheetName,exportRange = args.range,url = args.fileURL)