[Python] 如何用Python提供資料介接層給後端演算法與前端UI (一)

  • 8007
  • 0
  • 2018-09-13

一樣是公司需求,因為AI Team都是使用Python開發演算法程式,所以要使用演算法就一定要可以執行Python,但是前端的UI使用的程式語言不可能都能完整與Python整合,所以有了兩種替代方案,第一種是架設一個Web service,而且就是使用Python來架設,如此就可以在Web Service這一層直接呼叫其他部門開發的Python程式;第二種是透過command line的方式(後述)。這一篇要先講的是如何運用第一種方式提供UI一個類似運算中心的feature。

筆者目前工作偏向系統整合,需要將不同部門的系統串接起來一般常用的是透過DB互傳資訊,或是透過FTP互拋檔案,但如果是沒有這些環境時,也可以透過Web Service來做溝通。現在比較多人使用的Web Service Protocol是RESTFUL,不過因為筆者這邊需要的是嚴格規範,所以還是採用了傳統的SOAP來做。

筆者在Python是使用spyne來架設Web Service,因為我需要SOAP(如果您要的是RESTFUL,也可以考慮Flask):
網址1 https://pypi.org/project/spyne/
網址2 http://spyne.io/

到上面的pypi網址下載.whl並且安裝完畢之後,可以先到spyne.io看一下介紹,看得出來spyne支援多種input與output的protocol,筆者接著會在Windows環境使用SOAP來玩玩看。首先,進入Anaconda提供的Spyder IDE,雖然Visual Studio 2015也提供了很方便的Python環境,但是有些Python套件只支援官方原生的解譯器,在debug時會出現編譯錯誤而無法啟動,另外是筆者公司目前只有VS 2015,其實不知道2017有沒有改善這一點,所以還是使用Spyder來撰寫Python。

首先看到官網的這一段程式:

class HelloWorldService(ServiceBase):
    @rpc(Unicode, Integer, _returns=Iterable(Unicode))
    def say_hello(ctx, name, times):
        for i in range(times):
            yield 'Hello, %s' % name

application = Application([HelloWorldService],
    tns='spyne.examples.hello',
    in_protocol=Soap11(validator='lxml'),
    out_protocol=Soap11()
)

if __name__ == '__main__':
    # You can use any Wsgi server. Here, we chose
    # Python's built-in wsgi server but you're not
    # supposed to use it in production.
    from wsgiref.simple_server import make_server

    wsgi_app = WsgiApplication(application)
    server = make_server('0.0.0.0', 8000, wsgi_app)
    server.serve_forever()

這是在首頁將input與output都設定為SOAP 1.1時出現的程式碼片段,需要注意的是,其實這段程式碼不完整,因為沒有將import的套件也列出來,所以應該要這樣才能執行:

from spyne.model.primitive import Unicode, Integer #提供spyne參數類型(內建)
from spyne.model.complex import Iterable #提供spyne參數類型(複雜)
from spyne.protocol.soap import Soap11 #提供SOAP協定
from spyne.server.wsgi import WsgiApplication #建立Application
from wsgiref.simple_server import make_server #建立Server

class HelloWorldService(ServiceBase):
    @rpc(Unicode, Integer, _returns=Iterable(Unicode))
    def say_hello(ctx, name, times):
        for i in range(times):
            yield 'Hello, %s' % name

application = Application([HelloWorldService],
    tns='spyne.examples.hello',
    in_protocol=Soap11(validator='lxml'),
    out_protocol=Soap11()
)

if __name__ == '__main__':
    from wsgiref.simple_server import make_server

    wsgi_app = WsgiApplication(application)
    server = make_server('0.0.0.0', 8000, wsgi_app)
    server.serve_forever()

需要特別注意的一點是,因為這樣架設出來的Web Service是Single-thread,所以任何Request都要排隊,一個處理完之後才能處理另外一個,官網的範例程式也特別註明了這一點。因此,如果要架在production,則必須將Web Service這一段包裝過,然後Link給Apache之類的網站伺服軟體:
    # You can use any Wsgi server. Here, we chose
    # Python's built-in wsgi server but you're not
    # supposed to use it in production.
如果需要Multi-thread的話,也可以參考文末的寫法,跟這邊講的內容只差在兩行程式而已。雖然Google上面只能查到spyne套件原作者的實作範例,但是根據實際測試下來是可以用的。

再來要解釋一下官網程式碼各段落的用途:

class HelloWorldService(ServiceBase):
    @rpc(Unicode, Integer, _returns=Iterable(Unicode))
    def say_hello(ctx, name, times):
        for i in range(times):
            yield 'Hello, %s' % name

上面這一段是定義我們的Service裡面有哪些方法,只要是在def上方多加了一個Decorator(修飾器):@rpc的那些function,都會被放置到SOAP的WSDL文件中,成為一個可呼叫的方法。@rpc裡面定義的是輸入的參數類型,必須在import那邊加進來才能在這邊指定,而且@rpc的參數個數要等於say_hello()的參數個數減1。say_hello()的第一個參數是ctx,它是context的意思,用來對應@rpc的context;另外有一個@srpc,如果def的Decorator設定成@srpc,則不需要在say_hello()裡面加入ctx這個參數,也就是@srpc的參數個數就等於say_hello()的參數個數了。

application = Application([HelloWorldService],
    tns='spyne.examples.hello',
    in_protocol=Soap11(validator='lxml'),
    out_protocol=Soap11()
)

上面這一段則是定義Application對應的Service是誰,在這邊是將它定義為[HelloWorldService]這個class。另外就是tns要叫做什麼,還有input以及output的protocol各為何。

if __name__ == '__main__':
    from wsgiref.simple_server import make_server

    wsgi_app = WsgiApplication(application)
    server = make_server('0.0.0.0', 8000, wsgi_app)
    server.serve_forever()

最後這一段是,如果這支Python是main程式,也就是進入點的意思,則即時import simple_server,並且將Application包裝進WSGI規範、建立Server,然後serve Server。其中的'0.0.0.0'是該機器的IP,8000是要listen哪一個port的意思。

到這邊之後,就可以用Visual Studio來加入服務參考,或是要再用Python程式來測試也可以:

from suds.client import Client as SudsClient

url = 'http://0.0.0.0:8000/?wsdl'
client = SudsClient(url=url, timeout=15, cache=None)
return1 = client.service.say_hello('Albert', 3)

應該就可以得到跟官網一樣的結果:

<?xml version='1.0' encoding='utf-8'?>
<senv:Envelope xmlns:tns="spyne.examples.hello"
      xmlns:senv="http://schemas.xmlsoap.org/soap/envelope/">
  <senv:Body>
    <tns:say_helloResponse>
      <tns:say_helloResult>
        <tns:string>Hello, Albert</tns:string>
        <tns:string>Hello, Albert</tns:string>
        <tns:string>Hello, Albert</tns:string>
      </tns:say_helloResult>
    </tns:say_helloResponse>
  </senv:Body>
</senv:Envelope>

到這邊是基礎架構,然後我們就可以將say_hello()裡面改寫成呼叫其他部門的Python演算法程式,得到的回傳結果大部分都會是DataFrame,所以要再轉成JSON格式的字串往前端傳:

@rpc(Unicode, Unicode, Unicode, _returns=Iterable(Unicode))
def GetWaferMap(context, uploadedDirPath, uploadedFilePath, lotID):
    import decode_kuang as kuang
    
    waferDF = kuang.Decode(uploadedFilePath)
    return waferDF.to_json(orient='records')

這樣做之後就可以在前端UI與後端Python演算法之間做一個簡單的整合,做完之後看起來也沒什麼大不了的,只是留個紀錄以免日後忘記怎麼做。

[Updated]
經過一陣子的嘗試,最終找到簡便的方式可以把spyne變成是多執行緒。在spyne的2.12.1版本之後,可以在spyne.util.cherry裡面找到一個方法叫做cherry_graft_and_start(),這個方法是利用CherryPy套件來架設WSGI Server,所以我們必須要先安裝CherryPy才能正常呼叫這個方法。

原作者的cherry.py檔案:
https://github.com/arskom/spyne/blob/master/spyne/util/cherry.py
原作者的使用範例:
https://github.com/arskom/spyne/blob/master/examples/cherry/wsgi.py

在cherry.py那個GitHub的頁面中可以看到,原作者是在2.12.1-beta版本將cherry.py放到util裡面的,所以在這邊建議使用2.12.1之後的版本。然後再看到同一頁的內容中有標註一行comment:# Source: https://www.digitalocean.com/community/tutorials/how-to-deploy-python-wsgi-applications-using-a-cherrypy-web-server-behind-nginx,表示是根據這一篇文章來做這支cherry.py檔案的,進文章之後可以看到一個大大的進版畫面:
所以我們可以透過CherryPy架站來伺服WSGI Application,而它背後用的是Nginx的意思。cherry.py的重點在於,它裡面使用了CherryPy的thread_pool,也就是這一行(預設是30個thread
),也因此我們可以透過調整num_threads這個參數來達成Multi-thread的效果:

server.thread_pool = num_threads

然後再來看到原作者的使用範例,這是實際建立一個WSGI Application,並且透過cherry_graft_and_start()方法來提供Http api的例子。雖然原作者的例子的protocol是使用soft + json,不過我們上面的例子用到的SOAP還是適用的,也就是我們原本程式中的這兩行只要改為一行就可以換成用CherryPy來伺服,不過最上面要記得加上import:

from spyne.util.cherry import cherry_graft_and_start

#server = make_server('0.0.0.0', 8000, wsgi_app)
#server.serve_forever()

#這邊可以用num_threads直接調整thread數量
sys.exit(cherry_graft_and_start(wsgi_app, host='0.0.0.0', port=8000))

Python Version:
3.6.6

Python Packages:
spyne (2.12.14)
suds-jurko (0.6)
CherryPy (18.0.1)

Reference:
語言技術:Python Gossip