背景:
最近利用休息时间写了个小型exe程序,主要涉及的技术点有:多线程,读取xls文件,基于PyQt5的简单GUI页面,利用PyInstaller打包成exe。虽然有ChatGPT等协助,但难免还是在开发过程中遇到了一些疑难问题,所以开个记录贴刊登解决方式。
问题&解决方式:
1.PyQt+PyInstaller:tqdm报错AttributeError: ‘NoneType‘ object has no attribute ‘write‘:
这个问题我当时让GPT改了半天都没改到要点上,最后是搜索别人的贴子解决的:传送门https://blog.csdn.net/weixin_39916966/article/details/141954305?spm=1001.2014.3001.5501
只需加入下面的代码,把sys.stdout和sys.stderr重定向初始化就行了。
if sys.stdout is None:sys.stdout = io.StringIO()if sys.stderr is None:sys.stderr = io.StringIO()
2.PyInstaller找不到需要打包的文件路径:
在用PyInstaller打包的时候,经常会报错缺了xx依赖,导致在没有开发环境的电脑上无法运行exe程序。如果把错误发给GPT,他虽然能说出来需要什么库的什么文件,但是从电脑上找这个文件还是很费力。
于是就需要使用这个推荐工具了:Everything。传送门
https://gitcode.com/open-source-toolkit/bfdea?utm_source=highlight_word_gitcode&word=Everything
安装方便,搜索简单,功能强大,比电脑自带的搜索功能快了很多倍。
除此之外,还有一个方法,允许我们通过代码来查找目标文件路径。
例如,下面是GPT给出的参考代码,我们不知道该怎么结合自己电脑的安装情况来写打包依赖路径。
--add-data "C:/Path/To/PyQt5/Qt/plugins/platforms;platforms" 不
一般来说,安装了 PyQt5 后,该路径通常位于你 Python 环境的 site‑packages 下,例如:
- 如果你使用 pip 安装,可能在 “C:\Python38\Lib\site-packages\PyQt5\Qt\plugins\platforms”
- 如果你使用 conda 环境,可能在 “C:\Users\admin.conda\envs\Stock\Lib\site-packages\PyQt5\Qt\plugins\platforms”
如果上述这两个目录都没有的话,就可以运行下面的代码来查找实际路径:
import os
import PyQt5
print(os.path.join(os.path.dirname(PyQt5.__file__), "Qt", "plugins", "platforms"))
运行结果:
这个输出的路径就是你需要传递给 --add-data 参数的路径。然后你就可以把这个路径替换命令中的 “C:/Path/To/PyQt5/Qt/plugins/platforms”。
(注意,建议把路径换成正斜杠“ / ”。在 Windows 下,文件路径既可以使用正斜杠(/)也可以使用反斜杠(\)。通常建议使用正斜杠,因为反斜杠容易引起转义问题。)
3.有多种分隔符的xls表格读取:
有些表格同时存在空格、制表符等多种分隔符,可以用Sublime Text、Notepad++等软件打开,需要做一些特殊设置。
以我比较熟悉的Sublime Text为例,设置的方法如下:
(说句题外话,我对Sublime Text最早的记忆是,把原神模型导入Unity玩的一个步骤,就需要用Sublime Text打开模型的一个代码文件,改一下其中一个数字,之后Unity就能正常识别模型,玩MMD了。本来想找我以前学习看的B站教程视频放在这,可惜找不到了QAQ)
有一些xls数据是混合分隔符,比如既有制表符又有空格的,就可以用正则表达式来处理,这样可以同时匹配多种分隔符。
例如,GPT生成的如下代码,可以同时匹配空格和制表符进行分割。
import re
split_data = re.split(r'\s+|\t+', data)
4.检测文件编码:
我读一个xls的时候,xlrd库,csv库,pandas库,openpyxl库都试过了,确实用尽了所有的力气和手段都没能成功,于是就决定先看看这个文件的编码是什么。
chardet库检测文件编码的代码如下:
import chardetwith open('你的文件.xls', 'rb') as f:rawdata = f.read(10000) # 读取部分内容进行检测
result = chardet.detect(rawdata)
print(result)输出结果:{'encoding': 'GB2312', 'confidence': 0.99, 'language': 'Chinese'}
这段代码会输出一个字典,例如 {‘encoding’: ‘GB2312’, ‘confidence’: 0.99, …},预期就能成功得到编码答案,我发现这是一个GB2312的xls文件,那么接下来就要引出另一个问题了。
5.GB2312文件Python解析失败:“UnicodeDecodeError :‘gb2312’ codec can’t decode bytes in position 2-3:illegal multibyte sequence”:
这也是个GPT优化了半天都没突破的问题,我一开始以为是xls文件的问题,但是换了txt文件一样无法正常读取。
同样请出大神的帖子解决问题,换用兼容性更高的GB18030编码来读取:传送门
https://www.cnblogs.com/chucklu/p/16906161.html
以下面这段代码为例,就是可以读取GB2312的txt和xls的:
import akshare as ak
import datetime
import pandas as pd
import chardet#uploaded_file_path = "你的txt文件路径.txt"
uploaded_file_path = "你的xls文件路径.xls"try:print("开始读取文件...")with open(uploaded_file_path, 'rb') as f:rawdata = f.read()detected_encoding = chardet.detect(rawdata)['encoding']print("检测到文件编码: " + str(detected_encoding))df = pd.read_csv(uploaded_file_path, sep="\t", header=0, encoding='GB18030', engine='python')print(df)
except Exception as e:print("读取过程出现错误: " + str(e))
6.多线程异步运行的假卡死:
我的标题这么描述可能不准确,详细情况是我的程序有多个线程,一个主线程负责图形界面的消息更新和进度展示,还有一个线程负责读取、筛选、刷新数据等。
我已经把文件无法正常读取的问题解决了,但是一点exe运行程序,正常读完文件就卡死,图形界面也没有任何输出和报异常,加了好几个测试输出的语句也都没有效果。
为此我先去仔细学习了VSCode如何调试程序,打断点查看程序运行的细节。
调试发现读取文件的问题解决了,程序里面的目标dataframe(简称df)也一直有值不是空列表,程序看着是卡死、图形界面没有输出的原因是:
- 事件循环被阻塞:在主线程中执行耗时操作(例如循环、网络请求和 time.sleep())阻塞了 Qt 的事件循环,从而使得界面无法及时更新。
- 输出刷新延迟:即使调用了 log_message 将输出添加到 QPlainTextEdit 中,只有当控制权返回给事件循环时,界面才会刷新显示这些内容。
因此,即便有许多输出,由于主线程被后续耗时任务占用,GUI更新就一直没有发生。
解决的办法是把耗时的任务放到子线程中运行,或者在长任务中周期性调用QApplication.processEvents() 来让界面有机会刷新(不过这种方式只是权宜之计)。
请GPT对代码作出修改,GPT采取了将耗时的任务移至子线程,并通过信号反馈日志和进度,从而避免阻塞主线程,确保图形界面能够及时刷新输出,重新运行程序,果然程序的图形界面能正常输出了。
7.本地文件的判断与读取:
这个严格来说不是我遇到的问题,而是我一个新学到的很好奇的点。托GPT的福,我的exe程序可以允许用户传入电脑本地目录的文件,我之前从来没有写过这样的功能,所以很好奇是怎么实现的。
主要使用 isLocalFile() 和 toLocalFile() 这两个函数,他们是 Qt 框架中 QUrl 类的两个方法,用于处理 URL 数据,特别是与本地文件相关的操作。
isLocalFile():
isLocalFile() 是 QUrl 类的一个方法,用于判断一个 URL 是否指向本地文件系统中的一个文件。
- URL(Uniform Resource Locator)有很多分类, 可以指向多种资源,包括本地文件、网络资源(如 HTTP、FTP)、以及其他协议的资源。
- 例如:
- file:///path/to/local/file 是一个指向本地文件的 URL。
- http://example.com/file 是一个指向网络资源的 URL。
- ftp://example.com/file 是一个指向 FTP 服务器资源的 URL。
- 例如:
isLocalFile() 方法可以检查 URL 的协议(scheme)是否为 file。如果是,表示该 URL 指向本地文件系统中的一个文件。它还会检查路径是否有效,确保路径是本地文件系统的一部分。
from PyQt5.QtCore import QUrlurl1 = QUrl("file:///home/user/example.xls")
url2 = QUrl("http://example.com/example.xls")print(url1.isLocalFile()) # 输出: True
print(url2.isLocalFile()) # 输出: False
toLocalFile():
toLocalFile() 是 QUrl 类的另一个方法,用于将一个指向本地文件的 URL 转换为本地文件路径。
- URL 到路径的转换:
- 当一个 URL 的协议为 file 时,toLocalFile() 方法会将 URL 中的路径部分提取出来,并转换为本地文件系统中的路径。
- 例如,URL file:///home/user/example.xls 会被转换为路径 /home/user/example.xls。
- 仅适用于 file 协议的 URL,对于其他协议的 URL(如 http 或 ftp),调用 toLocalFile() 会返回空字符串。
- 路径格式化:
- toLocalFile() 方法会根据操作系统的文件路径规则(如 Windows 使用反斜杠 \,而 Linux/Unix 使用正斜杠 /)对路径进行格式化,确保返回的路径是本地文件系统可以识别的格式。
from PyQt5.QtCore import QUrlurl = QUrl("file:///home/user/example.xls")
local_path = url.toLocalFile()
print(local_path) # 输出: /home/user/example.xls
isLocalFile() 和 toLocalFile() 的结合使用:
在实际应用中,isLocalFile() 和 toLocalFile() 通常一起使用,以确保处理的 URL 是指向本地文件的,并且能够获取到正确的本地路径。
from PyQt5.QtCore import QUrldef process_url(url):if url.isLocalFile(): # 检查是否为本地文件local_path = url.toLocalFile() # 转换为本地路径print("本地文件路径:", local_path)# 进一步处理本地文件else:print("该 URL 不指向本地文件")# 测试
url1 = QUrl("file:///home/user/example.xls")
url2 = QUrl("http://example.com/example.xls")process_url(url1) # 输出: 本地文件路径: /home/user/example.xls
process_url(url2) # 输出: 该 URL 不指向本地文件
这两个方法在处理拖拽文件的场景中非常有用,因为拖拽操作可能会带来不同类型的 URL,而通过这两个方法可以确保只处理本地文件,并获取到正确的文件路径。
除了本地文件路径以外的其他url如何处理:
在 Qt 中,QUrl 提供了多种方法来检查和处理不同协议的 URL。对于 http 和 ftp 协议的 URL,虽然没有像 isLocalFile() 这样的直接方法,但可以通过以下方式实现类似的功能:
- 如果需要检查 URL 是否为 http 或 ftp 协议,可以直接使用 scheme() 方法。
通过比较协议字符串,可以判断 URL 是否为 http 或 ftp 协议。
QUrl url("http://example.com/file");
QString protocol = url.scheme();
if (protocol == "http") {qDebug() << "这是一个 HTTP 协议的 URL";
} else if (protocol == "ftp") {qDebug() << "这是一个 FTP 协议的 URL";
} else {qDebug() << "这是一个其他协议的 URL";
}
- 如果需要从用户输入中解析 URL,可以使用 fromUserInput() 方法。
如果输入的字符串类似于 http:// 或 ftp://,它会自动将其解析为对应的协议。
QUrl url = QUrl::fromUserInput("example.com");
if (url.scheme() == "http") {qDebug() << "自动解析为 HTTP 协议";
} else if (url.scheme() == "ftp") {qDebug() << "自动解析为 FTP 协议";
}
- 结合 isValid() 方法可以进一步验证 URL 的有效性。虽然它不能直接判断协议类型,但可以结合 scheme() 方法一起使用。
QUrl url("ftp://example.com/file");
if (url.isValid() && url.scheme() == "ftp") {qDebug() << "这是一个有效的 FTP 协议 URL";
} else {qDebug() << "URL 无效或不是 FTP 协议";
}
8.输出为Excel不容易出现乱码的UTF-8-SIG编码:
这个是我从其他同学那里学来的经验,我换了台mac电脑,发现输出的UTF-8编码的csv文件在mac上打开是乱码,然后坐我旁边的同学奇迹般地把这个问题妙手回春解决了,一问,噢,他用了UTF-8-SIG编码。
UTF-8-SIG 是 UTF-8 的一个变体,主要用于解决某些软件(如 Microsoft Excel)在处理 UTF-8 编码文件时可能出现的兼容性问题。
UTF-8-SIG 的好处:
- 更好的兼容性:
在某些软件(尤其是 Microsoft 的软件,如 Excel 和 Notepad)中,UTF-8 编码的文件可能会被错误地识别为 ANSI 编码,导致乱码。
UTF-8-SIG 文件由于包含 BOM,这些软件可以更准确地识别文件的编码格式,从而避免乱码问题。 - 避免乱码问题:
当文件在不同系统之间传输或被不同软件打开时,UTF-8-SIG 可以减少因编码识别错误导致的乱码问题。
在 Excel 中,使用 UTF-8-SIG 编码保存文件可以显著改善中文字符的显示效果,尤其是在处理 CSV 文件时。
如果需要确保文件在 Microsoft 软件中正确显示,建议使用 UTF-8-SIG 编码。
在 Python 中,可以使用 pandas 或 open() 函数指定编码为 utf-8-sig。
使用 pandas 保存为 UTF-8-SIG:
import pandas as pddf = pd.DataFrame({'列1': ['内容1', '内容2']})
df.to_csv('output.csv', index=False, encoding='utf-8-sig')
使用 open() 函数保存为 UTF-8-SIG:
with open('output.txt', 'w', encoding='utf-8-sig') as f:f.write('你好,世界!')
我自己的代码中,允许自定义文件名,并且保存为UTF-8-SIG的代码段:
fileName, _ = QFileDialog.getSaveFileName(self, "导出CSV", "", "CSV Files (*.csv);;All Files (*)")if fileName:try:export_df.to_csv(fileName, index=False, encoding='utf-8-sig')filename = os.path.basename(fileName)filepath = os.path.dirname(fileName)self.log_message(f"导出结果成功!文件名:{filename},保存路径:{filepath}")except Exception as e:self.log_message(f"导出结果失败:{e}")else:self.log_message("导出结果已取消")