本篇文章我们来练习使用openpyxl完成一个小需求,打造一个简单的低代码工具。在数据报表或日常工作中,我们经常会遇到收集表格中的数据生成报告的需求,如果是日报,每天都要从繁复庞大的表格中誊写数据,一不留心就出错了,能不能写一个程序可以输入表格输出对应的描述呢?当然是可以的,但是有很多类似的需求,每次都要重新修改一次程序吗?答案是否定的,此时我们就要定义一套自己的操作语言来灵活应对变化的需求。
看完本篇文章你将获得 成果见这里 的一个低代码工具,文本设计的是一种最简单的程式,不需要按照编译器的步骤来设计,定义一行是一个操作语句,只有常量定义语句和两个操作符,没有函数。
1. 读取代码并执行
需要注意的是 windows的txt编码可能是 utf-8 也有可能是 utf-8 BOM,utf-8 BOM在文件头会有一个不可见字符,如果用utf-8的方式去读utf-8 BOM文件就会出问题(显示的时候没问题,因为不可见字符输出时也不可见,但是程序中进行比较就会出问题),所以此处统一使用utf-8 BOM的方式读,即使读utf-8文件也不会出问题。handle_line是解析每行语句的函数。
program_path = sys.argv[1].lstrip()
with open(program_path,'r',encoding='utf_8_sig') as f:
line = f.readline()
while line:
handle_line(line)
line = f.readline()
2. 处理语句
这个层级的函数主要负责从一行语句中解析出操作符和参数,调用对应的函数(此处可以考虑将所有语句封装起来,耦合度会更低一些,该功能较简单,笔者暂未单独封装,只将复杂逻辑封装为单独函数)
def handle_line(line:str):
line = line.lstrip()
if(len(line) == 0 or line[0] == "#" or line[0] == '\n'):
return
if(line.find("@FROM") != -1):
k,v = line.split("@FROM")
k = k.strip()
v = v.strip()
# print(k+":"+v)
if(v.find("@USING") != -1):
table, sheet = v.split("@USING")
table = table.strip()
sheet = sheet.strip()
v = BaseExcel(table, data_only = True, sheet=sheet)
# print(k)
context[k] = v
elif line.find("@IS") != -1:
k,v = line.split("@IS")
k = k.strip()
v = v.strip()
context[k] = v
elif(line.find("@TO") != -1):
operation,target = line.split("@TO")
operation = operation.strip()
target = target.strip()
# print(line)
if line.find("@INJECT") != -1:
table, template = operation.split("@INJECT")
table = table.strip()
template = template.strip()
res = gen_desc(context[template],context[table])
else:
_,template = operation.split("@AUTOFILL")
template = template.strip()
res = gen_desc(context[template])
with open(target, 'a') as f:
f.write(res)
3. 生成描述
到了本文的核心部分,如何将模板中插的数据桩(【位置-格式】)标记转化为数据,本文设计了以下方式:
- 使用【和】将模板分成几段,放入列表
- 遍历段,将文字符合数据桩格式的段替换为数据(利用table.get_cell函数)
- 将段拼接起来,得到结果
def get_value(notation:str, table:BaseExcel):
note_list = notation.split('-')
# print(note_list)
if len(note_list) == 2:
position, data_type = note_list
else:
table, position, data_type = note_list
table = context[table]
return table.get_cell(position, data_type)
def gen_desc(template:str, table:BaseExcel=None):
template = re.split(r'[【】]',template)
for i in range(len(template)):
if re.match(".*-?([A-Z]+[0-9]+)-(D|F[0-9]+|P[0-9]+|C)", template[i]):
template[i] = get_value(template[i], table)
else:
pass
return "".join(template)
4. 简单的Excel操作类
该类提供与excel相关的所有操作,也是常量中实际存储的数据格式,由于该处需求较为简单,本类只实现了所需功能。
import openpyxl
from utils import nomalize
class BaseExcel():
def __init__(self, path, data_only=True, sheet=None):
self.path = path
self.sheet = sheet
self.workbook = openpyxl.load_workbook(path, data_only=data_only)
def using(self,sheet:str):
self.sheet = self.workbook[sheet]
def update_cell(self, sheet:str, position:str, value:any):
self.workbook[sheet][position] = value
def get_cell_with_sheet(self, sheet:str, position:str, format:str=None):
value = self.workbook[sheet][position]
value = nomalize(value)
return value
def get_cell(self, position:str, format:str=None):
value = self.workbook[self.sheet][position]
value = value.value
value = nomalize(value, format)
return value
5. 数据格式化
该部分代码很简单,但是也是核心需求,报表是一种展示,其展示的数据很有可能与表格中的形式不同,能够显式的指定数据是保证程序可用的关键一步,可见办公自动化看似简单的需求里面也包含很多需要注意的点,需要深入理解业务才能设计出真正能够提高效率的办公自动化软件。
def nomalize(value:any, format:str):
if value is None:
raise Exception("请检查数据单元格位置,读取到空值")
if format is None:return value
if format == 'D':
return str(int(round(value,0)))
if format[0] == 'F':
format = int(format[1:])
return str(round(value, format))
if format[0] == 'P':
format = int(format[1:])
return str(round(value*100, format))+"%"
else:
return value