作业批改:PDF压缩、PDF批注、发送邮件
目录:
PDF合并:pdftk、pymupdf
未试用,可见时间内应该不会用到。
PDF批注:Drawboard PDF(免费版)
使用体验:
- 的确能做到提笔写字;笔上两个按钮一个擦除、一个区域框选。这点使用体验和微软OneNote相同。
- 支持分页,以及手写批注。
- 打开用批注写作业的PDF比较慢,可能是因为要加载学生的批注?顺带,对于这些学生,改错时要小心擦除,因为可能不小心就擦掉了学生的作业……
- 所以先压缩,这样就不会有学生的批注了。
- 最不满意的地方:写字时会占满一个单核CPU(我是战66G2,轻薄本已连电源)。饶是如此,仍然会间或掉帧:体现为部分笔画未记录;与此同时,感觉笔触相当不跟手。
总体来说,我不是很喜欢这个软件,大概我还是很在意字迹跟手以及不掉帧的。Onenote做得很好,可惜不支持PDF手写批注或者分页。想知道评论区有没有建议。
PDF压缩:pymupdf
使用如下命令安装pymupdf
的两个包:
pip install fitz
pip install pymupdf
注意必须先安装fitz
再安装pymupdf
,顺序很重要!见:报错 import fitz no moduler named ‘frontend’
python实现PDF压缩提供了某种压缩方法。但是感觉非常丑陋(先创建并保存图片,再读取图片并合并)。
因此,我直接参考pymupdf教程,写了一版:
def compress(ori_file: str, new_file: str | None, *, dpi: int | None = None):
"""以dpi的分辨率打印文档,并重新输出到pdf
new_file=None时,覆盖原文件。"""
import fitz
input = fitz.open(ori_file) # open document
output = fitz.open()
for page in input: # iterate through the pages
pix: fitz.Pixmap = page.get_pixmap(dpi=dpi) # render page to an image
img = fitz.open(
"png", pix.tobytes("png")
) # make a PNG stream and open stream as PNG
imgPDF = fitz.open(
"pdf", img.convert_to_pdf()
) # make a PDF stream and open stream as PDF
output.insert_pdf(imgPDF)
img.close()
imgPDF.close()
input.close()
output.ez_save(new_file or ori_file)
output.close()
compress("try.pdf", "try2.pdf", dpi=100)
实测效果显著:
发送邮件
参考:python3使用smtplib发送邮件,带xlsx附件 - 腾讯云开发者社区-腾讯云
以下代码:
# encoding: utf-8
WEEK: int = 1 # 第几周
DRY_RUN = False # 是否为测试模式,测试模式不会真的发邮件
UNKNOWN_MAIL = "#N/A" # 无法获取邮箱时的邮箱地址
ALL_STUDENTS = {
"学生姓名": "邮箱@qq.com",
}
RECEIVER_NAMES: list[str] | None = None # None向所有人发邮件,否则向list中的人发邮件
class write_log(object):
"""记录日志
Init
- log = write_log("filename.log")
- log = write_log(open("filename.log", mode="a", encoding="utf-8"))
Call
- log("This is a log")
Close
- log.close()
"""
import io
def __init__(self, log_name: str | io.TextIOWrapper | None) -> None:
import io
import sys
import typing
if type(log_name) == str:
self.log = open(log_name, mode="a", encoding="utf-8")
elif type(log_name) == io.TextIOWrapper:
self.log = log_name
elif log_name is None:
# 默认输出到控制台
self.log = typing.cast(io.TextIOWrapper, sys.stdout)
else:
raise TypeError("log_name must be str or io.TextIOWrapper")
def __call__(self, *values: object, **kwds) -> None:
from datetime import datetime
timestamp = lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
print(timestamp(), *values, **kwds, file=self.log)
def close(self):
self.log.close()
class SendMail(object):
"""发送邮件
Init:
- send_email = SendMail("name@qq.com", "passwordpassword", "MyName")
- send_email = SendMail("name@qq.com", "passwordpassword", "MyName", log_file=log)
Call:
- send_email("receiver@example.com","title","This is the content.")
- send_email("receiver@example.com","title","This is the content.", receiver_name="Receiver Name")
- send_email("receiver@example.com","title","<p>This is the content.</p>", receiver_name="Receiver Name", content_style="html")
- send_email("receiver@example.com","title","<p>This is the content.</p>", receiver_name="Receiver Name", content_style="html", file_list=[("file/path/filename1.txt", "filename1.txt"),("file/path/filename2.txt", "邮件中显示的文件名.txt")])
"""
def __init__(
self,
sender: str,
sender_pwd: str,
sender_name: str | None,
*,
log_file: write_log = write_log(None),
):
from smtplib import SMTP_SSL
self.count = 0
self.total_size = 0
self.sender_address = sender
self.sender = (sender_name, sender) # 发件人
self.log = log_file
# SMTP服务器
self.server = SMTP_SSL("smtp.qq.com", 465, timeout=10)
# 登录账户
self.server.login(sender, sender_pwd)
self.log("已连接邮箱服务器……")
def __del__(self):
self.server.close()
def __call__(
self,
receiver: str, # 收件人
title: str, # 标题
content: str, # 内容
*,
receiver_name: str | None, # 收件人姓名
content_style: str = "plain",
file_list: list[tuple[str, str]] = [], # 文件列表 (文件位置,附件名)
):
"""
发送邮件
"""
self.count = self.count + 1
self.log(f"(第{self.count}封邮件) 正在生成……")
from email.mime.multipart import MIMEMultipart
from email import utils
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
# 创建一个带附件的实例
msg = MIMEMultipart()
# 时间
msg["Date"] = utils.format_datetime(utils.localtime())
# 发件人格式
msg["From"] = utils.formataddr(self.sender)
# 收件人格式
msg["To"] = utils.formataddr((receiver_name, receiver))
# 邮件主题
msg["Subject"] = title
# 邮件正文内容
msg.attach(MIMEText(content, content_style, "utf-8"))
# 添加附件
for file_path, file_name in file_list:
log(f" 正在添加附件:{file_name}")
# 构造附件
file = MIMEApplication(open(file_path, "rb").read())
# filename表示邮件中显示的附件名
file.add_header("Content-Disposition", "attachment", filename=file_name)
msg.attach(file)
# 发送邮件
log(
f" 正在发送给:{receiver_name or receiver}({receiver}), {len(msg.as_string())/2**20*3/4:5.2f}MB"
)
self.total_size += len(msg.as_string())
if not DRY_RUN:
self.server.sendmail(
self.sender_address,
receiver,
msg.as_string(),
)
def 获取附件名(文件夹: str) -> dict[str, list[tuple[str, str]]]:
from os import scandir
attachments: dict[str, list[tuple[str, str]]] = {}
with scandir(文件夹) as entries:
for entry in entries:
if entry.is_file():
attachments.setdefault(entry.name[0:ID_LENGTH], []).append(
(entry.path, entry.name)
)
return attachments
COL_NAME = 1
COL_ID = 0
COL_MAIL = 2
ID_LENGTH = 12 # 学号长度
if DRY_RUN:
log = write_log(f"第{WEEK}次作业邮件发送日志_dry.log")
log("DRY RUN模式")
else:
log = write_log(f"第{WEEK}次作业邮件发送日志.log")
if RECEIVER_NAMES:
log(f"收件人:{RECEIVER_NAMES}")
else:
log("即将向所有人发邮件")
if not DRY_RUN and input(f"第{WEEK}次,即将向所有人发邮件……(y/n):")[0] != "y":
log("已取消操作,日志结束")
exit()
# 标题
title = f"第{WEEK}次作业批改情况"
# 发送内容
content = (
lambda receiver, score, title_row, row: f"""<p><u>{receiver}</u>同学您好:</p>
<p>您本次作业分数为<u> {score} </u>。</p>
<p>
<table>
<colgroup>
{"".join("<col style='background-color:white'>" if i % 2 == 0 else "<col style='background-color:#E0E0E0'>" for i in range(COL_MAIL,len(title_row)))}
</colgroup>
<tbody>
<tr>
{"".join(f"<th>{i.value}</th>" for i in title_row[COL_MAIL+1:])}
</tr>
<tr>
{"".join(f"<td>{i}</td>" for i in row[COL_MAIL+1:])}
</tr>
</tbody>
</table>
</p>
"""
)
from openpyxl import load_workbook
import typing
workbook = load_workbook("作业评分.xlsx", read_only=True, data_only=True, keep_links=False)
table = workbook["sheet1"]
title_row = table[1]
send_email = SendMail("account@qq.com", "passwordpassword", "Name", log_file=log)
for row in table.iter_rows(3, values_only=True):
receiver_name = typing.cast(str, row[COL_NAME])
if RECEIVER_NAMES and receiver_name not in RECEIVER_NAMES:
continue
receiver = ALL_STUDENTS.get(receiver_name, UNKNOWN_MAIL)
if receiver == UNKNOWN_MAIL:
log(f"! 无法向{receiver_name}发送邮件(无邮箱)!")
continue
score = row[COL_MAIL + WEEK]
send_email(
receiver,
title,
content(receiver_name, score, title_row, row),
receiver_name=receiver_name,
content_style="html",
)
workbook.close()
log(f"共发送{send_email.count}封邮件,总计大小{send_email.total_size/2**20*3/4:5.2f}MB")
log("日志结束")
log.close()