執行 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/1207.png
  1. 選擇下載後解壓縮的內容中的 roads.shp 並按下 確定

../_images/2174.png
  1. 第一件事是重投影道路圖層到專案的 CRS,這樣一來我們就可以使用 公尺 而不是角度來當作等下分析使用的單位。選擇 地理運算 ‣ 工具箱

../_images/3121.png
  1. 尋找 Reproject layer 工具,點兩下開啟對話窗。

../_images/473.png
  1. Reproject layer 視窗中,輸入圖層 選擇 roads目標 CRS 選擇 EPSG:3460 Fiji 1986 / Fiji Map Grid,按下 Run

../_images/567.png
  1. 處理完成後可以看到重投影的圖層已載入到 QGIS 中。選擇 地理運算 ‣ 歷程與日誌

../_images/664.png
  1. 歷程與日誌 視窗中,展開 Algorithm 資料夾然後選擇最新一筆的紀錄,就會看到完整的處理指令顯示在下方面板中。這就是我們等下要使用在腳本內的指令。

../_images/763.png
  1. 回到 QGIS 視窗,按下右下角的 CRS 按鈕。

../_images/861.png
  1. 專案屬性 | CRS 視窗中,勾選 開啟即時 CRS 轉換,然後選擇 EPSG:3460 Fiji 1986 / Fiji Map Grid 為專案 CRS,這樣就能確保原本的圖層和重投影過的圖層都會正確顯示。

../_images/960.png
  1. 現在我們要來執行清理操作了。GRASS 具有強大的拓樸清理工具,在 QGIS 中可透過 v.clean 運算法存取。在 地理運算工具箱 中尋找此演算法,然後點兩下開啟視窗。

../_images/1068.png
  1. 有關此工具的更多說明,可以在 Help 分頁中找到,而在此教學中,我們要使用 snap 工具來清除任何在 1 公尺之內多餘的線條頂點。在 Layer to clean 中選擇 Reprojected layer,然後在 Cleaning tool 中選擇 snap,在 Threshold 中輸入 1.00,其他欄位留白,按下 Run

../_images/11107.png
  1. 處理完成後,會有 2 個新圖層加入 QGIS 中。Cleaned 是經過拓樸誤差修正後的圖層,而 Errors 則顯示了有修改過的圖徵,因此你可以參考 Errors 圖層,縮放地圖以查看被移除的頂點們。

../_images/1269.png
  1. 選擇 地理運算 ‣ 歷程與日誌,然後查看我們等一下要使用的命令列指令。

../_images/1366.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/1463.png
  1. 建立新的 Python 檔然後輸入以下程式碼,檔名取為 download_and_clean.py 然後儲存至桌面。

from qgis.core import *
print 'Hello QGIS!'
../_images/1558.png
  1. 切換到桌面,找到 launch.bat 然後點它兩下,腳本會開始執行,並會同時開啟一個命令列視窗。如果你在命令列視窗中看到了 Hello QGIS! 字樣出現,那就表示之前的設定和操作一切順利。如果你發現錯誤或沒有看到以上文字的話,請檢查 launch.bat 的內容,然後確認所有的路徑在你的作業系統中都是正確的。

../_images/1655.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/1752.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/1849.png
  1. 透過 launch.bat 執行此腳本,就可以看到新建立的重投影圖層的路徑。

../_images/1940.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/2035.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/2175.png
  1. 執行完成的腳本。每次執行腳本之後,就會有一份新的資料被下載並進行處理。

../_images/2234.png
  1. 為了要每日自動執行腳本,我們可以使用 Windows 中的 工作排程器。開啟工作排程器之後,按下 建立基本工作

備註

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

../_images/2331.png
  1. 把工作命名為 Daily Download and Cleanup 然後按下 下一步

../_images/2430.png
  1. 觸發程序 設定中選擇 每天,然後按 下一步

../_images/2527.png
  1. 選擇你喜歡的時段然後按 下一步

../_images/2625.png
  1. 執行 設定中選擇 啟動程式,按 下一步

../_images/2724.png
  1. 按下 瀏覽 然後選擇 launch.bat 腳本,按下 下一步

../_images/2823.png
  1. 在最後一個視窗按下 完成,就完成了例行排程的設定。從現在開始腳本就會每天在你選擇的時段執行,然後新的、處理過的資料就會每天產生。

../_images/2920.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'

If you want to give feedback or share your experience with this tutorial, please comment below. (requires GitHub account)