执行 QGIS 工作排程

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

内容说明

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

你还会学到这些

  • 使用 Python 下载档案并解压缩
  • 使用 PyQGIS 执行地理运算
  • 修正向量图层中的拓扑误差

取得资料

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

我们在本练习中要使用`shapefiles for Fiji <http://download.geofabrik.de/australia-oceania.html>`_ 。下载`fiji-latest.shp.zip <http://download.geofabrik.de/australia-oceania/fiji-latest.shp.zip>`_ 然后解压到硬碟中。

资料来源 [GEOFABRIK]

操作流程

  1. 首先我们来手动清理此 shapefile,熟悉一下我们等一下要在 Python 脚本中加入的指令。打开 QGIS,选择:menuselection:Layer –> Add Layer –> Add Vector Layer
../_images/1129.png
  1. 选择下载后解压缩的内容中的``roads.shp`` 并按下:guilabel:Open
../_images/270.png
  1. 第一件事是重投影道路图层到专案的 CRS,这样一来我们就可以使用*meters* 而不是角度来当作等下分析使用的单位。选择:menuselection:Processing –> Toolbox
../_images/338.png
  1. 寻找:guilabel:Reproject layer 工具,点两下开启对话窗。
../_images/428.png
  1. 在:guilabel:Reproject layer 视窗中,guilabel:Input layer 选择``roads``,guilabel:Target CRS`选择 ``EPSG:3460 Fiji 1986 / Fiji Map Grid`,按下:guilabel:Run
../_images/529.png
  1. 处理完成后可以看到重投影的图层已载入到 QGIS 中。选择:menuselection:Processing –> History and Log..
../_images/627.png
  1. 在:guilabel:History and Log 视窗中,展开:guilabel:`Algorithm`资料夹然后选择最新一笔的纪录,就会看到完整的处理指令显示在下方面板中。这就是我们等下要使用在脚本内的指令。
../_images/726.png
  1. 回到 QGIS 视窗,按下右下角的:guilabel:CRS 按钮。
../_images/825.png

9. 在 Project Properties | CRS 视窗中,勾选 Enable on-the-fly CRS transformation 然后选择``EPSG:3460 Fiji 1986 / Fiji Map Grid``为专案 CRS,这样就能确保原本的图层和重投影过的图层都会正确显示。

../_images/923.png

10. N现在我们要来执行清理操作了。GRASS 具有强大的拓朴清理工具,在 QGIS 中可透过 v.clean 运算法存取。 在 :guilabel:`Processing Toolbox`中寻找此演算法,然后点两下开启视窗。

../_images/1024.png

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

../_images/1130.png

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

../_images/1225.png
  1. 选择:menuselection:Processing –> History and Log 然后查看我们等一下要使用的命令列指令。
../_images/1323.png

14. 现在我们已经做好写程序代码的准备了!如果你需要设定文字编辑器或 IDE, 请参考在:doc:building_a_python_plugin`一章中的 编辑器或 **A Text Editor or a 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/1422.png
  1. 建立新的 Python 档然后输入以下程序代码,档名取为``download_and_clean.py`` 然后储存至桌面。
from qgis.core import *
print 'Hello QGIS!'
../_images/1521.png

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

../_images/1620.png

17. 回到文字编辑器,编辑``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/1719.png

18.我们从地理运算的历程日志中看到的第一个运算指令,就是要放在这边的重投影指令。 因此,贴上此段指令到脚本中,然后前后加入如下所示的几行。注意运算指令的回传值为字典(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/1819.png
  1. 透过 launch.bat 执行此脚本,就可以看到新建立的重投影图层的路径。
../_images/1917.png

20. 接下来要增加拓朴清理的程序代码。由于这会是我们的最终输出,所以我们要在``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/2014.png

21.执行脚本后可以看到 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/2118.png
  1. 执行完成的脚本。每次执行脚本之后,就会有一份新的资料被下载并进行处理。
../_images/2217.png
  1. 为了要每日自动执行脚本,我们可以使用 Windows 中的``Task Scheduler`` 开启工作排程器之后,按下:guilabel:Create Basic Task

注解

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

../_images/2314.png
  1. 把工作命名为 Daily Download and Cleanup 然后按下 Next
../_images/2412.png
  1. Trigger 设定中选择``Daily`` 然后按 Next
../_images/2510.png
  1. 选择你喜欢的时段然后按 Next
../_images/2610.png
  1. 在:guilabel:Action`设定中选择``Start a program`, 按 Next
../_images/278.png

28. 按下:guilabel:Browse 然后选择 launch.bat 脚本,按下 Next

../_images/286.png
  1. 在最后一个视窗按下:guilabel:Finish 就完成了例行排程的设定。从现在开始脚本就会每天在你选择的时段执行,然后新的、处理过的资料就会每天产生。
../_images/295.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'