PyQt5實現郵件合並功能(GUI)
1. 實戰Word批量
需要處理批量替換word的一些數據,數據源從Excel中來。
Excel的百分數會變為數字,以及浮點數會多好多精度,為了原汁原味的數據,直接復制數據到文本文件。通過\t來分隔即可,最後一個值多\n得註意。
然後在Word中加變量用{XXXX}
格式的得轉一下{}
,時間關系,用了 TEMP_XXX
之類的,str.replace()
去替換模板數據即可。女朋友發現Word有郵件合並功能,類似模板替換。
2. 進階-GUI工具
2.1 預備,查漏補缺
1)界面
看《PyQt快速開發與實戰》學習Qt designer生成ui、通過eric6或者命令編譯py文件、信號槽機制、簡單的如何讓界面和邏輯分離,以及以前的PyQt入門,打算直接上手做。
界面邏輯分離可以通過兩種方式:(註 Ui_m
為界面生成的py代碼文件)
# coding:utf-8 from PyQt5.QtWidgets import QMainWindow, QApplication from Ui_m import Ui_MainWindow import sys class MainWindow(QMainWindow): def __init__(self): super().__init__() def open_file(self): print(‘open file...‘) if __name__ == "__main__": app = QApplication(sys.argv) main_window_obj = MainWindow() ui = Ui_MainWindow() ui.setupUi(main_window_obj) main_window_obj.show() sys.exit(app.exec_())
# coding:utf-8 from PyQt5.QtWidgets import QMainWindow, QApplication from Ui_m import Ui_MainWindow import sys class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): super().__init__() self.setupUi(self) def open_file(self): print(‘open file...‘) if __name__ == "__main__": app = QApplication(sys.argv) main_window_obj = MainWindow() main_window_obj.show() sys.exit(app.exec_())
2) Excel數據處理
用常規的sheet.cell_value(i, j)
獲取數據,會有一些意外的情況,比如有些數字之後會多.0,百分比會是小數,小數多精度,太大的數字為科學計數法,日期也為浮點數,總之就是所得非所見。要所見即所得的話,直接復制,存取文本文件吧,每一列默認通過\t區分。
3) 數據讀取
基本的Excel和Word數據讀取:
import xlrd
import docx
def read_xls():
""" 讀取excel """
workbook = xlrd.open_workbook(r‘02.xls‘)
sheet = workbook.sheet_by_index(0)
cols = sheet.ncols
rows = sheet.nrows
data = []
for i in range(rows):
if i==0:
continue
row_content = []
for j in range(1, cols):
# print(sheet.cell_value(i, j), sheet.cell(i, j).ctype)
row_content.append(str(sheet.cell_value(i, j)))
data.append(row_content)
return data
def read_docs():
""" 讀取word數據 """
doc=docx.Document(r‘./01.docx‘)
text = []
for i in doc.paragraphs:
text.append(i.text)
data = ‘\n‘.join(text)
return data
if __name__ == ‘__main__‘:
e_data = read_xls()
w_data = read_docs()
2.2 主要流程
- 打開word模板(需要手動輸入所有的模板變量)
- 檢測所有的模板變量
- 添加一行數據
- 定義路由規則
- 測試數據
- 執行多行
2.3 畫界面
本來打算模板變量select選擇後,radio選擇相應的數據源,發現一篇Qt程序學習(三)------QTreeWidget、右鍵菜單、動態改變comboBox、QTreeWidgetItem的對應列的文字編輯,結合QTreeWidget
、Combo Box
可以實現想要的一一對應功能。學習一番QTreeWidget和Combo Box基本操作(在邏輯小節)。
2.4 寫邏輯
叮!項目壓測時候,發現excel的csv文件,文本文件打開是用,
逗號分隔的。可以直接處理excel了(雖然有局限,如果數據中有逗號就得更改系統配置,顯然不現實)。
所以流程變為:
- 讀取docx文件和csv兩個文件之後(word文件上面有,csv可以直接
csv.reader()
讀取) - 添加規則(一個模板變量對應一個下拉框)
- 生成數據
就可以了,第一條上面查漏補缺有,第三條剛開始的腳本邏輯處理都寫過了,所以重點放在添加規則,首先需要熟悉一下QTreeWidget
和 Combo Box
。
1) QTreeWidget 操作
實例化
self.treeWidget = QtWidgets.QTreeWidget(self.gridLayoutWidget)
self.treeWidget.setObjectName("treeWidget")
self.gridLayout.addWidget(self.treeWidget, 3, 4, 1, 1)
添加頭部(模板變量列和數據源列)
self.treeWidget.headerItem().setText(0, _translate("MainWindow", "模板變量"))
self.treeWidget.headerItem().setText(1, _translate("MainWindow", "數據源"))
增加一項數據:(上面的實例化和頭部可以用UI生成,item數據需要動態在代碼添加)
child = QTreeWidgetItem(self.treeWidget)
child.setText(0, ‘TEMP_COMPUTER‘)
child.setText(1, ‘PDP-01‘)
ok,還帶有滾動條,這樣到實際用的時候,左側模板變量數據可以通過Word文件獲取,數據源通過Excel頭部數據(實際上為文本)指定,通過類似select的Combo box
下拉框控件。
2)Combo box 操作
常規操作
# 實例化QComBox對象
self.cb=QComboBox()
# 單個添加條目
self.cb.addItem(‘C‘)
self.cb.addItem(‘C++‘)
self.cb.addItem(‘Python‘)
# 多個添加條目
self.cb.addItems([‘Java‘,‘C#‘,‘PHP‘])
將上面的添加QTreeWidget添加項目結合起來:左側為模板變量,右側為Combo box
數據
def add_qtree_item(self, item_data):
""" 新增item """
child = QTreeWidgetItem(self.treeWidget)
# 模板變量
child.setText(0 , ‘TEMP_COMPUTER‘)
# 數據源Combox
cb = QComboBox()
cb.addItem(‘PDP-8‘)
cb.addItem(‘PDP-11‘)
self.treeWidget.setItemWidget(child, 1 , cb)
效果如下
3)結合正式數據來綁定規則
圖中讀取後綴為.docx的Word文件來獲取模板變量,讀取後綴為.csv的Excel文件來獲取頭部當做數據源。
因時間關系,不打算將已選擇的項加標記,只獲取已選擇的,再次選擇時,提醒即可。但是現在這種綁定
def define_combo(self):
""" 定義combo_box """
cb = QComboBox()
cb.addItems(self.combo_box_item)
# 存儲所有的 combo box 實例
self.all_cb.append(cb)
# 添加一個改變的信號
cb.currentIndexChanged.connect(MainWindow.slot_change_item)
return cb
@staticmethod
def slot_change_item(index):
print(index)
選定之後,也只會收到一個被選定的項目索引的信號(整數,比如:3)。看不出來是哪個Combo box
實例發的信號。發現可以用lambda添加參數,直接將cb
實例傳過去:
# 添加一個改變的信號
cb.currentIndexChanged.connect(lambda : self.slot_change_item(cb))
return cb
def slot_change_item(self, cb_obj):
print(cb_obj)
print(cb_obj.currentIndex())
for i in self.all_cb:
print(i)
輸出:(第一行是激活的cb
實例,第二行是點擊的item索引,三四行為之前存儲的所有cb
實例,發現第三行和第一行是一個實例)
<PyQt5.QtWidgets.QComboBox object at 0x04B23DA0>
3
<PyQt5.QtWidgets.QComboBox object at 0x04B23DA0>
<PyQt5.QtWidgets.QComboBox object at 0x04B23E90>
至於模板變量列和數據源列的對應關系就不勞煩QTreeWidget了,自己直接處理了。
經過一番調整,終於初具雛形了
已存在模板的時候提醒了兩次,因為是這麽寫的
# 判斷是否已有模板對應該索引,已有重置為0
if index in self.rule.values():
self.error("已存在模板對應值,已重置,請重新選擇")
cb_obj.setCurrentIndex(0)
return False
第一個為1,當前為(1,0), 修改第二個為1時,檢測到 1 in (1,0), 然後置0,因0 in (1,0)再次觸發,所以兩次提醒:打日誌self.log(f"{index} in {self.rule.values()}")
輸出:
已存在模板對應值,已重置,請重新選擇
1 in dict_values([1, 0])
已存在模板對應值,已重置,請重新選擇
0 in dict_values([1, 0])
解決辦法是:
# 添加一個改變的信號 currentIndexChanged 和 activated
# cb.currentIndexChanged.connect(lambda : self.slot_change_item(cb))
cb.activated.connect(lambda : self.slot_change_item(cb))
在發送信號的時候采用activated
而不是 currentIndexChanged
就好了,因為currentIndexChanged
是每當組合框中的當前索引通過用戶交互或以編程方式更改時, 都會發送此信號。
user interaction or programmatically
activated
僅僅是當用戶在組合框中選擇項目時, 將發送此信號。
接下來就需要接入之前的處理邏輯
4)接入處理Word文件邏輯
移植過來就可以了
# coding:utf-8
""" word替換處理 """
from docx import Document
import os
class DocxHandle(object):
def __init__(self, doc_file, data, rule, out=print):
""" DocxHandle.
Args:
doc_file: template doc file.
data: 數據
rule: 規則
out: 輸出重定向
"""
self.document = Document(doc_file) # 打開文件docx文件
self.rule = rule
self.data = data
self.log = out
def save(self, title):
# 新建目錄
img_dir = ‘./docxdata/‘
if not os.path.exists(img_dir):
os.mkdir(img_dir)
self.document.save(f"{img_dir}{title}.docx") # 保存文檔
self.log(f"存儲文件:{img_dir}{title}.docx")
def test_rule_template(self):
for x,y in self.rule.items():
value = self.data[y]
self.log(f"{x},[{value}]")
def docx(self):
data = self.data
for x,y in self.rule.items():
value = data[y]
self.log(f"{x},[{value}]")
self.replace_text(x, value, self.document)
def replace_text(self, old_text, new_text, file):
"""# 傳入三個參數, 舊字符串, 新字符串, 文件對象"""
# 遍歷文件對象
for f in file.paragraphs:
# 如果 舊字符串 在 某個段落 中
if old_text in f.text:
# self.log(f"替換前===>{f.text}")
# 遍歷 段落 生成 i
for i in f.runs:
# 如果 舊字符串 在 i 中
if old_text in i.text:
# 替換 i.text 內文本資源
i.text = i.text.replace(old_text, new_text)
# self.log(f"替換後===>{f.text}")
2.5 最終結果
註意,遇到一個模板變量被拆分,看不出來。但是在Word分段解析的時候,會拆分開數據,導致不能替換。
所以如果有未檢測出來的模板變量,則報錯。
最終效果:
在打包之前多加了一個生成文檔模式功能:參考郵件合並。(合並文檔當模板有非默認字體時,得註意樣式問題)
最後,需要打包exe文件:pyinstaller
不支持3.7,需要下載 pyinstaller。有奇怪報錯,最後采用cx_Freeze
來打包:
只需要編寫文件install.py
from cx_Freeze import setup, Executable
setup( name = "Combine_docx",
version = "1.0",
description = "類似Word郵件合並功能",
executables = [Executable("./mainwindow.py")])
運行python install.py build
則打包成功。
3. 其他錯誤
1)打包之後導入csv文件會報錯
Traceback (most recent call last):
File "./mainwindow.py", line 78, in slot_open_excel_file
File "D:\Software\python3\lib\codecs.py", line 322, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: ‘utf-8‘ codec can‘t decode byte 0xd0 in position 0: invalid continuation byte
先獲取到文件編碼,再次使用編碼打開csv文件
# 獲取文件編碼
en_code = ‘utf-8‘
with open(self.excel_file, ‘rb‘) as f:
en_code = chardet.detect(f.read())[‘encoding‘]
with open(self.excel_file, encoding=en_code) as f:
# 解析csv文件
data = list(csv.reader(f))
2)設置程序圖標
設置程序圖標有多種方式,感覺用不到Designer建qrc資源文件,直接用
self.setWindowIcon(QIcon(‘combine.ico‘)) # 設置圖標
用python直接運行程序是可以有的,生成exe運行就無圖標了。
cx_freeze 加參數icon
只會在縮略圖中有,程序左上角還是沒有,夠用了。
終於完整的完成了一個小GUI工具 :) 雖然實際用起來一般般,特別是數據多的時候,模板變量得從前往後打,以避免在docx角度看到的模板變量是拆分的,但這只是一個開始。
參考
安裝python-docx
xlrd
excel讀取小數
<自動化辦公> Python 操控 Word
pip使用本地緩存文件來安裝包
Word郵件合並 Word郵件合並2
QTreeWidget、右鍵菜單、動態改變comboBox、QTreeWidgetItem的對應列的文字編輯
PyQt--QTreeWidget
PyQt5基本控件詳解之QComboBox(九)
PyQt: How to connect QComboBox to function with Arguments
PyQt5學習筆記16----PyQt信號和槽傳遞額外參數
[python,字典中如何根據value值取對應的key值](https://segmentfault.com/q/1010000008277059)
【記錄】用PyInstaller把Python代碼打包成單個獨立的exe可執行文件
- cx_Freeze打包
- 【記錄】用cx_Freeze把Python代碼打包成單個獨立的exe可執行文件
- PyQt5環境搭建及cx_freeze打包exe
- 利用cx_freeze打包python程序
解碼csv UnicodeDecodeError: (‘utf-8‘ codec) while reading a csv file duplicate
PyQt5實現郵件合並功能(GUI)