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的对应列的文字编辑,结合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角度看到的模板变量是拆分的,但这只是一个开始。

参考

原文地址:https://www.cnblogs.com/warcraft/p/10345565.html

时间: 2024-10-08 19:41:33

PyQt5实现邮件合并功能(GUI)的相关文章

Word邮件合并功能:一页内连续填充

有一个表格,需要将表格中姓名列数据列印填充到word表格中,如下顺序 A B C D E F G H I J K L M N O P 邮件合并时格式如下: <<姓名>><<下一条记录>> <<姓名>><<下一条记录>> <<姓名>><<下一条记录>> <<姓名>> <<姓名>><<下一条记录>>

WORD 实现邮件合并

个人理解WORD的邮件合并功能: WORD的邮件合并功能实际上是将某个文档的表格中的所有数据源按变量类别有序插入主文档,得到一系列目的文档的过程. 步骤如下: 其中: 数据源是另一份文档中的表格 第三步打开数据源:邮件--选择收信人--使用现有列表/创建新列表--插入合并域(下拉显示变量名称)--预览结果--完成并合并(--编辑单个文档--全部) 原文地址:https://www.cnblogs.com/jane-lau/p/8495414.html

邮件合并法

产生背景: 在日常生活中,经常要处理一些相同或相似的工作,比如:单位向外大量发送公函,在学校要给大量的学生邮寄录取通知书:在公司要给大量的客户寄送邀请函:在结婚时要给大量的朋友寄请帖等等.这些生活或者工作中碰到的事情有个相似的特点:就是发送和制作的量可能很大,且大部分内容相同或相似,如果逐一手工完成,工作量太大,微软为了解决这些问题,就在word里提供了邮件合并功能. 虽然刚开始该功能在其他领域往往并没有和邮件邮编等产生直接联系,但是随着时间的发展和工作的多样性,邮件合并因为这些发送公函.邮件等

redmine邮件发送功能配置详解

redmine的邮件发送功能还是很有用的. 像项目有更新啦,任务分配啦,都能邮件发送的相关责任人. 我自己在linux服务器上安装并启动了redmine后,邮件一直发送了不了. 查了网上的资料,都是讲修改下配置文件就可以了,他们没错,只是没有讲全. 下面是我整理的一个redmine邮件发送功能设置的一个完整流程. 1. sendmail安装与检查 linux机器上安装的redmine要能发送邮件,先得是本机的sendmail功能是正常的. 查看sendmail进程是否已正常启动: $ ps au

用ASP.NET Core 1.0中实现邮件发送功能-阿里云邮件推送篇

在上篇中用MailKit实现了Asp.net core 邮件发送功能,但一直未解决阿里云邮件推送问题,提交工单一开始的回复不尽如人意,比如您的网络问题,您的用户名密码不正确等,但继续沟通下阿里云客户还是很耐心的. 最终结论,是由于MailKit发送了两次EHLO命令,查看了MailKit源码后竟然发现,里面写了硬编码: if (host != "smtp.strato.de" && host != "smtp.sina.com") Ehlo (can

实现邮件分享功能

private void sendMail(){ Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("message/rfc822"); intent.putExtra(Intent.EXTRA_SUBJECT,"邮件标题"); intent.putExtra(Intent.EXTRA_TEXT, "邮件正文内容" + "\n"+"分享自--

邮件发送功能开发

作为一名.Net开发,"邮件发送"功能的开发和使用是必须要掌握的,因为这个功能作为"消息推送"的一种手段经常出现在各种.Net系统中,所以本文将对.Net平台下的"邮件发送",做一个细致的分析! 一.who需要邮件功能 1.服务提供方:需提供邮件收发客户端或Web服务.如:QQ邮箱.GMail邮箱.126.163等知名邮件服务提供商.注:如果你使用的第三方不知名邮件服务商提供的邮件收发服务,通过其发出的邮件,可能会被其他知名邮件服务提供商的STM

邮箱客户端的邮件过滤功能

虽然现在各种联系工具很发达,各种社交工具,但是邮件这种方式还是非常常用,也是非常重要的.公司邮箱,各种网站注册账号使用的邮箱等,既然有了邮箱,那么就要收发邮件,经常收发邮件,还是有一个客户端最方便了. 但是随着收发邮件多了,带来一个问题,各种已读邮件充斥在收件箱和发件箱中,各种订阅邮件聚集在一个收件箱中,顿时使一个小小的收件箱显得拥挤起来,邮件的检索也变得异常困难,这时如果有一个邮件分类过滤的功能该多好啊,别急,下面就一起看看常见的邮箱客户端的分类功能. Foxmail邮件转移功能 最初使用Fo

ThinkPHP中邮件发送功能

初次使用thinkphp框架,开发一个邮件发送功能,由于对框架不熟悉折腾了几个小时终于成功了,以下是代码记录. 此函数只能在ThinkPHP中使用且需要phpmailer扩展的支持:phpmail的下载地址:https://code.google.com/a/apache-extras.org/p/phpmailer 将phpmailer解压后放置扩展放置到第三方类库扩展目录下: ThinkPHP/Extend/Vendor/文件夹下即可,并使用vendor方法来导入.更详细介绍参考:http: