执行 QGIS 工作排程

使用 Python 脚本 (PyQGIS) 搭配处理框架,许多工作可以在 QGIS 中自动处理。在一般情况下,我们是在 QGIS 开启时,手动执行这些脚本;不过有些时候,你或许会希望有方法可以让这些脚本在不开启 QGIS 的情况下,直接于指令列中执行。没问题,在本教学中,我们就来看看要怎么使用独立的 Python 环境,使用 QGIS 提供的函数库与处理框架,撰写脚本和工作排程,并直接在命令列中执行。

内容说明

让我们假设我们要为某区域的 shapefile 进行分析,此 shapefile 每天会更新一次,而我们随时都要使用最新的档案,而且在我们使用档案内的资料之前,还要稍微「清理」一下它们。因此,我们要设计一个 QGIS 流程,可以每日自动执行以上工作,这样我们随时都能把我们的资料分析保持在最新的状态。以下我们要撰写一个独立的 Python 脚本,每日下载最新的 shapefile,然后对其执行拓朴清理(topological cleaning)运算。

你还会学到这些

  • 使用 Python 下载档案并解压缩

  • 使用 PyQGIS 执行地理运算

  • 修正向量图层中的拓扑误差

取得资料

Geofabrik 提供每日更新的 OpenStreetMap 资料集的 shapefile。

我们在本练习中要使用 斐济的 shapefiles。下载 fiji-latest.shp.zip 然后解压到硬碟中。

资料来源 [GEOFABRIK]

操作流程

  1. 首先我们来手动清理此 shapefile,熟悉一下我们等一下要在 Python 脚本中加入的指令。打开 QGIS,选择 图层 ‣ 加入图层 ‣ 加入向量图层

../_images/1189.png
  1. 选择下载后解压缩的内容中的 roads.shp 并按下 确定

../_images/2145.png
  1. 第一件事是重投影道路图层到专案的 CRS,这样一来我们就可以使用 公尺 而不是角度来当作等下分析使用的单位。选择 地理运算 ‣ 工具箱

../_images/381.png
  1. 寻找 Reproject layer 工具,点两下开启对话窗。

../_images/459.png
  1. Reproject layer 视窗中,输入图层 选择 roads目标 CRS 选择 EPSG:3460 Fiji 1986 / Fiji Map Grid,按下 Run

../_images/560.png
  1. 处理完成后可以看到重投影的图层已载入到 QGIS 中。选择 地理运算 ‣ 历程与日志

../_images/657.png
  1. 历程与日志 视窗中,展开 Algorithm 资料夹然后选择最新一笔的纪录,就会看到完整的处理指令显示在下方面板中。这就是我们等下要使用在脚本内的指令。

../_images/756.png
  1. 回到 QGIS 视窗,按下右下角的 CRS 按钮。

../_images/854.png
  1. 专案属性 | CRS 视窗中,勾选 开启即时 CRS 转换,然后选择 EPSG:3460 Fiji 1986 / Fiji Map Grid 为专案 CRS,这样就能确保原本的图层和重投影过的图层都会正确显示。

../_images/953.png
  1. 现在我们要来执行清理操作了。GRASS 具有强大的拓朴清理工具,在 QGIS 中可透过 v.clean 运算法存取。在 地理运算工具箱 中寻找此演算法,然后点两下开启视窗。

../_images/1052.png
  1. 有关此工具的更多说明,可以在 Help 分页中找到,而在此教学中,我们要使用 snap 工具来清除任何在 1 公尺之内多馀的线条顶点。在 Layer to clean 中选择 Reprojected layer,然后在 Cleaning tool 中选择 snap,在 Threshold 中输入 1.00,其他栏位留白,按下 Run

../_images/1190.png
  1. 处理完成后,会有 2 个新图层加入 QGIS 中。Cleaned 是经过拓朴误差修正后的图层,而 Errors 则显示了有修改过的图征,因此你可以参考 Errors 图层,缩放地图以查看被移除的顶点们。

../_images/1252.png
  1. 选择 地理运算 ‣ 历程与日志,然后查看我们等一下要使用的命令列指令。

../_images/1350.png
  1. 现在我们已经做好写程式码的准备了!如果你需要设定文字编辑器或 IDE,请参考在 以 Python 制作附加元件 一章中的 编辑器或 Python IDE 的说明。为了要在独立的 Python 脚本中使用 QGIS,我们必须要先设定好系统配置才行。有个方便的方法是透过 .bat 档案来执行,此档案会先设定好正确的配置选项,然后再呼叫 Python 脚本。因此,请建立称为 launch.bat 的新档案然后输入以下文字,记得根据你的 QGIS 配置改动其中的一些参数值,也别忘记把存取 Python 档案的路径中的使用者名称替换成你自己的。如果你是透过 OSGeo4W Installer 安装 QGIS 的话,在此档案中的路径会与你的系统路径相同。最后存档到你的桌面。

注解

Linux 和 Mac 的使用者则需要使用 shell 脚本来设定路径和环境变数。

REM Change OSGEO4W_ROOT to point to the base install folder
SET OSGEO4W_ROOT=C:\OSGeo4W64
SET QGISNAME=qgis
SET QGIS=%OSGEO4W_ROOT%\apps\%QGISNAME%
set QGIS_PREFIX_PATH=%QGIS%
REM Gdal Setup
set GDAL_DATA=%OSGEO4W_ROOT%\share\gdal\
REM Python Setup
set PATH=%OSGEO4W_ROOT%\bin;%QGIS%\bin;%PATH%
SET PYTHONHOME=%OSGEO4W_ROOT%\apps\Python27
set PYTHONPATH=%QGIS%\python;%PYTHONPATH%

REM Launch python job
python c:\Users\Ujaval\Desktop\download_and_clean.py
pause
../_images/1449.png
  1. 建立新的 Python 档然后输入以下程式码,档名取为 download_and_clean.py 然后储存至桌面。

from qgis.core import *
print 'Hello QGIS!'
../_images/1547.png
  1. 切换到桌面,找到 launch.bat 然后点它两下,脚本会开始执行,并会同时开启一个命令列视窗。如果你在命令列视窗中看到了 Hello QGIS! 字样出现,那就表示之前的设定和操作一切顺利。如果你发现错误或没有看到以上文字的话,请检查 launch.bat 的内容,然后确认所有的路径在你的作业系统中都是正确的。

../_images/1643.png
  1. 回到文字编辑器,编辑 download_and_clean.py 并加入以下的程式码。这个段落用来快速启动 QGIS,如果你在 QGIS 中执行脚本,并不需要此段落;但由于我们要在 QGIS 不开启的状况下执行,程式最前端就必须放上这几行,同时要注意有把使用者名称改成你自己的才行。完成之后,存档然后再次执行 launch.bat,如果你看到 Hello QGIS! 列印出来,就表示可以开始在脚本中加入地理运算的流程了。

import sys
from qgis.core import *

# Initialize QGIS Application
QgsApplication.setPrefixPath("C:\\OSGeo4W64\\apps\\qgis", True)
app = QgsApplication([], True)
QgsApplication.initQgis()

# Add the path to Processing framework
sys.path.append('c:\\Users\\Ujaval\\.qgis2\\python\\plugins')

# Import and initialize Processing framework
from processing.core.Processing import Processing
Processing.initialize()
import processing

print 'Hello QGIS!'
../_images/1740.png
  1. 我们从地理运算的历程日志中看到的第一个运算指令,就是要放在这边的重投影指令。因此,贴上此段指令到脚本中,然后前后加入如下所示的几行。注意运算指令的回传值为字典(dict)变数,其中包含了输出图层的路径,因此我们在这边把字典存成 ret 然后再列印出重投影后图层的路径。

roads_shp_path = "C:\\Users\\Ujaval\\Downloads\\fiji-latest.shp\\roads.shp"
ret = processing.runalg('qgis:reprojectlayer', roads_shp_path, 'EPSG:3460',
None)
output = ret['OUTPUT']
print output
../_images/1840.png
  1. 透过 launch.bat 执行此脚本,就可以看到新建立的重投影图层的路径。

../_images/1934.png
  1. 接下来要增加拓朴清理的程式码。由于这会是我们的最终输出,所以我们要在 grass.v.clean 函数中的最后 2 个参数加上输出档路径。如果这两个参数是空白的话,输出档就只会放在一个暂时资料夹之中。

processing.runalg("grass:v.clean",
                  output,
                  1,
                  1,
                  None,
                  -1,
                  0.0001,
                  'C:\\Users\\Ujaval\\Desktop\\clean.shp',
                  'C:\Users\\Ujaval\\Desktop\\errors.shp')
../_images/2027.png
  1. 执行脚本后可以看到 2 个新的 shapefiles 出现在你的桌面了,到此为止我们已完成脚本的地理运算部分,接下来我们来加入从网页中下载资料和自动解压缩的程式码。我们也顺便把解压缩后的档案路径储存起来,给之后的地理运算函数使用。要做到这些得需要一些额外的模组才行。(本教学最后附有完整的脚本档案供参考)

import os
import urllib
import zipfile
import tempfile

temp_dir = tempfile.mkdtemp()
download_url = 'http://download.geofabrik.de/australia-oceania/fiji-latest.shp.zip'
print 'Downloading file'
zip, headers = urllib.urlretrieve(download_url)
with zipfile.ZipFile(zip) as zf:
    files = zf.namelist()
    for filename in files:
        if 'roads' in filename:
            file_path = os.path.join(temp_dir, filename)
            f = open(file_path, 'wb')
            f.write(zf.read(filename))
            f.close()
            if filename == 'roads.shp':
                roads_shp_path = file_path
../_images/2146.png
  1. 执行完成的脚本。每次执行脚本之后,就会有一份新的资料被下载并进行处理。

../_images/2227.png
  1. 为了要每日自动执行脚本,我们可以使用 Windows 中的 工作排程器。开启工作排程器之后,按下 建立基本工作

注解

Linux 和 Mac 的使用者可以使用 crontab 执行排程工作。

../_images/2324.png
  1. 把工作命名为 Daily Download and Cleanup 然后按下 下一步

../_images/2422.png
  1. 触发程序 设定中选择 每天,然后按 下一步

../_images/2520.png
  1. 选择你喜欢的时段然后按 下一步

../_images/2618.png
  1. 执行 设定中选择 启动程式,按 下一步

../_images/2718.png
  1. 按下 浏览 然后选择 launch.bat 脚本,按下 下一步

../_images/2816.png
  1. 在最后一个视窗按下 完成,就完成了例行排程的设定。从现在开始脚本就会每天在你选择的时段执行,然后新的、处理过的资料就会每天产生。

../_images/2914.png

以下放上完整的 download_and_clean.py 档做为参考。

import sys
from qgis.core import *

import os
import urllib
import zipfile
import tempfile

# Initialize QGIS Application
QgsApplication.setPrefixPath("C:\\OSGeo4W64\\apps\\qgis", True)
app = QgsApplication([], True)
QgsApplication.initQgis()

# Add the path to Processing framework  
sys.path.append('c:\\Users\\Ujaval\\.qgis2\\python\\plugins')

# Import and initialize Processing framework
from processing.core.Processing import Processing
Processing.initialize()
import processing

# Download and unzip the latest shapefile
temp_dir = tempfile.mkdtemp()
download_url = 'http://download.geofabrik.de/australia-oceania/fiji-latest.shp.zip'
print 'Downloading file'
zip, headers = urllib.urlretrieve(download_url)
with zipfile.ZipFile(zip) as zf:
    files = zf.namelist()
    for filename in files:
        if 'roads' in filename:
            file_path = os.path.join(temp_dir, filename)
            f = open(file_path, 'wb')
            f.write(zf.read(filename))
            f.close()
            if filename == 'roads.shp':
                roads_shp_path = file_path

print 'Downloaded file to %s' % roads_shp_path

# Reproject the Roads layer
print 'Reprojecting the roads layer'

ret = processing.runalg('qgis:reprojectlayer', roads_shp_path, 'EPSG:3460', None)
output = ret['OUTPUT']

# Clean the Roads layer
print 'Cleaning the roads layer'

processing.runalg("grass:v.clean",
                  output,
                  1,
                  1,
                  None,
                  -1,
                  0.0001,
                  'C:\\Users\\Ujaval\\Desktop\\clean.shp',
                  'C:\Users\\Ujaval\\Desktop\\errors.shp')
print 'Success'