问题背景
在尝试制作实验报告分类器,统计考勤数据等功能的过程中;发现有很多功能是相似的,因此本文将这些相似的功能抽象化,形成了一个拓展类。以方便后续的使用。
代码
文件处理拓展
class file_tools:
"""
本类是对文件的一些拓展操作
依赖库为 os, shutil
目前实现的功能有:
1. copy 两个文件之间的拷贝, 新文件所在路径可以不存在
2. copy_fold 一个文件夹内所有文件向另一个文件的拷贝, 新文件路径可以不存在
3. get_student_id 获取学号信息, 如果失败会返回None, False
4. get_extend_name 获取文件的拓展名信息, 如果不是文件则返回None
"""
@staticmethod
def copy(old_filepath, new_filepath):
"""
copy函数实现了在一个已有的文件到一个位置路径的拷贝
如果路径不存在, 则会自动创建一个新的路径(文件夹)
如果原路径不是一个文件, 或者拷贝过程出现失败, 都会返回False
正常拷贝结果返回是True
"""
from shutil import copyfile
import os
if os.path.isfile(old_filepath) == False:
return False
#获取到新的路径的文件夹路径
fold_path = os.path.split(new_filepath)[0]
try:
#如果没有对应的文件夹, 那么创建一个新的
if os.path.exists(fold_path) == False:
os.mkdir(fold_path)
#拷贝文件
copyfile(old_filepath , new_filepath)
return True
except:
print(r'拷贝文件失败, 请检查!')
return False
@staticmethod
def copy_fold(old_fold, new_fold):
"""
本函数实现两个文件夹下所有文件的拷贝
其中不包括子文件夹及其内容的拷贝
实现细节是枚举每一个文件, 然后进行拷贝
返回的是 拷贝数量, 是否全部拷贝完成
"""
import os
count_copy = 0
all_flag = True
#枚举每一个文件
for path in os.listdir(old_fold):
if os.path.isfile(path) == False:
continue
#构造文件路径, 和目标路径
old_path = old_fold + '\\' + path
new_path = new_fold + '\\' + path
#调用单个文件的拷贝函数, 根据返回结果进行进一步判定
if copy(old_path, new_path) == False:
print(r'文件 ',file,r' 拷贝失败')
all_flag = False
else:
count_copy += 1
return count_copy, all_flag
@staticmethod
def get_student_id(filepath,id_list=None):
"""
完成学号的提取, 要求是学号必须是在文件名的最前面, id_list中最好是字符串类型
另外还可以根据第二个参数id_list判断学号是不是在名单中, 默认所有学号都在
返回学号, ok的模式, 其中学号是在list中的学号.
如果提取失败或学号不在list中. 返回None, False
"""
import os
#首先分割文件名, 如果是文件的话, 提取出文件名来
basepath = os.path.split(filepath)[1]
#先根据学号是的格式尝试提取学号来判定是不是满足题目条件
id0 = None
if len(basepath) >= 9 and basepath[0] == '1':
id0 = basepath[:9]
elif len(basepath) >= 10 and basepath[0] == '2':
id0 = basepath[:10]
else:
return None, False
#根据学号全是数字来判断是不是满足
if id0.isdigit() == False:
return None, False
#根据传入的id_list来判断是不是在list中
if (id_list == None) or (id_list != None and str(id0) in str(id_list)):
return id0, True
return None, False
@staticmethod
def get_extend_name(filepath):
"""
本函数实现了获取文件拓展名的功能, 方便后续进行对应的文件处理
如果传入不是文件, 或者获取拓展名失败, 则返回None. 否则返回拓展名(str),不带'.'
"""
import os
#判断是不是文件
if os.path.isfile(filepath) == False:
return None
#对文件名进行进一步操作
basename = os.path.split(filepath)[1]
if basename != None and '.' in basename:
return (basename.split('.'))[-1]
return None
文件转化拓展
这部分为后面使用docx的拓展创造条件
class one2other(file_tools):
"""
one2other提供了文档类型之间相互转化的静态方法
如doc2pdf,doc2docx,docx2pdf.
传入参数均为两个(inputfile,outputfile)
需要精确到 '.doc' / '.docx' / '.pdf' 级别
需要使用的库是 os , win32com, file_tools
实现的功能:
1. doc2pdf 从doc转到pdf, 支持word2007版本, word2013版本打开的所有文件
2. doc2docx 从doc转到docx, 方便后续使用docx_tools
3. docx2pdf 从docx转到pdf, 后续导出文件
"""
@staticmethod
def doc2pdf(inputfile, outputfile):
"""
本函数实现了doc到pdf的转换
传入参数为精确到文件+后缀的 输入文件名, 输出文件名
返回值为True,表示转化成功, False,表示转化失败
"""
import os
from win32com.client import Dispatch, constants, gencache
#开启库的向下兼容
gencache.EnsureModule('{00020905-0000-0000-C000-000000000046}', 0, 8, 4)
w = Dispatch("Word.Application")
#尝试以下转化
try:
doc = w.Documents.Open(inputfile, ReadOnly = 1)
doc.ExportAsFixedFormat(outputfile, constants.wdExportFormatPDF,\
Item = constants.wdExportDocumentWithMarkup, CreateBookmarks = constants.wdExportCreateHeadingBookmarks)
#print(r'转换pdf成功!')
return True
#出问题就是转化失败
except:
#print(r'转换失败?')
return False
finally:
w.Quit(constants.wdDoNotSaveChanges)
@staticmethod
def doc2docx(inputfile,outputfile):
"""
本函数实现了doc到docx的转换
传入参数为精确到文件+后缀的 输入文件名, 输出文件名
返回值为True,表示转化成功, False,表示转化失败
"""
import os
from win32com.client import Dispatch, constants, gencache
w = client.Dispatch('Word.Application')
#尝试一下进行转换
try:
doc = w.Documents.Open(inputfile) # 目标路径下的文件
doc.SaveAs(outputfile, 16) # 转化后路径下的文件
doc.Close()
#print(r'转换docx成功!')
return True
#出问题就返回失败啊
except:
#print(r'转换失败?')
return False
finally:
w.Quit(constants.wdDoNotSaveChanges)
@staticmethod
def docx2pdf(input, output):
"""
本函数实现了doc到docx的转换
传入参数为精确到文件+后缀的 输入文件名, 输出文件名
返回值为True,表示转化成功, False,表示转化失败
"""
import os
from win32com.client import Dispatch, constants, gencache
w = Dispatch('Word.Application')
#尝试一下转换文件
try:
# 打开文件
doc = w.Documents.Open(input, ReadOnly=1)
# 转换文件
doc.ExportAsFixedFormat(output, constants.wdExportFormatPDF,\
Item=constants.wdExportDocumentWithMarkup, CreateBookmarks = constants.wdExportCreateHeadingBookmarks)
#print(r'转换pdf成功!')
return True
#有问题就转换不成啊
except:
#print(r'转换失败?')
return False
finally:
w.Quit(constants.wdDoNotSaveChanges)
docx文件拓展处理
这部分主要针对实验报告等文件
class docx_tools(file_tools):
"""
本类提供对docx的拓展处理, 完成docx文件的需求处理
依赖拓展库:os, docx, file_tools
实现的功能有:
1. check_operational 文档是否为docx文件检测
2. get_picture_num 提取文档中的图片个数
3. get_keyword_line_place 提取文档中关键词的首次出现位置, 返回首次出现的行数列表
4. get_keyword_line_places 提取文档中关键词群的首次出现位置, 返回首次出现的行数列表
"""
@staticmethod
def check_operational(filepath):
"""
函数功能判断该文件是不是能够被docx库操作
返回一个文件 代表可以操作,None代表不能操作
"""
import os, docx
pathlist = filepath.split('.')
#print(pathlist)
if pathlist[-1] != 'docx':
return None
#第一部分首先检测后缀名是不是docx,如果不是则判定处理不了
try:
docu = docx.Document(filepath)
return docu
except:
print(filepath, r'文件打开错误')
return None
#尝试利用docx库打开这个文件, 如果打开成功, 则认为是可以的
@staticmethod
def get_picture_num(docu):
"""
函数功能为返回文档中的图片个数
传入参数为一个文档docx.Document 的类型
返回是文档中的图片个数
"""
import os, docx
pic_num = 0
#直接从基层开始的代码搞上去
for rel in docu.part._rels:
#获得资源
rel = docu.part._rels[rel]
#判断标签内容是不是存在
if "image" not in rel.target_ref:
continue
#统计到结果里面
pic_num += 1
#然后返回数目
return pic_num
@staticmethod
def get_keyword_line_place(docu, keyword_list, default = -1, to_str = lambda x : x):
"""
函数功能:查找keyword_list中每个单词的出现的句子位置
传入参数: 文档, 关键词列表, 默认返回值, 关键词对应转化函数
其中句子位置与paragraphs的数组标号对应, -1 为默认值
返回为一个列表, 与keyword_list相对应
"""
import os, docx
place_list = [-1]*len(keyword_list)
now_num = 0
for i in range(0,len(docu.paragraphs)):
#枚举每一个行, 然后检索是不是还有需要检索的单词
if now_num >= len(keyword_list):
break
#枚举需要检索的单词的序号, 然后进行检索
for index2 in range(0,len(keyword_list)):
#如果已经找到了, 那就不用找了
if place_list[index2] != -1:
continue
#然后构造模式串
head_one = to_str(keyword_list[index2])
if head_one in docu.paragraphs[i].text:
place_list[index2] = i
#找到的序号+1
now_num += 1
#构造返回值序列
return [x if x!=-1 else default for x in place_list]
@staticmethod
def get_keyword_line_places(docu, keywords_list, default = -1, to_str = lambda x : x):
"""
keywords_list 形如 [['A','a'],['B','b'],'Aa']
实现模式为枚举keywords_list[index1]中的每个元素, 找到一个最小的位置
函数功能:查找keyword_list中每个单词的出现的句子位置
传入参数: 文档, 关键词列表, 默认返回值, 关键词对应转化函数
其中句子位置与paragraphs的数组标号对应, -1 为默认值
返回为一个列表, 与keyword_list相对应
"""
import os, docx
place_list = [-1]*len(keywords_list)
now_num = 0
for i in range(0,len(docu.paragraphs)):
#枚举每一个行, 然后检索是不是还有需要检索的单词
if now_num >= len(keywords_list):
break
#枚举需要检索的单词的序号, 然后进行检索
for index1 in range(0,len(keywords_list)):
#如果已经找到了, 那就不用找了
if place_list[index1] != -1:
continue
keyword_list = keywords_list[index1]
#构造单元序列
for head0 in keyword_list:
head_one = to_str(head0)
#然后构造模式串
if head_one in docu.paragraphs[i].text:
place_list[index2] = i
#找到的序号+1
now_num += 1
break
#构造返回值序列
return [x if x!=-1 else default for x in place_list]
excel拓展处理
这部分针对成绩单,考勤记录等excel文件
class excel_tools(file_tools):
"""
本类实现了excel的处理拓展类, 主要侧重于
依赖库: pandas, numpy, file_tools
实现的功能:
1. get_one2other 抽象提取: 提取一个列到另一个列的映射, 传入为一个文件地址, key列名, value列名
2. get_id2index 提取学号→序号:提取出成绩单中的学号/序号等对应信息, 返回对应的map
3. get_id2name 提取学号→姓名:提取出成绩单中的学号/姓名等对应信息, 返回对应的map
4. print_to_excel 尝试输出excel文件, 如果输出失败, 则会在一定的等待时间后重试
"""
@staticmethod
def get_one2other(rankpath, one, other):
"""
rankpath是excel的地址,one是一个列的名,other是一个列的名,返回one→other的映射
对一个excel表格,获取one列的值,按照对应关系对应到other
如果正常提取,则返回一个map,如果出现异常,则返回None
"""
import pandas as pd
#读取rank中的名字的顺序,并映射到序号列表中,方便后续进行排序
rank = None
#进行一波尝试,尝试读取文件
try:
rank = pd.read_excel(rankpath)
except:
return None
if one not in rank.columns.values or other not in rank.columns.values:
return None
#列表推导式生成字典
map_one2other = {str(rank[one][i]):int(rank[other][i]) for i in range(0,len(rank[one]))}
return map_one2other
@staticmethod
def get_id2index(rankpath):
"""
根据更加抽象的函数扩展出来的特定的
提取:学号→序号 的特殊函数
"""
map_id2index = get_one2other(rankpath, r'学号',r'序号')
return map_id2index
@staticmethod
def get_id2name(rankpath):
"""
根据更加抽象的函数扩展出来的特定的
提取:学号→姓名 的特殊函数
"""
map_id2name = get_one2other(rankpath, r'学号',r'姓名')
return map_id2name
@staticmethod
def print_to_excel(df, filepath, errorhint=r'导出失败, 正在重试'):
"""
本函数实现了导出到excel文件中的功能, 由于可能会出现忘记关闭原文件等情况, 因此加入自动重试机制
传入参数为 DataFrame, 文件路径, 错误提示语句
"""
import pandas
import time
if get_extend_name(filepath) != '.xlsx':
return False
false_count = 0
while True:
#最多尝试10次, 如果过多直接返回错误
if false_count > 10:
return False
try:
#尝试导出到指定文件
df.to_excel(filepath)
return True
break
except:
#显示重新尝试
print(errorhint, r'\n1S后进行重试')
time.sleep(1)
false_count += 1
return False
其他常用
这部分针对一些应用中的功能抽象
class user_tools:
"""
本类实现了常见功能的模块化, 方便写程序的时候进行调用. 方便使用者的写程序, 写界面的便捷性
依赖库: tkinter, time, socket, os, sys, struct, threading
实现的功能:
1. get_paths 可视化窗口询问文件夹路径
2. get_path 可视化窗口询问文件路径
3. deferdo 延迟等待, 会显示剩余时间
4. socket_client_file tcp协议的发送文件客户端
5. socket_client_fold tcp协议的发送文件夹客户端
6. socket_service tcp协议的服务端
"""
@staticmethod
def get_paths(inputstr=r'请选择文件夹'):
"""
本函数实现了可视化窗口询问文件夹路径的功能, 而且不会产生除了询问框外的其他视图
传入参数inputstr为问询框的标题
"""
from tkinter import filedialog
import tkinter as tk
#设置一个tkinter的窗口,然后设置为隐藏
root = tk.Tk()
root.withdraw()
#隐藏根tk, 免除白框框
#弹出提示后询问文件夹路径
folderpath = filedialog.askdirectory(title=inputstr, initialdir=r'./')
return folderpath
@staticmethod
def get_path(inputstr=r'请选择文件'):
"""
本函数实现了可视化窗口询问文件路径的功能, 而且不会产生除了询问框外的其他视图
传入参数inputstr为问询框的标题
"""
from tkinter import filedialog
import tkinter as tk
#设置一个tkinter的窗口,然后设置为隐藏
root = tk.Tk()
root.withdraw()
#隐藏根tk, 免除白框框
#弹出提示之后询问路径
filepath = filedialog.askopenfilename(title=inputstr, initialdir=r'./')
return filepath
@staticmethod
def deferdo(defer_second = 5):
"""
本函数实现了延迟等待的功能, 将多秒的延迟等待可视化了等待时间
defer_second为整数, 表示需要延时的时长(秒为单位), 默认为5s
"""
import time
for i in range(0,int(defer_second)):
print(defer_second-i,' 秒后返回正常运行!')
time.sleep(1)
@staticmethod
def socket_client_file(host='127.0.0.1',port=888):
"""
socket TCP传递文件的客户端,传入参数为地址和端口
默认为: host = '127.0.0.1', port = 888
"""
import socket
import os
import sys
import struct
#开启tcp连接,并尝试连接
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
except socket.error as msg:
print (msg)
sys.exit(1)
print (s.recv(1024))
# 需要传输的文件路径
filepath = get_path('请选择文件:')
# 判断是否为文件
if os.path.isfile(filepath):
# 定义定义文件信息。128s表示文件名为128bytes长,l表示一个int或log文件类型,在此为文件大小
fileinfo_size = struct.calcsize('128sl')
# 定义文件头信息,包含文件名和文件大小
fhead = struct.pack('128sl', os.path.basename(filepath).encode('utf-8'), os.stat(filepath).st_size)
# 发送文件名称与文件大小
s.send(fhead)
# 将传输文件以二进制的形式分多次上传至服务器
fp = open(filepath, 'rb')
while 1:
data = fp.read(1024)
if not data:
print ('{0} file send over...'.format(os.path.basename(filepath)))
break
s.send(data)
# 关闭当期的套接字对象
s.close()
@staticmethod
def socket_client_fold(host='127.0.0.1',port=888):
"""
socket TCP传递文件夹的客户端,传入参数为地址和端口
默认为: host = '127.0.0.1', port = 888
"""
import socket
import os
import sys
import struct
#开启tcp连接,并尝试连接
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
except socket.error as msg:
print (msg)
sys.exit(1)
print (s.recv(1024))
# 需要传输的文件路径
foldpath = get_paths('请选择文件夹:')
for file in os.listdir(foldpath):
# 判断是否为文件
if os.path.isfile(file):
# 定义定义文件信息。128s表示文件名为128bytes长,l表示一个int或log文件类型,在此为文件大小
fileinfo_size = struct.calcsize('128sl')
# 定义文件头信息,包含文件名和文件大小
fhead = struct.pack('128sl', (os.path.basename(foldpath)+'/'+file).encode('utf-8'), os.stat(flodpath + '/' +file).st_size)
# 发送文件名称与文件大小
s.send(fhead)
# 将传输文件以二进制的形式分多次上传至服务器
fp = open(filepath, 'rb')
while 1:
data = fp.read(1024)
if not data:
print ('{0} file send over...'.format(os.path.basename(file)))
break
s.send(data)
# 关闭当期的套接字对象
s.close()
@staticmethod
def socket_service(host='127.0.0.1',port=888):
"""
socket TCP接收文件/文件夹的客户端,传入参数为地址和端口
默认为: host = '127.0.0.1', port = 888
"""
import threading
import socket
import struct
#建立socket套接字,等待连接
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port))
# 设置监听数
s.listen(10)
except socket.error as msg:
print (msg)
sys.exit(1)
print ('Waiting connection...')
#单独的接收数据的一个线程
def deal_data(conn, addr):
print ('Accept new connection from {0}'.format(addr))
# conn.settimeout(500)
# 收到请求后的回复
conn.send('Hi, Welcome to the server!'.encode('utf-8'))
while True:
# 申请相同大小的空间存放发送过来的文件名与文件大小信息
fileinfo_size = struct.calcsize('128sl')
# 接收文件名与文件大小信息
buf = conn.recv(fileinfo_size)
# 判断是否接收到文件头信息
if buf:
# 获取文件名和文件大小
filename, filesize = struct.unpack('128sl', buf)
fn = filename.strip('\00')
fn = fn.decode('utf-8')
print ('file new name is {0}, filesize if {1}'.format(str(fn),filesize))
recvd_size = 0 # 定义已接收文件的大小
# 存储在该脚本所在目录下面
fp = open('./' + str(fn), 'wb')
print ('start receiving...')
# 将分批次传输的二进制流依次写入到文件
while not recvd_size == filesize:
if filesize - recvd_size > 1024:
data = conn.recv(1024)
recvd_size += len(data)
else:
data = conn.recv(filesize - recvd_size)
recvd_size = filesize
fp.write(data)
fp.close()
print ('end receive...')
# 传输结束断开连接
conn.close()
break
#无限循环然后等待连接
while True:
# 等待请求并接受(程序会停留在这一旦收到连接请求即开启接受数据的线程)
conn, addr = s.accept()
# 接收数据
t = threading.Thread(target=deal_data, args=(conn, addr))
t.start()
return