【Python 实战】---- 实现一个可选择、配置操作的批量文件上传工具(三)上传类的实现
1. 前言
很早之前,我实现过一个【Python 实战】---- 接口自动化:60行代码,如何通过Python requests实现图片上传,这个的确简化了前端开发对图片操作的时间,但是随着项目开发的越多,遇到一个新的问题,就是图片需要上传到每个项目的自己服务器中,而且每个项目的参数也不一样,有的需要加密,有的不需要,这就会出现我们需要根据不同的项目,对代码进行上传的微调,然后再打包成工具使用,最后就会发现有很多工具,完全达不到一个程序员的优雅,因此就想到使用 GUI 配置管理,将所有的工具进行配置管理。上一篇文章只是对事件进行了实现,但是上传类没有实现,现在详细实现上传类。
2. 上传类的调用
ini
# 生成icon.js内容但不写入文件
uploader = FileUploader(self.current_config)
3. 递归获取文件夹下所有指定后缀的文件
3.1 实现分析
- 初始化一个空的结果列表
results
用于存储找到的文件路径。 - 使用
os.walk()
方法递归遍历给定目录dir_path
下的所有文件和子目录。 - 对于遍历到的每个文件,将其路径与目录路径拼接成完整的文件路径。
- 使用
os.path.splitext()
方法获取文件的后缀名,并去掉开头的点号(.
)。 - 检查文件后缀名是否在传入的后缀名列表
suffixes
中,如果匹配则将该文件的完整路径添加到结果列表中。 - 在遍历过程中如果发生异常,则打印异常信息。
- 最后返回包含所有匹配文件完整路径的列表。
3.2 实现代码
python
def get_files_with_suffix(self, dir_path, suffixes):
"""递归获取文件夹下所有指定后缀的文件"""
results = []
try:
for root, dirs, files in os.walk(dir_path):
for file in files:
file_path = os.path.join(root, file)
if os.path.splitext(file_path)[1][1:] in suffixes:
results.append(file_path)
except Exception as e:
print(e)
return results
4. 上传单个文件
4.1 实现分析
- 使用二进制模式打开指定路径的文件。
- 构造上传文件的参数,包括文件字段名、文件名和文件内容,以及文件类型。
- 从配置中获取额外的数据字段,如果未配置则使用空字典,并复制该字典以避免修改原始配置。
- 向数据字典中添加必要的字段,包括文件类型字段和名称字段。
- 使用
requests
库发送 POST 请求到配置中的 URL,携带配置的请求头、数据和文件参数。 - 检查响应状态码是否为 200(成功)。
- 如果状态码为 200,解析响应的 JSON 数据并检查其中的
code
字段是否与配置中的成功码匹配。 - 如果上传成功,返回响应数据;如果上传失败或响应码不匹配,打印错误信息并返回
None
。 - 在整个过程中如果发生异常,捕获异常并打印错误信息,然后返回
None
。
4.2 实现代码
python
def upload_file(self, file_path, file_type):
"""上传单个文件"""
try:
with open(file_path, 'rb') as f:
files = {self.config.file_field_name: (os.path.basename(file_path), f, f"image/{file_type}")}
# 使用配置中的data字段,如果未配置则使用默认值
data = getattr(self.config, 'data', {})
# 合并必要的字段
data = data.copy() # 创建副本以避免修改原始配置
data[self.config.file_type_field] = file_type
data[self.config.name_field] = self.config.name_value
res = requests.post(url=self.config.url, headers=self.config.headers, data=data, files=files)
if res.status_code == 200:
response_data = res.json()
if response_data.get("code") == self.config.success_code:
return response_data
else:
print(f"Failed to upload {file_path}: {response_data}")
return None
else:
print(f"Failed to upload {file_path}: HTTP {res.status_code}")
return None
except Exception as e:
print(f"Error uploading {file_path}: {e}")
return None
5. 批量上传图片
5.1 实现分析
- 首先清空上一次的上传日志文件 'upload.log'。
- 调用
get_files_with_suffix
方法获取目录下所有匹配后缀名的文件路径列表。 - 初始化一个空的结果列表
results
用于存储上传成功的文件信息。 - 遍历文件路径列表,对每个文件执行以下操作:
- 计算文件相对于上传目录的相对路径,用作显示名称。
- 生成上传日志信息,并通过回调函数显示在界面上,同时写入日志文件。
- 获取文件的后缀名(文件类型)。
- 调用
upload_file
方法上传文件。 - 如果上传成功,将文件相对路径和上传结果添加到结果列表中,生成成功日志信息并通过回调函数显示,同时写入日志文件。
- 如果上传失败,生成失败日志信息并通过回调函数显示,同时写入日志文件。
- 返回包含所有上传成功文件信息的结果列表。
5.2 实现代码
python
def upload_images(self, dir_path, suffixes, log_callback=None):
"""批量上传图片"""
# 清空上一次的日志
open('upload.log', 'w').close()
files = self.get_files_with_suffix(dir_path, suffixes)
results = []
for file_path in files:
relative_path = os.path.relpath(file_path, dir_path)
# 使用原文件名
display_name = relative_path
log_message = f"Uploading {display_name}...\n"
if log_callback:
log_callback(log_message.strip())
# 将日志写入文件
with open('upload.log', 'a', encoding='utf-8') as log_file:
log_file.write(log_message)
file_type = os.path.splitext(file_path)[1][1:]
result = self.upload_file(file_path, file_type)
if result:
results.append({
"fileName": relative_path,
"result": result
})
success_message = f"Uploaded {display_name} successfully\n"
if log_callback:
log_callback(success_message.strip())
# 将成功日志写入文件
with open('upload.log', 'a', encoding='utf-8') as log_file:
log_file.write(success_message)
else:
fail_message = f"Failed to upload {display_name}\n"
if log_callback:
log_callback(fail_message.strip())
# 将失败日志写入文件
with open('upload.log', 'a', encoding='utf-8') as log_file:
log_file.write(fail_message)
return results
6. 上传结果写入 icon.js 文件
6.1 实现分析
- 初始化数据结构 :创建一个空字典
icon_data
用于存储处理后的图片信息。 - 处理上传结果 :遍历传入的
results
列表(包含上传文件的结果信息):- 检查每个文件的上传结果,只有当返回的
code
与配置中的success_code
匹配时才继续处理。 - 提取文件名(不含路径)并去除扩展名,生成基础的 key 名称(格式为
文件名Icon
)。 - 如果配置中设置了前缀(
prefix
),则将前缀和 key 都转换为驼峰命名格式,并组合成最终的 key。 - 获取图片路径前缀(从界面输入框
img_prefix_entry
获取)。 - 取出上传结果中的实际图片路径值(由配置中的
content_field
指定字段)。 - 如果设置了图片路径前缀且原始路径值不为空,则将前缀与原始路径拼接;否则直接使用原始路径值。
- 将处理好的 key-value 对存入
icon_data
字典中。
- 检查每个文件的上传结果,只有当返回的
- 生成文件内容 :按照 JavaScript 对象的格式,将
icon_data
中的所有键值对构造成一个字符串,格式为export default { "key": "value", ... }
。 - 确定文件路径 :如果传入了有效的
dir_path
参数,则将icon.js
文件保存在该目录下;否则默认保存在当前目录(./icon.js
)。 - 写入文件 :尝试以 UTF-8 编码将构造好的内容写入到
icon.js
文件中。如果写入成功返回True
,若出现异常则打印错误信息并返回False
。
6.2 实现代码
python
def write_results_to_icon_js(self, results, dir_path=None):
"""将上传结果写入 icon.js 文件"""
icon_data = {}
# 构建 icon_data 对象
for item in results:
if item["result"].get("code") == self.config.success_code:
# 使用原文件名生成key
file_name = os.path.basename(item["fileName"])
file_name_without_ext = os.path.splitext(file_name)[0]
# 生成key
key = f"{file_name_without_ext}Icon"
# 添加前缀(如果配置了前缀)并转换为驼峰命名
prefix = getattr(self.config, 'prefix', '')
if prefix:
# 将前缀和key转换为驼峰命名
prefix = self.to_camel_case(prefix)
key = self.to_camel_case(key)
key = f"{prefix}{key}"
# 获取图片路径前缀
img_prefix = self.img_prefix_entry.get() if hasattr(self, 'img_prefix_entry') else ''
# 获取原始值
original_value = item["result"].get(self.config.content_field)
# 如果有图片路径前缀且原始值不为空,则拼接前缀
if img_prefix and original_value:
icon_data[key] = f"{img_prefix}{original_value}"
else:
icon_data[key] = original_value
# 生成文件内容
content = "export default {\n"
for key, value in icon_data.items():
content += f' "{key}": "{value}",\n'
content += "}\n"
# 确定文件路径
if dir_path and os.path.exists(dir_path):
file_path = os.path.join(dir_path, 'icon.js')
else:
file_path = './icon.js'
# 写入文件
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return True
except Exception as e:
print(f"Error writing to icon.js: {e}")
return False
7. 工具函数【字符串转换为驼峰命名】
7.1 实现分析
- 首先使用正则表达式
re.sub(r'[^a-zA-Z0-9_]', '', text)
移除字符串中的所有特殊字符,仅保留字母、数字和下划线。 - 然后通过下划线
'_'
对处理后的字符串进行分割,得到一个单词列表。 - 最后,将列表中的第一个单词保持小写,后续所有单词的首字母大写并拼接在一起,形成最终的驼峰命名格式字符串。例如,输入
"hello_world_example"
会转换为"helloWorldExample"
。
7.2 实现代码
python
def to_camel_case(self, text):
"""将字符串转换为驼峰命名格式"""
# 移除特殊字符,只保留字母、数字和下划线
import re
text = re.sub(r'[^a-zA-Z0-9_]', '', text)
# 按下划线分割并转换为驼峰命名
parts = text.split('_')
return parts[0] + ''.join(part.capitalize() for part in parts[1:])
8. 总结
- 上传类的方法基本都实现完成,需要处理一些细节问题。
- 就是单文件上传后边准备转为外部文件实现,此处读取实现上传后返回的结果,这样配置就不需要那么麻烦,而且也解决了有的平台上传是参数加密问题,直接执行外部脚本,此单文件上传只需要关心脚本返回的结果。