likes
comments
collection
share

Electron实战 -- 构建Windows桌面版Scrapydweb(一)

作者站长头像
站长
· 阅读数 4

背景

Scrapydweb是管理Scrapyd实例(一个爬虫工具)的Web应用,其运行依赖Scrapyd服务的运行,现计划利用Electron将其打包成一个桌面版应用。

思路如下:

  1. 利用pyinstaller将Scrapyd、Scrapydweb分别打包成可执行文件(one-folder bundle)
  2. 构建Electron app, 在其中先后启动Scrapyd和Scrapydweb
  3. 利用electron-forge对Electron app进行打包,构建最终的发布包

详细步骤

打包Scrapyd、Scrapydweb

本文所有操作均在Windows Powershell中进行

确保你的Windows中已安装python>=3.7

检查python版本

Electron实战 -- 构建Windows桌面版Scrapydweb(一)

安装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目录也是不可丢弃的。

Electron实战 -- 构建Windows桌面版Scrapydweb(一)

安装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实战 -- 构建Windows桌面版Scrapydweb(一)

构建electron app

检查是否安装node、git

Electron实战 -- 构建Windows桌面版Scrapydweb(一)

创建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安装包。

Electron实战 -- 构建Windows桌面版Scrapydweb(一)

改进方向

  • 合并scrapyd和scrapydweb打包,减少体积
  • index.js中启动子进程之间作同步(考虑到scrapydweb依赖scrapyd)
  • electron-forge直接将外部文件打包进去(是否可以?),而不是通过手动压缩的方式

存在的问题(补充)

  1. 打包(package&deploy)爬虫egg文件失败
  2. 列出爬虫(获取爬虫列表)错误

这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打包。

Electron实战 -- 构建Windows桌面版Scrapydweb(一)