Created
November 9, 2017 03:40
-
-
Save toriwasa/37c690862ddf67d43cfd3e1af4e40649 to your computer and use it in GitHub Desktop.
Markdown形式で記述したテスト仕様書をExcelに変換するツール
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
import re | |
from pathlib import PurePath | |
from typing import Any, Dict # NOQA | |
import fire | |
import pandas as pd | |
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side | |
from openpyxl.styles.borders import BORDER_THIN | |
def convert_to_excel(input_path: str, output_sheet: str = 'Sheet1') -> None: | |
"""Markdown形式で記述したテスト仕様書をExcelに変換するツール | |
Arguments: | |
input_path: 変換対象のMarkdownファイルパス | |
output_sheet: 出力するExcelファイルのシート名。デフォルトはSheet1 | |
Returns: | |
None | |
実行するとインプットファイルと同じディレクトリに拡張子がxlsxになったファイルが出力される。 | |
変換するMarkdownは以下の形式で記述する:: | |
# テスト名 | |
## 大項目 e.g. APIインターフェース | |
### [正常|異常]/中項目 e.g. 異常/パラメータ指定なし | |
#### 小項目 e.g. パラメータ指定なし | |
1. 確認手順 e.g. パラメータなしでリクエストを行う | |
2. 確認手順 | |
* [ ] 想定動作 e.g. パラメータ指定不足エラーが返却される | |
""" | |
pure_input_path = PurePath(input_path.replace('\\', '/')) | |
output_path: str = str(pure_input_path.with_suffix('.xlsx')) | |
df_header = ( | |
'大項目', '正常系/異常系', '中項目', '小項目', '確認手順', '想定動作', | |
'確認手順最大文字数', '想定動作最大文字数' | |
) | |
# Excelデータ変換用の空データフレーム | |
df = pd.DataFrame(index=[], columns=df_header) | |
with open(input_path, 'r', encoding='utf-8') as input_file: | |
current_item_dict: Dict[str, Any] = { | |
'大項目': '', | |
'正常系/異常系': '', | |
'中項目': '', | |
'小項目': '', | |
'確認手順': '', | |
'想定動作': '', | |
'確認手順最大文字数': 0, | |
'想定動作最大文字数': 0 | |
} | |
for line in input_file: | |
# Markdownをパースしていく | |
if re.match(r'[0-9]+\. ', line): | |
current_item_dict['確認手順'] += line | |
# 文字数が最大なら更新しておく | |
if current_item_dict['確認手順最大文字数'] < len(line): | |
current_item_dict['確認手順最大文字数'] = len(line) | |
elif line.startswith('* [ ] '): | |
current_item_dict['想定動作'] += '・' + line[6:] | |
# 文字数が最大なら更新しておく | |
if current_item_dict['想定動作最大文字数'] < len(line): | |
current_item_dict['想定動作最大文字数'] = len(line) | |
elif current_item_dict['確認手順'] and current_item_dict['想定動作']: | |
# ここまでの項目をデータフレームに追加 | |
# 確認手順、想定動作については最後の改行コードを除いて追加する | |
se = pd.Series([ | |
current_item_dict['大項目'], | |
current_item_dict['正常系/異常系'], | |
current_item_dict['中項目'], | |
current_item_dict['小項目'], | |
current_item_dict['確認手順'][:-1], | |
current_item_dict['想定動作'][:-1], | |
current_item_dict['確認手順最大文字数'], | |
current_item_dict['想定動作最大文字数'] | |
], index=df.columns) | |
df = df.append(se, ignore_index=True) | |
# 終わったら初期化 | |
current_item_dict['確認手順'] = '' | |
current_item_dict['想定動作'] = '' | |
current_item_dict['確認手順最大文字数'] = 0 | |
current_item_dict['想定動作最大文字数'] = 0 | |
elif line.startswith('## '): | |
current_item_dict['大項目'] = line[3:-1] | |
elif line.startswith('### '): | |
current_item_dict['正常系/異常系'] = line[4:6] | |
current_item_dict['中項目'] = line[7:-1] | |
elif line.startswith('#### '): | |
current_item_dict['小項目'] = line[5:-1] | |
# ファイル終了時点の最後の項目を追加 | |
if current_item_dict['確認手順'] and current_item_dict['想定動作']: | |
se = pd.Series([ | |
current_item_dict['大項目'], | |
current_item_dict['正常系/異常系'], | |
current_item_dict['中項目'], | |
current_item_dict['小項目'], | |
current_item_dict['確認手順'][:-1], | |
current_item_dict['想定動作'][:-1], | |
current_item_dict['確認手順最大文字数'], | |
current_item_dict['想定動作最大文字数'] | |
], index=df.columns) | |
df = df.append(se, ignore_index=True) | |
# ここからExcelデータをwriterインスタンスに書き込む | |
writer = pd.ExcelWriter(output_path) | |
result_df = df.iloc[:, 0:6] | |
# 後でマージできるようにマルチインデックス化 | |
result_df = result_df.set_index(['大項目', '正常系/異常系', '中項目', '小項目']) | |
# 項目番号をカラムとして追加しておく | |
result_df['No'] = df.index + 1 | |
result_df = result_df.loc[:, ['No', '確認手順', '想定動作']] | |
result_df.to_excel(writer, sheet_name=output_sheet, merge_cells=True) | |
# ここからExcelデータの見た目を整えていく | |
worksheet = writer.sheets[output_sheet] | |
# 囲む罫線インスタンスを定義 | |
surround_border = Border( | |
left=Side(style=BORDER_THIN), | |
right=Side(style=BORDER_THIN), | |
top=Side(style=BORDER_THIN), | |
bottom=Side(style=BORDER_THIN) | |
) | |
# 記入列のヘッダー追加 | |
header_names = [ | |
'動作結果', '1st合否', '確認日', '確認者', | |
'動作結果', '2nd合否', '確認日', '確認者' | |
] | |
for col, header in zip('HIJKLMNO', header_names): | |
col_address = col + '1' | |
worksheet[col_address] = header | |
worksheet[col_address].border = surround_border | |
worksheet[col_address].alignment = Alignment( | |
horizontal='center' | |
) | |
# ヘッダーの改行 | |
worksheet['B1'] = '正常系\n/異常系' | |
# ヘッダーのスタイル | |
for col in 'ABCDEFGHIJKLMNO': | |
col_address = col + '1' | |
worksheet[col_address].font = Font(b=True, color='ffffff') | |
worksheet[col_address].alignment = Alignment( | |
vertical='center', | |
horizontal='center', | |
wrap_text=True | |
) | |
# 1回目結果 | |
if col in 'HIJK': | |
worksheet[col_address].fill = PatternFill( | |
patternType='solid', | |
fgColor='5079bd' | |
) | |
# 2回目結果 | |
elif col in 'LMNO': | |
worksheet[col_address].fill = PatternFill( | |
patternType='solid', | |
fgColor='505cbd' | |
) | |
else: | |
worksheet[col_address].fill = PatternFill( | |
patternType='solid', | |
fgColor='4f81bd' | |
) | |
# 列幅調整 | |
max_length_dict = {} | |
for col in ['大項目', '中項目', '小項目']: | |
max_length_dict[col + '最大文字数'] = df[col].map(len).max() * 2 | |
max_length_dict['正常系/異常系最大文字数'] = 9 | |
max_length_dict['No最大文字数'] = 4 | |
max_length_dict['確認手順最大文字数'] = df['確認手順最大文字数'].max() | |
max_length_dict['想定動作最大文字数'] = df['確認手順最大文字数'].max() * 1.5 | |
data_column_name = [ | |
'大項目', '正常系/異常系', '中項目', '小項目', | |
'No', '確認手順', '想定動作' | |
] | |
for col, k in zip('ABCDEFG', data_column_name): | |
max_length = max_length_dict[k + '最大文字数'] | |
worksheet.column_dimensions[col].width = max_length | |
# データセルのスタイル調整 | |
end_row = result_df.loc[:, 'No'][-1] | |
for n in range(end_row): | |
row = n + 2 | |
# 位置調整 | |
worksheet['E' + f'{row}'].alignment = Alignment(vertical='top') | |
worksheet['F' + f'{row}'].alignment = ( | |
Alignment(vertical='top', wrap_text=True) | |
) | |
worksheet['G' + f'{row}'].alignment = Alignment(vertical='top') | |
# 罫線 | |
for col in 'EFGHIJKLMNO': | |
worksheet[col + f'{row}'].border = surround_border | |
# 最後にWriteインスタンスを永続化する | |
writer.save() | |
if __name__ == '__main__': | |
fire.Fire(convert_to_excel) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment