学校业务自动化 - 生活教育委员会文件自动化

힘센캥거루
2024년 12월 22일
62
python

学校业务自动化 - 生活教育委员会文件自动化-1

怀揣教育梦想成为教师,却在非教育事务上耗费了过多时间。

因此,在这篇文章中,我打算介绍生活教育委员会的出勤事务处理自动化

指导学生的过程无法自动化,但文件处理过程可以自动化。

过长的部分用链接代替。

1. 生活教育委员会

原本生教委的目标学生需经过以下事务处理流程。

学校业务自动化 - 生活教育委员会文件自动化-2

每个学生至少需要4份文件,10人就是40份。

其中,出勤事务有相当多复杂的部分。

必须逐一记录学生的未认可出勤在文件上。

学校业务自动化 - 生活教育委员会文件自动化-3

原来是从班主任那里收到学生的出勤记录,以韩文文档逐一确认后手动转抄。

这个过程太繁琐,所以决定用spreadsheet和python一举完成

2. 想法

学校业务自动化 - 生活教育委员会文件自动化-4

在编程中,比技能更重要的是想法本身。

考虑如何自动化文档撰写过程。

  1. 从需要的数据中提取重复值,形成并分发电子表格。
  2. 获得因未认可出勤而成为生教委对象学生的未认可出勤天数及班主任意见书。以往是以印刷品进行收集,现可通过spreadsheet汇总。
  3. 使用spreadsheet API获取数据。
  4. 使用pandas清洗数据。
  5. 利用韩文文件的输入框和pywin32制作hwp, hwpx格式的文件。
  6. 请班主任及学生确认事项后,一并扫描并上报审批。

通过这些过程实现文档生成的自动化。

3. 所需数据收集

学校业务自动化 - 生活教育委员会文件自动化-5

在开始之前,先看看各个文件需要哪些数据。

  • 学生自辩书
    • 姓名学号性别事项标题学生的未认可出勤,学生姓名
  • 事实确认书(班主任填写)
    • 学生姓名学号性别事项标题,班主任姓名
  • 学生事项调查报告
    • 姓名学号性别事项标题事项发生日期时间,事项发生地点,相关学生,事项内容,相关学校生活规定撰写日期,撰写教师
  • 学生生活教育委员会出席及意见提交请求书
    • 举行日期时间,举行地点,姓名学号事项发生日期时间事项发生地点违规行为相关规定撰写日期
  • 书面意见书
    • 相关学生姓名学号,保护者姓名,学生与保护者的关系
  • 信封
    • 学生姓名,邮政编码,地址

其中,共同需要的数据用粗体标出。

如果将这些整理成spreadsheet,可以通过去除数据重复来设置列的索引。

然后来制作表格吧。

4. 电子表格制作及API设置

表格制作如下所示。

学校业务自动化 - 生活教育委员会文件自动化-6

  • 学生基本信息
    • 学号,姓名,姓名
  • 内容
    • 事项编号,发生日期时间,未认可出勤,班主任意见,撰写日期,生教委日期

此处事项内容、违规行为、可适用的学校生活规定缺失的原因是因该事项涉及出勤事务。

现在需要设置访问该电子表格的API。

学校业务自动化 - 生活教育委员会文件自动化-7

原本想涵盖API设置,但这样撰写需要2周时间,于是仅以链接替代。

spreadsheet的API设置请参考官方文档junsugi的velog

大致的spreadsheet API设置流程如下。

  1. API使用设置
  2. OAuth同意屏幕配置
  3. 桌面应用的用户认证信息批准
  4. 安装Google客户端库
  5. 使用

5. 韩文文档设置

在使用python之前需要进行韩文文档设置。

由于只通过编程处理所有事情耗时较长,因此使用输入字段来大致确定数据输入位置。

学校业务自动化 - 生活教育委员会文件自动化-8

在学生事项调查报告、学生自辩书等文档的数据输入位置上分别输入字段名。

此时需要注意的是字段名与spreadsheet的列名需一致

我在以下文档中指定了格式。

学校业务自动化 - 生活教育委员会文件自动化-9

如有问题请参考日常编程(日常)的Tistory

6. python代码

首先用pip安装所需的库。

pip install pywin32 pandas google-api-python-client google-auth-httplib2 google-auth-oauthlib

然后创建py或ipynb文件,导入所需模块。

根据经验,管理中所需的变量位于顶部较为方便,因此路径或班主任的名字位于最上方。

如果数量多,可以用json文件管理。

import win32com.client as win32
import pandas as pd
import time
import pathlib
import datetime as dt
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

hwp = win32.gencache.EnsureDispatch('HWPFrame.HwpObject')
hwp.RegisterModule('FilePathCheckDLL', 'SecurityModule')

rootpath = pathlib.Path.cwd()/"school"/"guidance"
errorfilepath = rootpath / "错误报告.txt"
beforefilespath = rootpath / "format" / "before"
rulesdfpath = rootpath / "生教委_名单.xlsx"
addressdfpath = rootpath / "format" / "info.xlsx"
phonedfpath = rootpath / "format" / "phoneNumber.xlsx"
lastsavepath = rootpath / "created"
backuppath = rootpath / "backup"
lastsavepath.mkdir(exist_ok=True)
        
teachers = {301:"김ㅇㅇ", 302:"박ㅇㅇ", 303: "남궁ㅇ", 304:"사공ㅇ"} 

从官方文档中获取电子表格所需代码。

看似复杂,但实际上是接收表的ID和范围返回数据的形式。

def main(spreadID, spreadRange):
    SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]

    creds = None
    
    if pathlib.Path(f"{rootpath}/jsonFiles/token.json").exists():
        creds = Credentials.from_authorized_user_file(f"{rootpath}/jsonFiles/token.json", SCOPES)

    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                f"{rootpath}/jsonFiles/credentials.json", SCOPES
            )
            creds = flow.run_local_server(port=0)
            
        with open(f"{rootpath}/jsonFiles/token.json", "w") as token:
            token.write(creds.to_json())

    try:
        service = build("sheets", "v4", credentials=creds)

        sheet = service.spreadsheets()
        result = (
            sheet.values()
            .get(spreadsheetId=spreadID, range=spreadRange)
            .execute()
        )
        values = result.get("values", [])

        if not values:
            print("No data found.")
            return
    
        return values
    except HttpError as err:
        print(err)

现在是将返回的数据加工成dataframe的阶段。

去掉不需要的数据并加工从表格获取的信息。

根据学号匹配适用的学校生活规则、学生地址等。

def load_checkdata():
    # 调用电子表格并加工数据
    SPREADSHEET_ID = "你的电子表格API key"
    SPREADRANGE = "表名!A8:V200"
    sheet = main(SPREADSHEET_ID, SPREADRANGE)
    df_origin = pd.DataFrame(sheet[1:], columns=sheet[0])
    df_origin.drop("序号", axis=1, inplace=True)
    df_origin.reset_index(drop=True, inplace=True)
    df_origin.dropna(subset="学号", inplace=True)
    df_origin = df_origin.loc[df_origin['学号'] != ""]
    df_origin.reset_index(drop=True, inplace=True)
    df_origin.fillna("", inplace=True)
    df_origin["事项标题"] = "出勤不良"
    df_origin["事项发生地点"] = "无"
    df_origin["学生生活规则"] = "1项 经常无故缺勤的学生"
    df_origin["违规行为"] = "出勤不良"
    df_origin["完成与否"] = df_origin["完成与否"].str.replace("TRUE", "完成")
    df_origin.rename(columns={"班主任意见":"事项内容"},inplace=True)
    
    # 有时学号不识别,转换为数字
    excel["学号"] = pd.to_numeric(excel["学号"])
    excel.sort_values("学号", inplace=True)
    excel.reset_index(drop=True, inplace=True)

    # 输入班主任姓名
    excel["班主任"]="班主任"
    for student in range(len(excel)):
        studentClass = int(excel.loc[student, "学号"])//100
        excel.loc[student, "班主任"] = teachers[studentClass]

    excel = excel.dropna(subset=["学号"])
    excel = excel.drop(excel[excel['完成与否']=="完成"].index)
    
    # 以学号为基准输入地址
    address = pd.read_excel(addressdfpath)
    phone = pd.read_excel(phonedfpath)
    rules = pd.read_excel(rulesdfpath, sheet_name="Sheet2")
    
    excel = pd.merge(excel, address, on="学号", how="left")
    excel = pd.merge(excel, phone, on="学号", how="left")
    
    # 输入违规项目
    add_columns = ["违规项目"]
    excel[add_columns] = ""
    for i in range(len(excel)):
        # 收集违规项目
        student_rules = []
        for j in range(len(rules)):
            if rules.loc[j, "内容"] in excel.loc[i, "学生生活规则"]:
                student_rules.append(rules['项目'][j])
        excel.loc[i, "违规项目"] = ", ".join(student_rules)
    
    excel.reset_index(drop=True, inplace=True)
    excel['学号']=excel["学号"].astype(int)
    if len(excel) < 1 :
        return 

    # 为防止原本文档损坏,进行备份
    backupfilename = dt.datetime.now().strftime("%Y%m%d %H")
    excel.to_excel(backuppath / f"{backupfilename}_backup.xlsx")

    return excel

这样就得到了包含学生地址、邮政编码的dataframe。

虽然dataframe的列较长,但内存充足,不必担心。

常用的代码进行了模块化。

# 复制页面
name.SetActive_XHwpDocument()
if len(df) > 1:
    hwp.MovePos(3)
    hwp.Run('SelectAll')
    hwp.Run('Copy')
    hwp.MovePos(3)
    for i in range(len(df)-1):
        hwp.MovePos(3)
        hwp.Run('Paste')
        hwp.MovePos(3)  
    hwp.Run('DeleteBack')
hwp.Run('DeleteBack')

# 将指定韩文文档的输入框中输入文本
# 激活名为name的韩文文档,根据df输入数据
name.SetActive_XHwpDocument()
for page in range(len(df)):
        for field in field_lst :  
            hwp.PutFieldText(f'{field}{{{{{page}}}}}', df[field].iloc[page])

现在利用这些数据生成韩文文件。

通过load_checkdata获取数据,利用pywin32在各文档的输入框中输入合适的值。

抱歉,代码有点长。

如果觉得这是个烂代码,只参考我的思路即可。

def start():
    # 首先加载Excel数据。
    excel_check = load_checkdata()
    if excel is None:
        return
        
    if len(excel) < 1 :
        return

    # 打开格式文件夹内的所有文件
    beforefiles = list(beforefilespath.iterdir())
    for file in beforefiles:
        hwp.Run('FileNew')        
        hwp.Open(file)

    # 变量名指定
    offical = hwp.XHwpDocuments.Item(1)
    report_check = hwp.XHwpDocuments.Item(2)
    student_excuse = hwp.XHwpDocuments.Item(3)
    confire = hwp.XHwpDocuments.Item(4)
    mail = hwp.XHwpDocuments.Item(5)
    demand = hwp.XHwpDocuments.Item(6)
    
    # 撰写公文
    offical.SetActive_XHwpDocument()
    dateDict = {0: '周一', 1:'周二', 2:'周三', 3:'周四', 4:'周五', 5:'周六', 6:'周日'}
    date_origin = excel['生教委举行日'].iloc[0]
    date = dt.datetime.strptime(str(date_origin), '%Y年 %m月 %d日')
    dow = dateDict[date.weekday()]
    date_strf = dt.datetime.strftime(date, '%Y.%m.%d.')
    
    once = ["生教委举行日", "星期", "事项", "人数"]
    tables = ["违规行为", "学号", "姓名", "事项编号"]
    guidanceType = excel["违规行为"].unique()
    for one in once : 
        if one == "星期":
            hwp.PutFieldText(f'{one}{{{{{0}}}}}', dow)
            continue
        elif one =="事项":
            if len(guidanceType) == 1:
                    hwp.PutFieldText(f'{one}{{{{{0}}}}}', f"{guidanceType[0]} 1项")
            else :
                hwp.PutFieldText(f'{one}{{{{{0}}}}}', f"{guidanceType[0]}{len(guidanceType)-1}项")
            continue
        elif one =="人数":
            hwp.PutFieldText(f'{one}{{{{{0}}}}}', len(excel))
            continue

        hwp.PutFieldText(f'{one}{{{{{0}}}}}', date_strf)

    for student in range(len(excel)):
        for table in tables :
            if table == "学号":
                studentGrade = int(excel['学号'].iloc[student])//10000
                studentClass = int(excel['学号'].iloc[student])//100 - studentGrade*100
                hwp.PutFieldText(f'学号{{{{{student}}}}}', f"{studentGrade}-{studentClass}")
                continue
            if table == "姓名":
                studentName = excel['姓名'].iloc[student]
                hwp.PutFieldText(f'姓名{{{{{student}}}}}', f"{studentName[0]}{(len(studentName)-1)*'O'}")
                continue
            hwp.PutFieldText(f'{table}{{{{{student}}}}}', excel[table].iloc[student])
    hwp.SaveAs(lastsavepath / beforefiles[0].name)

    # 撰写事项调查报告
    report_check.SetActive_XHwpDocument()
    field_list = [i for i in hwp.GetFieldList().split('\x02')]
    copypage(report_check, excel_check)
    fillpage(report_check, excel_check, field_list)
    hwp.SaveAs(lastsavepath / beforefiles[1].name)

    # 撰写自辩书
    student_excuse.SetActive_XHwpDocument()
    field_list = [i for i in hwp.GetFieldList().split('\x02')]
    copypage(student_excuse, excel_check)
    fillpage(student_excuse, excel_check, field_list)
    hwp.Run('DeleteBack')
    hwp.SaveAs(lastsavepath / beforefiles[2].name)

    # 撰写收件确认书
    confire.SetActive_XHwpDocument()
    field_list = [i for i in hwp.GetFieldList().split('\x02')]
    copypage(confire, excel)
    fillpage(confire, excel, field_list)
    hwp.SaveAs(lastsavepath / beforefiles[3].name)
    
    # 撰写信封格式
    mail.SetActive_XHwpDocument()
    field_list = [i for i in hwp.GetFieldList().split('\x02')]
    copypage(mail, excel)
    fillpage(mail, excel, field_list)
    for page in range(len(excel)):
        hwp.PutFieldText(f'邮政编码{{{{{page}}}}}', (" ").join(list(str(int(excel["邮政编码"].iloc[page])))))
    hwp.SaveAs(lastsavepath / beforefiles[4].name)

    # 撰写生教委通知及出席要求书
    demand.SetActive_XHwpDocument()
    field_list = [i for i in hwp.GetFieldList().split('\x02')]
    copypage(demand, excel)
    fillpage(demand, excel, field_list)
    todayDate = dt.date.today().strftime("%Y年 %m月 %d日")
    for page in range(len(excel)):
        hwp.PutFieldText(f'撰写日期{{{{{page}}}}}', todayDate)
    hwp.SaveAs(lastsavepath / beforefiles[5].name)
    hwp.Quit()

这样,只要从班主任那里通过spreadsheet获得出勤相关内容后执行一次start()函数即可轻松完成。

当然,亲自面见学生确认事实是必需的,但文档撰写所耗时间大大缩短。

用这种方式可以将生教委中的事前文档、事后文档、特别教育委托书等所有文档过程自动化

7. 结束语

学校业务自动化 - 生活教育委员会文件自动化-10

有人可能会问这样的问题。

这样制作代码不是更花时间吗?

当然,为生活教育委员会文档撰写所需时间相对花在编写和学习代码上的时间长一些。

但是,前者对自我发展无益,而后者则能带来更多帮助。

同时,可以将节省下来的时间更多地用于课程准备。

学校业务自动化 - 生活教育委员会文件自动化-11

可惜,暂时无法期待教育厅或教育部为教师们制作这样的程序。

可能将来也无法期待。

因此我们必须自力更生。

希望这篇文章能给某些人带来启发。

댓글을 불러오는 중...