開發函數計算的正確姿勢 —— 爬蟲
這一次,我們拿一個簡單的場景來舉例子——開發一個簡單的爬蟲函數(代碼參考函數計算控制臺模板),介紹如何以正確姿勢,從零開始,開發一個自動伸縮、按調用次數收費的 serverless 爬蟲應用。
開發步驟
我們將這個完整的應用拆分成多步,並且在每一步完成後,我們都會進行相應的運行驗證。
- 創建 Fun 項目
首先,我們創建一個名為 image-crawler 的目錄作為項目的根。然後在該目錄下創建一個名為 template.yml 的文件,內容為:
ROSTemplateFormatVersion: ‘2015-09-01‘
Transform: ‘Aliyun::Serverless-2018-04-03‘
Resources:
localdemo:
Type: ‘Aliyun::Serverless::Service‘
Properties:
Description: ‘local invoke demo‘
image-crawler:
Type: ‘Aliyun::Serverless::Function‘
Properties:
Handler: index.handler
CodeUri: code/
Description: ‘Hello world with python2.7!‘
如果不了解 Fun 定義的 Serverless Application Model,可以參考 這裏。
操作完成後,我們的項目目錄結構如下:
.
└── template.yml
- 編寫 helloworld 函數代碼
在根目錄下創建一個名為 code 的目錄,並在該目錄下創建一個名為 index.py 的文件,內容為一個簡單的 helloworld 函數:
def handler(event, context):
return ‘hello world!‘
在項目根目錄下執行:
fun local invoke image-crawler
函數運行成功:
操作完成後,我們的項目目錄結構如下:
.
├── code
│ └── index.py
└── template.yml
- 事件觸發函數運行
我們簡單修改第 2 步的代碼,將 event 打印到 log 中。
import logging
logger = logging.getLogger()
def handler(event, context):
logger.info("event: " + event)
return ‘hello world!‘
通過觸發事件的方式運行函數,得到如下結果:
可以看到,我們的函數已經能正確接收到觸發事件了。
Fun Local 更多幫助信息,參考。
- 獲取網頁源碼內容
接下來,我們添加獲取網頁內容的代碼。
import logging
import json
import urllib
logger = logging.getLogger()
def handler(event, context):
logger.info("event: " + event)
evt = json.loads(event)
url = evt[‘url‘]
html = get_html(url)
logger.info("html content length: " + str(len(html)))
return ‘Done!‘
def get_html(url):
page = urllib.urlopen(url)
html = page.read()
return html
代碼邏輯比較簡單,我們這裏直接使用了 urllib 庫,讀取網頁內容。
運行函數,得到以下輸出:
- 解析網頁中的圖片
我們打算通過正則解析網頁中包含的 jpg 圖片,因此這一步會比較繁瑣,因為涉及到正則表達式的微調。為了能快速的解決問題,我們決定利用 fun local 提供的 local debugging 解決問題。local debugging 方法參考: 《函數計算本地運行與調試 - Fun Local 基本用法》。
首先,我們在下面這一行下個斷點:
logger.info("html content length: " + str(len(html)))
然後以 debug 的方式啟動,vscode 調試器連接後,函數會繼續運行到我們斷點的這一行:
我們可以直接在 Locals 一欄看到本地變量,其中包含了 html 這個變量,也就是我們獲取到的 html 源碼。我們可以將它的值復制出來,分析下,然後設計正則表達式。
我們可以先寫一個簡單的,比如可以是 http:\/\/[^\s,"]*.jpg。
怎麽快速校驗這段代碼的正確性呢?我們可以利用調試器提供的 Watch(監視) 功能。
創建一個 Watch 變量,將下面的值輸入進去:
re.findall(re.compile(r‘http:\/\/[^\s,"]*.jpg‘), html)
回車後,即可看到代碼的執行效果:
這裏一般不太容易一次寫對,可以反復修改正則測試,直到正確為止。
我們得到的正確的圖片解析的邏輯添加到代碼中:
reg = r‘http:\/\/[^\s,"]*.jpg‘
imgre = re.compile(reg)
def get_img(html):
return re.findall(imgre, html)
然後在 handler 方法中調用即可:
def handler(event, context):
logger.info("event: " + event)
evt = json.loads(event)
url = evt[‘url‘]
html = get_html(url)
img_list = get_img(html)
logger.info(img_list)
return ‘Done!‘
編寫完成後,可以繼續本地執行,驗證下結果:
echo ‘{"url": "https://image.baidu.com/search/index?tn=baiduimage&word=%E5%A3%81%E7%BA%B8"}‘ \
| fun local invoke image-crawler
可以看到,img_list 已經輸出到控制臺了:
- 將圖片上傳到 oss
解析到的圖片,我們選擇使用 oss 存儲。
首先,我們需要通過環境變量配置 OSS Endpoint 以及 OSS Bucket。
在 template 中配置環境變量(需提前創建好 oss bucket):
EnvironmentVariables:
OSSEndpoint: oss-cn-hangzhou.aliyuncs.com
BucketName: fun-local-test
然後就可以直接在函數中獲取到這兩個環境變量了:
endpoint = os.environ[‘OSSEndpoint‘]
bucket_name = os.environ[‘BucketName‘]
另外,fun local 運行函數時,還會提供一個額外的變量用來標識這是一個本地運行的函數。通過這個標識,我們可以用來做一些本地化的操作,比如我們可以在線上運行時連接 RDS,在本地運行時連接 Mysql。
這裏,我們用該標識以不同的的方式創建 oss client,原因是線上運行時,通過 credentials 獲取到的是扮演角色的臨時 ak,有有效期限制,而本地運行時,沒有該限制。oss 提供了這兩種方式的構造方法,我們直接使用即可:
creds = context.credentials
if (local):
auth = oss2.Auth(creds.access_key_id,
creds.access_key_secret)
else:
auth = oss2.StsAuth(creds.access_key_id,
creds.access_key_secret,
creds.security_token)
bucket = oss2.Bucket(auth, endpoint, bucket_name)
接著我們遍歷所有圖片,將所有的圖片上傳到 oss:
count = 0
for item in img_list:
count += 1
logging.info(item)
Get each picture
pic = urllib.urlopen(item)
# Store all the pictures in oss bucket, keyed by timestamp in microsecond unit
bucket.put_object(str(datetime.datetime.now().microsecond) + ‘.png‘, pic)
再在本地運行一下函數:
echo ‘{"url": "https://image.baidu.com/search/index?tn=baiduimage&word=%E5%A3%81%E7%BA%B8"}‘ \
| fun local invoke image-crawler
可以從日誌看到,圖片被一張一張的解析出來,並被上傳到 oss 上了。
登陸 oss 控制臺,可以看到這些圖片。
部署
本地開發完成後,我們還需要將其發布到線上,讓其成為一個可被調用的服務。以往,你可能覺得比較麻煩,比如要登陸控制臺,創建服務、創建函數、配置環境變量,創建角色等,現在有了 fun 後,這一切都不需要了。
不過,本地與線上還是有些區別的,那就是要授權函數計算能夠訪問 OSS,怎麽做呢?很簡單,在我們的 template 中加入一行配置即可(Polices 文檔,可以參考):
Policies: AliyunOSSFullAccess
添加後的 template.yml 內容如下:
ROSTemplateFormatVersion: ‘2015-09-01‘
Transform: ‘Aliyun::Serverless-2018-04-03‘
Resources:
localdemo:
Type: ‘Aliyun::Serverless::Service‘
Properties:
Description: ‘local invoke demo‘
Policies: AliyunOSSFullAccess
image-crawler:
Type: ‘Aliyun::Serverless::Function‘
Properties:
Handler: index.handler
CodeUri: code/
Description: ‘Hello world with python2.7!‘
Runtime: python2.7
EnvironmentVariables:
OSSEndpoint: oss-cn-hangzhou.aliyuncs.com
BucketName: fun-local-test
然後,使用 fun deploy 後,可以看到部署成功的日誌。
驗證
通過控制臺驗證
登陸控制臺,可以看到,我們的服務、函數、代碼、環境變量等都已經就緒了。
在觸發事件中,寫入我們用來測試的 json,然後執行:
可以發現,會獲得與本地一致的效果:
通過 fcli 驗證
fcli 幫助文檔 參考。
在終端執行以下命令,可以獲取函數列表:
fcli function list --service-name localdemo
可以看到我們的 image-crawler 已經創建成功了。
{
"Functions": [
"image-crawler",
"java8",
"nodejs6",
"nodejs8",
"php72",
"python27",
"python3"
],
"NextToken": null
}
使用以下命令則可以調用函數運行:
fcli function invoke --service-name localdemo \
--function-name image-crawler \
--event-str ‘{"url": "https://image.baidu.com/search/index?tn=baiduimage&word=%E5%A3%81%E7%BA%B8"}‘
運行成功後,會得到與控制臺與 fun local 一致的結果。
小結
至此,我們的開發就算告一段落。
本文利用 fun local 提供的本地運行、調試的能力,做到了在本地開發函數,並且通過反復的執行函數得到反饋以便於快速叠代代碼。
在本地開發完成後,不需要對代碼進行任何修改,通過 fun deploy 命令,一鍵部署到雲端,達到預期的效果。
本文介紹的方法,並不是開發函數計算的唯一方式。本文的目的,是能夠向開發者傳達一種信號——開發函數計算時,只要身姿正確,就會非常享受,開發流程也會十分順暢。祝您使用愉快。
開發函數計算的正確姿勢 —— 爬蟲