Electron实战 -- 构建Windows桌面版Scrapydweb(一)
背景
Scrapydweb是管理Scrapyd实例(一个爬虫工具)的Web应用,其运行依赖Scrapyd服务的运行,现计划利用Electron将其打包成一个桌面版应用。
思路如下:
- 利用pyinstaller将Scrapyd、Scrapydweb分别打包成可执行文件(one-folder bundle)
- 构建Electron app, 在其中先后启动Scrapyd和Scrapydweb
- 利用electron-forge对Electron app进行打包,构建最终的发布包
详细步骤
打包Scrapyd、Scrapydweb
本文所有操作均在Windows Powershell中进行
确保你的Windows中已安装python>=3.7
检查python版本
安装pyinstaller
pip install pyinstaller
安装Scrapyd
pip install scrapyd
打包Scrapyd
进入Scrapyd的安装目录(可以通过pip show scrapyd
查看),观察到scrapyd的启动文件是scripts/scrapyd_run.py
,于是执行下述打包指令。
# 生成spec文件
pyi-makespec `
--add-data "VERSION:scrapyd" `
--add-data "txapp.py:scrapyd" `
--add-data "default_scrapyd.conf:scrapyd" `
--hidden-import "scrapyd.app" `
--hidden-import "scrapyd.spiderqueue" `
--hidden-import "scrapyd.eggstorage" `
--hidden-import "scrapyd.jobstorage" `
--hidden-import "scrapyd.launcher" `
--hidden-import "scrapyd.website" `
--hidden-import "scrapyd.webservice" `
--hide-console "hide-early" `
scripts/scrapyd_run.py
# 按照spec文件打包
pyinstaller scrapyd_run.spec
程序启动(例如双击.exe文件)会弹出一个控制台窗口,如果想隐藏这个窗口,就可以像上面这样指定hide-console=hide-early
打包完成后,在当前目录下会看到dist目录,目录结构如下。其中,scrapyd_run.exe就是最终的可执行文件。由于我们是以one-folder模式打的包,_internal目录也是不可丢弃的。
安装Scrapydweb
pip install scrapydweb
打包Scrapydweb
由于Scrapydweb启动依赖一个配置文件,且Scrapydweb在第一次启动检查到该文件不存在时,会自动生成该配置文件。因此我们先启动一次Scrapyweb,在命令行直接运行
scrapydweb
即可。
这时,可以在当前目录下看到新生成了scrapydweb_settings_v10.py。由于该示例中我们只会启动一个本地的scrapyd实例,因此需要修改一下该配置文件。将其中('username', 'password', 'localhost', '6801', 'group'),
一行注释掉。
SCRAPYD_SERVERS = [
'127.0.0.1:6800',
# 'username:password@localhost:6801#group',
#('username', 'password', 'localhost', '6801', 'group'),
]
接下来类似打包Scrapyd的流程,先进入Scrapydweb的安装目录,观察到其启动文件是run.py,于是执行下述指令。
# 生成spec文件
pyi-makespec `
--add-data "data:scrapydweb/data" `
--add-data "static:scrapydweb/scrapydweb/static" `
--add-data "templates:scrapydweb/templates" `
--hide-console "hide-early" `
run.py
# 按照spec文件打包
pyinstaller run.spec
再将上面得到的scrapydweb_settings_v10.py拷贝到dist/run目录下,目录结构如下。其中,run.exe是最终的可执行文件。
构建electron app
检查是否安装node、git
创建electron app
npm init electron-app@latest my-app
此时,可以看到在my-app目录下生成了若干目录和文件。编辑src/index.js
,在其中启动scrapyd和scrapydweb子进程。最终完整的index.js如下:
const { app, BrowserWindow } = require('electron');
const path = require('node:path');
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
app.quit();
}
const backendScrapyd = require('child_process').spawn('scrapyd_run.exe',
{cwd: path.join(app.getPath('exe'), '../scrapyd_run')});
const backendScrapydweb = require('child_process').spawn('run.exe',
{cwd: path.join(app.getPath('exe'), '../run')});
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
// and load the index.html of the app.
// mainWindow.loadFile(path.join(__dirname, 'index.html'));
mainWindow.loadURL('http://localhost:5000');
// Open the DevTools.
// mainWindow.webContents.openDevTools();
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow();
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
backendScrapydweb.kill();
backendScrapyd.kill();
app.quit();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
打包electron app
npm run make
这里make失败了,原因是ENOTEMPTY,类似错误参见github.com/electron/fo…
可以看到在当前目录下生成一个out目录,里面是构建好的文件。将上面打包好的scrapyd和scrapydweb复制到out目录,结构如下。my-app.exe就是最终的可执行文件,再将整个目录压缩即可得到一个完整的桌面版Scrapydweb安装包。
改进方向
- 合并scrapyd和scrapydweb打包,减少体积
- index.js中启动子进程之间作同步(考虑到scrapydweb依赖scrapyd)
- electron-forge直接将外部文件打包进去(是否可以?),而不是通过手动压缩的方式
存在的问题(补充)
- 打包(package&deploy)爬虫egg文件失败
- 列出爬虫(获取爬虫列表)错误
这2个错误的原因均是由于在Scrapyd、Scrapydweb运行过程中调用了其他python程序。例如Scrapyd在列出爬虫的代码(见下方get_spider_list
)中使用了sys.executable
。它在python中表示python解释器(在Windows中即python.exe),但在Pyinstaller中表示打包后的可执行文件(在本例中即scrapyd_run.exe),因此最终我们得到错误的执行结果。
Pyinstaller对
sys.executable
的解释请参考pyinstaller.org/en/stable/r…
def get_spider_list(project, runner=None, pythonpath=None, version=''):
"""Return the spider list from the given project, using the given runner"""
if "cache" not in get_spider_list.__dict__:
get_spider_list.cache = UtilsCache()
try:
return get_spider_list.cache[project][version]
except KeyError:
pass
if runner is None:
runner = Config().get('runner')
env = os.environ.copy()
env['PYTHONIOENCODING'] = 'UTF-8'
env['SCRAPY_PROJECT'] = project
if pythonpath:
env['PYTHONPATH'] = pythonpath
if version:
env['SCRAPYD_EGG_VERSION'] = version
# 这里sys.executable指python解释器
pargs = [sys.executable, '-m', runner, 'list', '-s', 'LOG_STDOUT=0']
proc = Popen(pargs, stdout=PIPE, stderr=PIPE, env=env)
out, err = proc.communicate()
# ...
类似的问题在github.com/pyinstaller… 中有提及,根据Pyinstaller成员rokm的解释,像Scrapyd这样的项目可能并不适合用Pyinstaller打包。
转载自:https://juejin.cn/post/7356406517942124595