Prefect 折騰手記:編寫一個簡易工作流程序

文章首發於個人公衆號:「阿拉平平」

在處理複雜工作時,將所有的邏輯都寫到一個任務中是一種很糟糕的做法。將其拆解成多個子任務,重新編排並監控運行狀況則要靠譜的多。也許你正在尋找一個好用的工作流引擎,那麼這款基於 Python 的工作流工具:Prefect[1] 說不定可以幫助到你。

在這篇文章中,我將介紹並演示 Prefect 的用法,編寫一個簡單的工作流程序來說明 Prefect 是如何使用的。文中使用的 Python 版本爲 3.6.5,Prefect 版本爲 0.13.19。

快速開始

安裝 Prefect 前請確保已安裝 Python,且版本在 3.6 以上。

安裝很簡單,執行以下命令:

pip install prefect

官方的示例代碼如下:

from prefect import task, Flow, Parameter


@task(log_stdout=True)
def say_hello(name):
    print("Hello, {}!".format(name))


with Flow("My First Flow") as flow:
    name = Parameter('name')
    say_hello(name)


flow.run(name='world') # "Hello, world!"
flow.run(name='Marvin') # "Hello, Marvin!"

我們運行看下輸出結果:

[2020-12-16 11:52:27+0800] INFO - prefect.FlowRunner | Beginning Flow run for 'My First Flow'
[2020-12-16 11:52:27+0800] INFO - prefect.TaskRunner | Task 'name': Starting task run...
[2020-12-16 11:52:27+0800] INFO - prefect.TaskRunner | Task 'name': Finished task run for task with final state: 'Success'
[2020-12-16 11:52:27+0800] INFO - prefect.TaskRunner | Task 'say_hello': Starting task run...
[2020-12-16 11:52:27+0800] INFO - prefect.TaskRunner | Hello, world!
[2020-12-16 11:52:27+0800] INFO - prefect.TaskRunner | Task 'say_hello': Finished task run for task with final state: 'Success'
[2020-12-16 11:52:27+0800] INFO - prefect.FlowRunner | Flow run SUCCESS: all reference tasks succeeded
[2020-12-16 11:52:27+0800] INFO - prefect.FlowRunner | Beginning Flow run for 'My First Flow'
[2020-12-16 11:52:27+0800] INFO - prefect.TaskRunner | Task 'name': Starting task run...
[2020-12-16 11:52:27+0800] INFO - prefect.TaskRunner | Task 'name': Finished task run for task with final state: 'Success'
[2020-12-16 11:52:27+0800] INFO - prefect.TaskRunner | Task 'say_hello': Starting task run...
[2020-12-16 11:52:27+0800] INFO - prefect.TaskRunner | Hello, Marvin!
[2020-12-16 11:52:27+0800] INFO - prefect.TaskRunner | Task 'say_hello': Finished task run for task with final state: 'Success'
[2020-12-16 11:52:27+0800] INFO - prefect.FlowRunner | Flow run SUCCESS: all reference tasks succeeded

可以看到,Prefect 很好地執行了任務,並輸出了運行的日誌。接下來,我們來嘗試用 Prefect 編寫一個簡易的工作流程序。

項目實踐

現在有這麼個需求:獲取 GitHub Trending 每日數據,並保存成 CSV 文件。這個要怎麼實現呢?

我們先拆解下需求,大致可以分爲以下步驟:

  1. download_data:調用接口,獲取 GitHub Trending 每日數據。
  2. handle_data:對數據進行處理,選取需要的字段。
  3. save_data:將處理好的數據保存成 CSV 文件。

功能實現

以上的每個步驟分別對應一個子任務,我們來實現下。

首先是 download_data。通過 requests 庫,這個不難實現,具體代碼如下:

import requests

GITHUB_TRENDING_URL = "https://trendings.herokuapp.com/repo"

def download_data():
    params = {'since': 'today'}
    trending_data = requests.get(GITHUB_TRENDING_URL, params).json()
    return trending_data

我將接口返回的數據轉成了 JSON 格式,數據如下:

{
    "count": 24,
    "msg": "suc",
    "items": [
        {
            "repo": "getmeli/meli",
            "repo_link": "[https://github.com/getmeli/meli](https://github.com/getmeli/meli)",
            "desc": "",
            "lang": "TypeScript",
            "stars": "951",
            "forks": "18",
            "added_stars": "291 stars today",
            "avatars": [
                "[https://avatars3.githubusercontent.com/u/32174276?s=40&v=4](https://avatars3.githubusercontent.com/u/32174276?s=40&v=4)",
                "[https://avatars3.githubusercontent.com/u/13135149?s=40&v=4](https://avatars3.githubusercontent.com/u/13135149?s=40&v=4)"
            ]
        },
... many more records  
    ]
}

接下來是實現 handle_data。由於只需要 items 中的內容,所以對其進行處理,代碼如下:

def handle_data(data):
    return [i for i in data["items"]]

最後是 save_data。選取 items 中的字段,保存到本地,代碼如下:

import csv

def save_data(rows):
    headers = ["repo", "repo_link", "stars", "forks", "added_stars"]
    with open("/tmp/trending.csv", "w", newline="") as f:
        f_csv = csv.DictWriter(f, headers, extrasaction='ignore')
        f_csv.writeheader()
        f_csv.writerows(rows)

工作流

子任務實現後,就可以用 Prefect 編寫工作流了,代碼片段如下:

from prefect import Flow

with Flow("GitHub_Trending_Flow") as flow:
    data = download_data()
    rows = handle_data(data)
    save_data(rows)

flow.run()

在 Prefect 中,Flow 用於描述任務之間的依賴關係,比如執行的先後順序或是數據的傳遞。創建了 Flow 後,就可以通過調用 flow.run() 來執行。

任務

Prefect 將每個步驟當作一項任務,對應代碼中的一個函數。但是,怎麼將函數聲明爲 Prefect 的任務呢?

最簡單的方法是使用裝飾器 @task,比如將 download_data 聲明爲任務:

from prefect import task
import requests

GITHUB_TRENDING_URL = "https://trendings.herokuapp.com/repo"

@task
def download_data():
    params = {'since': 'daily'}
    trending_data = requests.get(GITHUB_TRENDING_URL, params).json()
    return trending_data

參數

現在需求有變:需要將獲取的 GitHub Trending 數據從每日改成每週。考慮到之後時間還可能發生變化,所以我們將時間改爲參數。

導入 Parameter,並將 since 作爲參數傳入,具體代碼如下:

from prefect import task, Flow, Parameter
import requests

GITHUB_TRENDING_URL = "https://trendings.herokuapp.com/repo"

@task
def download_data(since):
    params = {'since': since}
    trending_data = requests.get(GITHUB_TRENDING_URL, params).json()
    return trending_data

with Flow("GitHub_Trending_Flow") as flow:
    since = Parameter("since")
    download_data(since)    

flow.run(since="weekly")

工作流編排

Prefect 提供了開源的 server 以及 UI 來編排工作流。但在使用前,請確保安裝了 docker 和 docker-compose。

如果是第一次啓動需要運行以下命令配置本地工作流:

prefect backend server

運行後會在 ~/.prefect 目錄下生成配置文件,之後運行以下命令啓動 server:

prefect server start

啓動 server 後,訪問 http://localhost:8080,如果 server 不安裝在本機,則需要修改 ip 地址。同時也要注意修改主頁 「PREFECT SERVER」中 GraphQL 的地址:

執行工作流任務至少需要運行一個 agent,可以在本機開啓,命令如下:

prefect agent local start

接下來需要創建項目,可以通過命令行創建項目:

prefect create project "GitHub_Trending"

項目創建後,加入以下代碼可以將工作流注冊到 server 中,這裏的 project_name 要和剛創建的項目名對應:

flow.register(project_name="GitHub_Trending")

運行代碼進行註冊,選好項目可以看到註冊成功的工作流。


接着試下從頁面運行工作流,不過別忘了指定參數的值:


運行過程中,我們可以看到每個任務執行所消耗的時間。而在 「SCHEMATIC」中,我們也可以很清晰地瞭解整個工作流任務的依賴關係。


寫在最後

文章中僅演示了 Prefect 的部分功能,事實上,Prefect 中還有許多高級的用法,大家有興趣的話,可以參考官方文檔[2],相信大家完全可以編寫功能更復雜的工作流程序。

對這個項目有興趣的小夥伴也可以讀下這篇文章[3],作者編寫了個統計疫情數據並上傳至 S3 的工作流程序,並在 GitHub 上開源了,很不錯的一篇文章。

最後附上示例的完整代碼:

#!/usr/bin/env python3

from prefect import task, Flow, Parameter
import requests
import csv

GITHUB_TRENDING_URL = "https://trendings.herokuapp.com/repo"

@task
def download_data(since):
    params = {'since': since}
    trending_data = requests.get(GITHUB_TRENDING_URL, params).json()
    return trending_data

@task
def handle_data(data):
    return [i for i in data["items"]]

@task
def save_data(rows):
    headers = ["repo", "repo_link", "stars", "forks", "added_stars"]
    with open("/tmp/trending.csv", "w", newline="") as f:
        f_csv = csv.DictWriter(f, headers, extrasaction='ignore')
        f_csv.writeheader()
        f_csv.writerows(rows)

with Flow("GitHub_Trending_Flow") as flow:
    since = Parameter("since")
    data = download_data(since)
    rows = handle_data(data)
    save_data(rows)

#flow.run(since="weekly")
flow.register(project_name="GitHub_Trending")

References

[1] Prefect:https://github.com/PrefectHQ/prefect
[2] 文檔:https://docs.prefect.io/core/
[3] 文章:https://makeitnew.io/prefect-a-modern-python-native-data-workflow-engine-7ece02ceb396

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章