自己的一个独立 macOS 项目,是关于PDF文件操作的相关功能,上线了两年多了,一直使用的 Swift 和 APPKit 原生进行的功能开发。
链接如下:Easy PDF
有用户提出需求,希望可以开发 PDF 文件转 office 文件的功能。当时看到这个需求时,搜索了一下资料,感觉难度有点大,耗时耗力就没理会。后来无意中发现了一个 Python 库,pdf2docx,可以完美实现 PDF 文件转 Word 文件的功能,但无奈的是这么厉害的功能是 Python 实现的。后来就冒出想法,可不可以在 Swift 项目中去集成 Python 开发的功能库呢,然后开始全网搜索各种资料...
通过这次功能开发实现,发现 Python 搭配 Swift 可以做很多之前没想过的一些功能,还需要待研究。
先说最终成果
成功在 Swift macOS 项目中集成了 Python3 环境,并使用 pip3 安装依赖库,通过 Swift 去调用 Python 第三方库的函数,并且通过了苹果的 App Store 的审核.
一、需要准备的资料
- Python 环境安装,Mac 系统没有 Python3 环境,建议去 Python 官网下载安装包来安装,方便简单,主要是后面可能需要重复卸载和安装,Python 官网下载地址,我是下载的 Python 3.11.0
-
支持 Swift 和 macOS 的 Python 库, Python-Apple-support,下载后解压后有两个文件夹,python-stdlib 和 Python.xcframework,下载的版本号一定要和上面下载的 Python3 安装包是同一个版本,很重要,否则会出现一些奇奇怪怪的问题。 有兴趣的可以深入研究一下,一个使用 Python 来开发 macOS 应用的开源项目 briefcase
-
PythonKit:Swift 调用 Python 函数的库,没有太多的选择
二、项目中集成 Python3 解释器
- 建一个新项目,项目中先把 PythonKit 搞进去,用 Swift Package Manager 或 cocoapods 都可以,推荐使用 Swift Package Manager,项目看起来更加清爽。
- 将 python-stdlib 和 Python.xcframework 文件拖入文件夹,选中 Copy items if needed 和 Create folder references,很重要。
- 检查 Python.xcframework 设置为 Do Not Embed
- 添加 SystemConfiguration.Framework
- 检查 python-stdlib
- 创建 Python 头文件 新建一个文件 module.modulemap,内容如下,将这个文件保存到项目中的Python.xcframework/macos_arm64_x86_64/Headers 中
module Python {
umbrella header "Python.h"
export *
link "Python"
}
7. 添加一个 Run Script,内容如下,取消 Based on dependency analysis
set -e
echo "Signing as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)"
find "$CODESIGNING_FOLDER_PATH/Contents/Resources/python-stdlib/lib-dynload" -name "*.so" -exec /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \;
- 检查 Python3 的运行环境是否已经准备好,可以先把电脑本地安装的 Python3 环境删掉测试一下,看项目中的 Python3 是否是我们自己下载的版本。
import Cocoa
import PythonKit
import Python
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Python 初始化
guard let stdLibPath = Bundle.main.path(forResource: "python-stdlib", ofType: nil) else { return }
guard let libDynloadPath = Bundle.main.path(forResource: "python-stdlib/lib-dynload", ofType: nil) else { return }
setenv("PYTHONHOME", stdLibPath, 1)
setenv("PYTHONPATH", "\(stdLibPath):\(libDynloadPath)", 1)
Py_Initialize()
let sys = Python.import("sys")
print("Python \(sys.version_info.major).\(sys.version_info.minor)")
print("Python Version: \(sys.version)")
print("Python Encoding: \(sys.getdefaultencoding().upper())")
}
}
打印出 Python 版本,说明运行环境已经准备好了
- 使用 pip3 安装第三方依赖,毕竟使用第三方依赖库才是我们的最终目的。 前提条件: Mac 电脑的本地 Python3 环境需要安装好,pip3也得安装,安装很简单,点击下载好的 python-3.11.0-macos11.pkg 直接安装就可以了,pip3 的安装方式就不说了,也没啥难点。
安装 Python 依赖库命令,以安装 pdf2docx 为例
pip3 install pdf2docx -t /Users/Desktop/EmbeddedPython/EmbeddedPython/python-stdlib
/Users/Desktop/EmbeddedPython/EmbeddedPython/python-stdlib 是项目中 python-stdlib 的路径,安装完成后 python-stdlib 文件夹中就会有第三方依赖库了。
- Python 第三方依赖库 pdf2docx 的方法调用。将转换的文件保存到下载文件夹,项目中需要设置沙盒权限。
代码示例:
import Cocoa
import Python
import PythonKit
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let stdLibPath = Bundle.main.path(forResource: "python-stdlib", ofType: nil) else { return }
guard let libDynloadPath = Bundle.main.path(forResource: "python-stdlib/lib-dynload", ofType: nil) else { return }
setenv("PYTHONHOME", stdLibPath, 1)
setenv("PYTHONPATH", "\(stdLibPath):\(libDynloadPath)", 1)
Py_Initialize()
// 测试文件,可编辑的 PDF 文件
let pdfFilePath = Bundle.main.path(forResource: "test", ofType: "pdf")
// 将转换的 docx 文件保存到下载文件夹
var url = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0] as URL
url = url.appendingPathComponent("test.docx")
let docFilePath = url.path
// 导入 pdf2docx 模块
let pdf2docx = Python.import("pdf2docx")
// 创建 Converter
let converter = pdf2docx.Converter(pdfFilePath)
// 调用 convert 方法
converter.convert(docFilePath)
}
}
pdf2docx 在 Python 中的使用方式,可以和上面 Swift 的调用方式进行对比
from pdf2docx import Converter
pdf_file = '/path/to/test.pdf'
docx_file = 'path/to/test.docx'
# pdf 转 docx
cv = Converter(pdf_file)
cv.convert(docx_file) # 所有页面
11. PDF 文件转 Word 的效果,还是很满意的。
三、关于 App Store 的审核
第一次兴致勃勃的打包提交审核,app的安装包由 5MB 变为了 120MB,也是没办法的事情,毕竟项目中集成了 Python3 运行环境和一些必须的 Python 第三方依赖库。
-
第一次被拒,回复说项目中有很多弃用的API,这个是自动触发了苹果机审扫描到的一些标识,开始我也没发现,联系了 Python-Apple-support 的开源作者,才发现了问题,直接给苹果回复,项目中没有使用弃用方法和私有API,他会重新审核。
-
第二次被拒,弃用API的问题解决了,第二次苹果回复了一大堆 Python 里面的方法名,都是带有下划线前缀的方法,被判定为是私有方法,这个不能忍啊,明显也是机器扫描的结果,也是直接回复苹果,描述一下这些方法只是 Python 函数的命名规则,然后把 Python-Apple-support 的开源链接扔给苹果,让他自己去检查这些方法是否是私有API。
-
经过两次回复,苹果最终给审核通过了
四、关于打包上线的问题
打包都是在 Debug 模式下,Release 模式打包的应用程序会崩溃,暂时没找到原因。
python pip3 安装第三方库时,会根据当前电脑的架构去选择安装 arm64 或者 x86_64 的版本,我们在打包应用程序的时候,就需要根据不同的情况设置打包版本。
查看应用程序的简介,可以看到以下两种。
“应用程序(Intel)”打包出来的是英特尔芯片 x86_64 架构,在 M 芯片的设备是通过 Rosetta 技术运行。 “应用程序(通用)”则是通用的。
- 项目是在 M 系列芯片的苹果电脑上打包,如果项目之前已上线且是支持 x86_64 的,那么就修改 “Build Settings”-> Architectures,输入 x86_64,打包出来的应用程序是 “应用程序(Intel)”。
- 项目是在 M 系列芯片的苹果电脑上打包,不需要支持 x86_64,苹果也是允许的,应用只支持 arm64.
- 在英特尔芯片的苹果电脑上打包,打包出来的应用程序是 “应用程序(Intel)”,和第一种情况类似
五、后记
这个地方需要注意一个问题,当电脑上安装了 Python3 运行环境,然后项目中没有集成 Python3 环境,这个时候项目代码中不设置 Python 路径,也是可以正常运行的,Xcode会去查找电脑本地的 Python3 解释器。但是对于需要上线的项目来说,你不能要求用户在本地安装 Python3 的运行环境,所以无奈,只能把 Python3 的解释器集成到项目里面去。