PyQt5實現郵件合併功能(GUI)

1. 實戰Word批量

須要處理批量替換word的一些數據,數據源從Excel中來。html

Excel的百分數會變爲數字,以及浮點數會多好多精度,爲了原汁原味的數據,直接複製數據到文本文件。經過\t來分隔便可,最後一個值多\n得注意。python

而後在Word中加變量用{XXXX}格式的得轉一下{},時間關係,用了 TEMP_XXX之類的,str.replace()去替換模板數據便可。女友發現Word有郵件合併功能,相似模板替換。git

2. 進階-GUI工具

2.1 預備,查漏補缺

1)界面

看《PyQt快速開發與實戰》學習Qt designer生成ui、經過eric6或者命令編譯py文件、信號槽機制、簡單的如何讓界面和邏輯分離,以及之前的PyQt入門,打算直接上手作。github

界面邏輯分離能夠經過兩種方式:(注 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區分。segmentfault

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 畫界面

word_ui.png

原本打算模板變量select選擇後,radio選擇相應的數據源,發現一篇Qt程序學習(三)------QTreeWidget、右鍵菜單、動態改變comboBox、QTreeWidgetItem的對應列的文字編輯,結合QTreeWidgetCombo Box 能夠實現想要的一一對應功能。學習一番QTreeWidget和Combo Box基本操做(在邏輯小節)。app

word_ui_2.png

2.4 寫邏輯

叮!項目壓測時候,發現excel的csv文件,文本文件打開是用,逗號分隔的。能夠直接處理excel了(雖然有侷限,若是數據中有逗號就得更改系統配置,顯然不現實)。工具

因此流程變爲:學習

  • 讀取docx文件和csv兩個文件以後(word文件上面有,csv能夠直接csv.reader()讀取)
  • 添加規則(一個模板變量對應一個下拉框)
  • 生成數據

就能夠了,第一條上面查漏補缺有,第三條剛開始的腳本邏輯處理都寫過了,因此重點放在添加規則,首先須要熟悉一下QTreeWidgetCombo 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')

QTree_temp_data.png

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)

效果以下

QTree_and_ComboBox.png

3)結合正式數據來綁定規則

rule1.png

圖中讀取後綴爲.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了,本身直接處理了。

通過一番調整,終於初具雛形了

temp_doc.png

已存在模板的時候提醒了兩次,由於是這麼寫的

# 判斷是否已有模板對應該索引,已有重置爲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分段解析的時候,會拆分開數據,致使不能替換。

bug_word_text.png

因此若是有未檢測出來的模板變量,則報錯。

最終效果:combine_doc.gif

在打包以前多加了一個生成文檔模式功能:參考郵件合併。(合併文檔當模板有非默認字體時,得注意樣式問題)

combine_doc_info.png

最後,須要打包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角度看到的模板變量是拆分的,但這只是一個開始。

參考

相關文章
相關標籤/搜索