1. 程式人生 > >PyQt5實現郵件合並功能(GUI)

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

程序學習 connect 科學計數法 設置 xlrd temp 學習筆記 getitem 系統

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的對應列的文字編輯,結合QTreeWidgetCombo Box 可以實現想要的一一對應功能。學習一番QTreeWidget和Combo Box基本操作(在邏輯小節)。

技術分享圖片

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‘)

技術分享圖片

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)