20. QGIS服务器和Python

20.1. 引言

要了解有关QGIS Server的更多信息,请阅读 QGIS服务器指南/手册

QGIS服务器是三种不同的东西:

  1. QGIS服务器库:提供用于创建OGC Web服务的API的库

  2. QGIS服务器FCGI:一个FCGI二进制应用程序 qgis_mapserv.fcgi 它与Web服务器一起实现了一组OGC服务(WMS、WFS、WCS等)。和OGC API(WFS3/OAPIF)

  3. QGIS开发服务器:一个开发服务器二进制应用程序 qgis_mapserver 实现一组OGC服务(WMS、WFS、WCS等)和OGC API(WFS3/OAPIF)

食谱的这一章主要关注第一个主题,通过解释QGIS服务器API的用法,它展示了如何使用Python扩展、增强或定制服务器行为,或者如何使用QGIS服务器API将QGIS服务器嵌入到另一个应用程序中。

有几种不同的方法可以改变QGIS服务器的行为或扩展其功能以提供新的定制服务或API,这些是您可能面临的主要情况:

  • EMBEDDING → 从另一个Python应用程序使用QGIS服务器API

  • STANDALONE → 将QGIS服务器作为独立的WSGI/HTTP服务运行

  • FILTERS → 使用筛选器插件增强/定制QGIS服务器

  • SERVICES → 添加新的 SERVICE

  • OGC接口 → 添加新的 OGC API

嵌入和独立的应用程序需要直接从另一个Python脚本或应用程序使用QGIS Server PythonAPI。当您想要向标准的QGIS Server二进制应用程序(FCGI或开发服务器)添加自定义特性时,其余选项更适合:在这种情况下,您需要为服务器应用程序编写一个Python插件并注册您的自定义过滤器、服务或API。

20.2. 服务器API基础知识

典型的QGIS服务器应用程序涉及的基本类包括:

QGIS服务器FCGI或开发服务器工作流可以总结如下:

1initialize the QgsApplication
2create the QgsServer
3the main server loop waits forever for client requests:
4    for each incoming request:
5        create a QgsServerRequest request
6        create a QgsServerResponse response
7        call QgsServer.handleRequest(request, response)
8            filter plugins may be executed
9        send the output to the client

在内部 QgsServer.handleRequest(request, response) 方法调用筛选器插件回调,并 QgsServerRequestQgsServerResponse 插件可以通过 QgsServerInterface 班级。

警告

QGIS服务器类不是线程安全的,在构建基于QGIS Server API的可伸缩应用程序时,您应该始终使用多处理模型或容器。

20.3. 独立或嵌入

对于独立的服务器应用程序或嵌入,您需要直接使用上面提到的服务器类,将它们包装到一个Web服务器实现中,该实现管理与客户端的所有HTTP协议交互。

下面是QGIS Server API用法的最小示例(不含HTTP部分):

 1from qgis.core import QgsApplication
 2from qgis.server import *
 3app = QgsApplication([], False)
 4
 5# Create the server instance, it may be a single one that
 6# is reused on multiple requests
 7server = QgsServer()
 8
 9# Create the request by specifying the full URL and an optional body
10# (for example for POST requests)
11request = QgsBufferServerRequest(
12    'http://localhost:8081/?MAP=/qgis-server/projects/helloworld.qgs' +
13    '&SERVICE=WMS&REQUEST=GetCapabilities')
14
15# Create a response objects
16response = QgsBufferServerResponse()
17
18# Handle the request
19server.handleRequest(request, response)
20
21print(response.headers())
22print(response.body().data().decode('utf8'))
23
24app.exitQgis()

这里是一个完整的独立应用程序示例,用于在QGIS源代码库上进行持续集成测试,它展示了一系列不同的插件筛选器和身份验证方案(不是为了生产,因为它们只是为了测试而开发,但学习起来仍然很有趣): qgis_wrapped_server.py

20.4. 服务器插件

当QGIS服务器应用程序启动时,只需加载一次服务器Python插件,即可用于注册过滤器、服务或API。

服务器插件的结构与桌面插件非常相似, QgsServerInterface 对象对插件可用,并且插件可以通过使用服务器接口公开的方法之一将一个或多个定制过滤器、服务或API注册到相应的注册表。

20.4.1. 服务器筛选器插件

筛选器有三种不同的风格,可以通过对下面的一个类进行子类化并通过调用 QgsServerInterface

过滤器类型

基类

QgsServerInterface注册

I/O

QgsServerFilter

registerFilter()

访问控制

QgsAccessControlFilter

registerAccessControl()

高速缓存

QgsServerCacheFilter

registerServerCache()

20.4.1.1. I/O过滤器

I/O过滤器可以修改核心服务(WMS、WFS等)的服务器输入和输出(请求和响应)。允许对服务工作流进行任何类型的操作。例如,可以限制对选定层的访问、将XSL样式表注入到XML响应、向生成的WMS图像添加水印等。

在这一点上,您可能会发现快速浏览 server plugins API docs

每个筛选器应至少实现以下三个回调之一:

所有筛选器都可以访问请求/响应对象 (QgsRequestHandler ),并且可以操作它的所有属性(输入/输出)并引发异常(尽管我们将在下面看到一种非常特殊的方式)。

所有这些方法都返回一个布尔值,指示调用是否应该传播到后续筛选器。如果其中一个方法返回 False 然后链停止,否则调用将传播到下一个筛选器。

下面的伪代码显示了服务器如何处理典型请求以及何时调用过滤器的回调:

 1for each incoming request:
 2    create GET/POST request handler
 3    pass request to an instance of QgsServerInterface
 4    call onRequestReady filters
 5
 6    if there is not a response:
 7        if SERVICE is WMS/WFS/WCS:
 8            create WMS/WFS/WCS service
 9            call service’s executeRequest
10                possibly call onSendResponse for each chunk of bytes
11                sent to the client by a streaming services (WFS)
12        call onResponseComplete
13    request handler sends the response to the client

以下段落详细介绍了可用的回调。

20.4.1.1.1. 在请求就绪时

当请求准备就绪时调用此函数:传入的URL和数据已被解析,并在进入核心服务(WMS、WFS等)之前调用。开关,这是您可以操作输入并执行如下操作的点:

  • 身份验证/授权

  • 重定向

  • 添加/删除某些参数(例如类型名)

  • 引发异常

您甚至可以通过更改以下内容来完全替换核心服务 SERVICE 参数,从而完全绕过核心服务(这并不是说这有多大意义)。

20.4.1.1.2. 发送响应时

每当从响应缓冲区刷新任何部分输出(即 FCGI stdout 如果使用FCGI服务器)并从那里发送到客户端。当大量内容被流传输(如WFS GetFeature)时,就会出现这种情况。在这种情况下 onSendResponse() 可能会被多次调用。

请注意,如果响应不是流的,则 onSendResponse() 将根本不会被调用。

在所有情况下,最后(或唯一)块将在调用之后发送到客户端 onResponseComplete()

归来 False 将防止将数据刷新到客户端。当插件希望从响应中收集所有块并检查或更改响应时,这是可取的 onResponseComplete()

20.4.1.1.3. 完成响应时

当核心服务(如果命中)完成其流程并且准备将请求发送到客户端时,调用一次。如上所述,此方法将在将最后(或唯一)数据块发送到客户端之前调用。对于流服务,多个呼叫 onSendResponse() 可能已经被召唤了。

onResponseComplete() 是提供新服务实现(WPS或定制服务)和对来自核心服务的输出执行直接操作(例如,在WMS图像上添加水印)的理想位置。

请注意,返回 False 将阻止执行下一个插件 onResponseComplete() 但是,在任何情况下,都要阻止将响应发送到客户端。

20.4.1.1.4. 从插件引发异常

在这个主题上仍有一些工作要做:当前实现可以通过设置 QgsRequestHandler 属性设置为QgsMapServiceException的实例,这样,主C++代码就可以捕获已处理的Python异常并忽略未处理的异常(或者更好的做法是:记录它们)。

这种方法基本上是有效的,但它并不是非常“僵化”的:更好的方法是从Python代码中引发异常,然后看到它们冒泡到C++循环中进行处理。

20.4.1.1.5. 编写服务器插件

服务器插件是标准的QGIS Python插件,如中所述 开发Python插件 ,这只是提供了一个额外的(或替代的)界面:一个典型的QGIS桌面插件可以通过 QgisInterface 实例,则服务器插件只能访问 QgsServerInterface 它在QGIS Server应用程序上下文中执行时。

要使QGIS服务器知道插件具有服务器接口,需要特殊的元数据条目(在 metadata.txt ):

server=True

重要

只有具有 server=True 元数据集将由QGIS服务器加载和执行。

这个 qgis3-server-vagrant 这里讨论的示例插件(还有更多)在GitHub上提供,一些服务器插件也发布在官方的 QGIS plugins repository

20.4.1.1.5.1. 插件文件

下面是我们的示例服务器插件的目录结构。

1PYTHON_PLUGINS_PATH/
2  HelloServer/
3    __init__.py    --> *required*
4    HelloServer.py  --> *required*
5    metadata.txt   --> *required*
20.4.1.1.5.1.1. __init__.py

该文件是Python的导入系统所必需的。此外,QGIS服务器要求该文件包含 serverClassFactory() 函数,该函数在服务器启动时将插件加载到QGIS服务器中时调用。它接收对 QgsServerInterface 并且必须返回插件类的实例。这就是示例插件 __init__.py 看起来像是:

def serverClassFactory(serverIface):
    from .HelloServer import HelloServerServer
    return HelloServerServer(serverIface)
20.4.1.1.5.1.2. HelloServer.py

这就是魔术发生的地方,这就是魔术的样子: HelloServer.py )

服务器插件通常包含一个或多个打包到 QgsServerFilter

每个 QgsServerFilter 实现以下一个或多个回调:

下面的示例实现了一个最小过滤器,该过滤器打印 HelloServer! 以防出现 SERVICE 参数等于“Hello”:

 1class HelloFilter(QgsServerFilter):
 2
 3    def __init__(self, serverIface):
 4        super().__init__(serverIface)
 5
 6    def onRequestReady(self) -> bool:
 7        QgsMessageLog.logMessage("HelloFilter.onRequestReady")
 8        return True
 9
10    def onSendResponse(self) -> bool:
11        QgsMessageLog.logMessage("HelloFilter.onSendResponse")
12        return True
13
14    def onResponseComplete(self) -> bool:
15        QgsMessageLog.logMessage("HelloFilter.onResponseComplete")
16        request = self.serverInterface().requestHandler()
17        params = request.parameterMap()
18        if params.get('SERVICE', '').upper() == 'HELLO':
19            request.clear()
20            request.setResponseHeader('Content-type', 'text/plain')
21            # Note that the content is of type "bytes"
22            request.appendBody(b'HelloServer!')
23        return True

筛选器必须注册到 serverIface 如下例所示:

class HelloServerServer:
    def __init__(self, serverIface):
        serverIface.registerFilter(HelloFilter(serverIface), 100)

的第二个参数 registerFilter() 设置优先级,该优先级定义具有相同名称的回调的顺序(首先调用优先级较低的)。

通过使用这三个回调,插件可以以多种不同的方式操作服务器的输入和/或输出。在任何时刻,插件实例都可以访问 QgsRequestHandler 通过 QgsServerInterface 。这个 QgsRequestHandler 类有很多方法可用于在进入服务器的核心处理之前更改输入参数(通过使用 requestReady() )或在核心服务处理完请求之后(通过使用 sendResponse() )。

以下示例涵盖了一些常见用例:

20.4.1.1.5.2. 修改输入

示例插件包含一个测试用例,它更改来自查询字符串的输入参数,在本例中,一个新参数被注入(已经解析) parameterMap ,然后该参数通过核心服务(WMS等)可见,在核心服务处理结束时,我们检查该参数是否仍然存在:

 1class ParamsFilter(QgsServerFilter):
 2
 3    def __init__(self, serverIface):
 4        super(ParamsFilter, self).__init__(serverIface)
 5
 6    def onRequestReady(self) -> bool:
 7        request = self.serverInterface().requestHandler()
 8        params = request.parameterMap( )
 9        request.setParameter('TEST_NEW_PARAM', 'ParamsFilter')
10        return True
11
12    def onResponseComplete(self) -> bool:
13        request = self.serverInterface().requestHandler()
14        params = request.parameterMap( )
15        if params.get('TEST_NEW_PARAM') == 'ParamsFilter':
16            QgsMessageLog.logMessage("SUCCESS - ParamsFilter.onResponseComplete")
17        else:
18            QgsMessageLog.logMessage("FAIL    - ParamsFilter.onResponseComplete")
19        return True

这是您在日志文件中看到的内容的摘录:

1 src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloServerServer - loading filter ParamsFilter
2 src/core/qgsmessagelog.cpp: 45: (logMessage) [1ms] 2014-12-12T12:39:29 Server[0] Server plugin HelloServer loaded!
3 src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 Server[0] Server python plugins loaded
4 src/mapserver/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [1ms] inserting pair SERVICE // HELLO into the parameter map
5 src/mapserver/qgsserverfilter.cpp: 42: (onRequestReady) [0ms] QgsServerFilter plugin default onRequestReady called
6 src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] SUCCESS - ParamsFilter.onResponseComplete

在突出显示的行上,“Success”字符串表示插件通过了测试。

The same technique can be exploited to use a custom service instead of a core one: you could for example skip a WFS SERVICE request or any other core request just by changing the SERVICE parameter to something different and the core service will be skipped. Then you can inject your custom results into the output and send them to the client (this is explained below).

小技巧

如果您真的想实现一个定制服务,建议子类 QgsService 并在上注册服务 registerFilter() 通过调用其 registerService(service)

20.4.1.1.5.3. 修改或替换输出

水印过滤器示例展示了如何将WMS输出替换为在WMS核心服务生成的WMS图像的顶部添加水印图像获得的新图像:

 1from qgis.server import *
 2from qgis.PyQt.QtCore import *
 3from qgis.PyQt.QtGui import *
 4
 5class WatermarkFilter(QgsServerFilter):
 6
 7    def __init__(self, serverIface):
 8        super().__init__(serverIface)
 9
10    def onResponseComplete(self) -> bool:
11        request = self.serverInterface().requestHandler()
12        params = request.parameterMap( )
13        # Do some checks
14        if (params.get('SERVICE').upper() == 'WMS' \
15                and params.get('REQUEST').upper() == 'GETMAP' \
16                and not request.exceptionRaised() ):
17            QgsMessageLog.logMessage("WatermarkFilter.onResponseComplete: image ready %s" % request.parameter("FORMAT"))
18            # Get the image
19            img = QImage()
20            img.loadFromData(request.body())
21            # Adds the watermark
22            watermark = QImage(os.path.join(os.path.dirname(__file__), 'media/watermark.png'))
23            p = QPainter(img)
24            p.drawImage(QRect( 20, 20, 40, 40), watermark)
25            p.end()
26            ba = QByteArray()
27            buffer = QBuffer(ba)
28            buffer.open(QIODevice.WriteOnly)
29            img.save(buffer, "PNG" if "png" in request.parameter("FORMAT") else "JPG")
30            # Set the body
31            request.clearBody()
32            request.appendBody(ba)
33        return True

In this example the SERVICE parameter value is checked and if the incoming request is a WMS GETMAP and no exceptions have been set by a previously executed plugin or by the core service (WMS in this case), the WMS generated image is retrieved from the output buffer and the watermark image is added. The final step is to clear the output buffer and replace it with the newly generated image. Please note that in a real-world situation we should also check for the requested image type instead of supporting PNG or JPG only.

20.4.1.2. 访问控制过滤器

访问控制筛选器为开发人员提供了对哪些层、功能和属性可以访问的细粒度控制,可以在访问控制筛选器中实现以下回调:

20.4.1.2.1. 插件文件

下面是我们的示例插件的目录结构:

1PYTHON_PLUGINS_PATH/
2  MyAccessControl/
3    __init__.py    --> *required*
4    AccessControl.py  --> *required*
5    metadata.txt   --> *required*
20.4.1.2.1.1. __init__.py

该文件是Python的导入系统所必需的。对于所有QGIS服务器插件,该文件包含一个 serverClassFactory() 函数,该函数在插件在启动时加载到QGIS服务器时调用。它接收对 QgsServerInterface 并且必须返回插件的类的一个实例。这就是示例插件 __init__.py 看起来像是:

def serverClassFactory(serverIface):
    from MyAccessControl.AccessControl import AccessControlServer
    return AccessControlServer(serverIface)
20.4.1.2.1.2. AccessControl.py
 1class AccessControlFilter(QgsAccessControlFilter):
 2
 3    def __init__(self, server_iface):
 4        super().__init__(server_iface)
 5
 6    def layerFilterExpression(self, layer):
 7        """ Return an additional expression filter """
 8        return super().layerFilterExpression(layer)
 9
10    def layerFilterSubsetString(self, layer):
11        """ Return an additional subset string (typically SQL) filter """
12        return super().layerFilterSubsetString(layer)
13
14    def layerPermissions(self, layer):
15        """ Return the layer rights """
16        return super().layerPermissions(layer)
17
18    def authorizedLayerAttributes(self, layer, attributes):
19        """ Return the authorised layer attributes """
20        return super().authorizedLayerAttributes(layer, attributes)
21
22    def allowToEdit(self, layer, feature):
23        """ Are we authorised to modify the following geometry """
24        return super().allowToEdit(layer, feature)
25
26    def cacheKey(self):
27        return super().cacheKey()
28
29class AccessControlServer:
30
31   def __init__(self, serverIface):
32      """ Register AccessControlFilter """
33      serverIface.registerAccessControl(AccessControlFilter(serverIface), 100)

此示例为每个人提供完全访问权限。

插件的作用是知道谁登录了。

在所有这些方法上,我们都有参数上的Layer,以便能够定制每一层的限制。

20.4.1.2.2. 层过滤器表达式

用于添加表达式以限制结果。

例如,要将其限制为属性 role 等于 user

def layerFilterExpression(self, layer):
    return "$role = 'user'"
20.4.1.2.3. LayerFilterSubset字符串

与前面的相同,但使用 SubsetString (在数据库中执行)

例如,要将其限制为属性 role 等于 user

def layerFilterSubsetString(self, layer):
    return "role = 'user'"
20.4.1.2.4. 层权限

限制对层的访问。

返回类型为 LayerPermissions() ,它具有以下属性:

  • canRead 去看它在 GetCapabilities 并拥有读取访问权限。

  • canInsert 以能够插入新特征。

  • canUpdate 才能更新要素。

  • canDelete 才能删除要素。

例如,要将所有内容限制为只读访问权限:

1def layerPermissions(self, layer):
2    rights = QgsAccessControlFilter.LayerPermissions()
3    rights.canRead = True
4    rights.canInsert = rights.canUpdate = rights.canDelete = False
5    return rights
20.4.1.2.5. 授权层属性

用于限制特定属性子集的可见性。

参数属性返回当前的一组可见属性。

例如,要隐藏 role 属性:

def authorizedLayerAttributes(self, layer, attributes):
    return [a for a in attributes if a != "role"]
20.4.1.2.6. 允许编辑

这用于限制对要素子集的编辑。

它用在 WFS-Transaction 协议。

例如,只能编辑具有该属性的要素 role 具有价值的 user

def allowToEdit(self, layer, feature):
    return feature.attribute('role') == 'user'
20.4.1.2.7. CacheKey

QGIS Server维护功能的缓存,然后您可以在此方法中返回角色,以使每个角色都有缓存。或返回 None 以完全禁用高速缓存。

20.4.2. 定制服务

在QGIS Server中,WMS、WFS和WCS等核心服务作为的子类实现 QgsService

实现一个新服务,当查询字符串参数 SERVICE 匹配服务名称,则可以实现您自己的 QgsService 并在上注册您的服务 serviceRegistry() 通过调用其 registerService(service)

以下是名为的自定义服务的示例 CUSTOM

 1from qgis.server import QgsService
 2from qgis.core import QgsMessageLog
 3
 4class CustomServiceService(QgsService):
 5
 6    def __init__(self):
 7        QgsService.__init__(self)
 8
 9    def name(self):
10        return "CUSTOM"
11
12    def version(self):
13        return "1.0.0"
14
15    def executeRequest(self, request, response, project):
16        response.setStatusCode(200)
17        QgsMessageLog.logMessage('Custom service executeRequest')
18        response.write("Custom service executeRequest")
19
20
21class CustomService():
22
23    def __init__(self, serverIface):
24        serverIface.serviceRegistry().registerService(CustomServiceService())

20.4.3. 自定义接口

在QGIS服务器中,OAPIF(又名WFS3)等核心OGC API被实现为 QgsServerOgcApiHandler 注册到的实例的子类 QgsServerOgcApi (或者它的父类 QgsServerApi )。

要实现当url路径与特定URL匹配时将执行的新API,您可以实现自己的 QgsServerOgcApiHandler 实例,将它们添加到 QgsServerOgcApi 并将该API注册到 serviceRegistry() 通过调用其 registerApi(api)

以下是URL包含以下内容时将执行的自定义API的示例 /customapi

 1import json
 2import os
 3
 4from qgis.PyQt.QtCore import QBuffer, QIODevice, QTextStream, QRegularExpression
 5from qgis.server import (
 6    QgsServiceRegistry,
 7    QgsService,
 8    QgsServerFilter,
 9    QgsServerOgcApi,
10    QgsServerQueryStringParameter,
11    QgsServerOgcApiHandler,
12)
13
14from qgis.core import (
15    QgsMessageLog,
16    QgsJsonExporter,
17    QgsCircle,
18    QgsFeature,
19    QgsPoint,
20    QgsGeometry,
21)
22
23
24class CustomApiHandler(QgsServerOgcApiHandler):
25
26    def __init__(self):
27        super(CustomApiHandler, self).__init__()
28        self.setContentTypes([QgsServerOgcApi.HTML, QgsServerOgcApi.JSON])
29
30    def path(self):
31        return QRegularExpression("/customapi")
32
33    def operationId(self):
34        return "CustomApiXYCircle"
35
36    def summary(self):
37        return "Creates a circle around a point"
38
39    def description(self):
40        return "Creates a circle around a point"
41
42    def linkTitle(self):
43        return "Custom Api XY Circle"
44
45    def linkType(self):
46        return QgsServerOgcApi.data
47
48    def handleRequest(self, context):
49        """Simple Circle"""
50
51        values = self.values(context)
52        x = values['x']
53        y = values['y']
54        r = values['r']
55        f = QgsFeature()
56        f.setAttributes([x, y, r])
57        f.setGeometry(QgsCircle(QgsPoint(x, y), r).toCircularString())
58        exporter = QgsJsonExporter()
59        self.write(json.loads(exporter.exportFeature(f)), context)
60
61    def templatePath(self, context):
62        # The template path is used to serve HTML content
63        return os.path.join(os.path.dirname(__file__), 'circle.html')
64
65    def parameters(self, context):
66        return [QgsServerQueryStringParameter('x', True, QgsServerQueryStringParameter.Type.Double, 'X coordinate'),
67                QgsServerQueryStringParameter(
68                    'y', True, QgsServerQueryStringParameter.Type.Double, 'Y coordinate'),
69                QgsServerQueryStringParameter('r', True, QgsServerQueryStringParameter.Type.Double, 'radius')]
70
71
72class CustomApi():
73
74    def __init__(self, serverIface):
75        api = QgsServerOgcApi(serverIface, '/customapi',
76                            'custom api', 'a custom api', '1.1')
77        handler = CustomApiHandler()
78        api.registerHandler(handler)
79        serverIface.serviceRegistry().registerApi(api)