2023-11-18 update: 三年后 IBM Cloud 砍了云函数业务,本文可以不用看了

最后来一些豆知识收尾。

9. 使用 Cloud Function 可能会遇到的问题

  1. 如果依赖里有需要编译二进制 .so 的 packages,务必在与 Cloud Function 容器相同的环境下安装到 virtualenv。 (否则会遇到诸如 ModuleNotFoundError 这种问题[1])
1
2
3
4
# ’python:3.7‘ 环境目前是:

os.uname() == ["Linux", "action", "4.4.0-197-generic", "#229-Ubuntu SMP Wed Nov 25 11:05:42 UTC 2020", "x86_64"]
sys.version == "3.7.9 (default, Sep 10 2020, 17:09:36) \n[GCC 8.3.0]"

针对 Python 版本问题,一个解决方法是:

  1. 安装 pyenv;

  2. 安装编译 Python 所需依赖[2];

  3. 安装对应版本的 Python(从 python.org 下载安装包太慢的话,自行下载放至 pyenv 的 cache 目录下即可);

  4. 安装 virtualenv(pyenv-virtualenv 插件生成的 virtualenv 没有 bin/active_this.py);

  5. 按照之前的步骤创建 virtualenv.

或者用 Docker 的解决方法,这里还有写默认环境有哪些 packages。

  1. 因为只需要 active_this.py,所以 virtualenv/bin/python* 的二进制软链接其实没用。 在 zip 打包时可以加 -y 参数,只打包软链接而不是二进制文件。(可减少 10MB+ 体积)

  2. Cloud Function 上传 zip 包最大限制为 50MB。(fyi, numpy 就超了)

  3. 通过公开 RESTful API 执行函数,出错时客户端得到的代码并不是 「激活标志」,需要到日志查看。

  4. 通过公开 RESTful API 执行函数,传给 maindict 里还有 __ow_body, __ow_query 等内容,详见这里

10. 写 Telegram Bot 时可能会遇到的问题

  1. 如果 Webhook 接口返回 HTTP 4xx 错误(main 有未处理异常)的话,Telegram 会一直重试;而 Cloud Function 是按调用次数计费的...

  2. 发送时间超过 48h 的消息是没法撤回的,尝试撤回的话 python-telegram-bot 会丢出异常。

  3. Telegram 的 Markdown/MarkdownV2 都不支持表格,HTML 标签也只支持给定的几个。 (可以试试只用 Pillow 把表格画成图片,matplotlib 什么的就不要想了)

from PIL import Image, ImageFont, ImageDraw
from collections import namedtuple
def position_tuple(*args):
Position = namedtuple('Position', ['top', 'right', 'bottom', 'left'])
if len(args) == 0:
return Position(0, 0, 0, 0)
elif len(args) == 1:
return Position(args[0], args[0], args[0], args[0])
elif len(args) == 2:
return Position(args[0], args[1], args[0], args[1])
elif len(args) == 3:
return Position(args[0], args[1], args[2], args[1])
else:
return Position(args[0], args[1], args[2], args[3])
def draw_table(table, header=[], font=ImageFont.load_default(), cell_pad=(20, 10), margin=(10, 10), align=None, colors={}, stock=False):
"""
Draw a table using only Pillow
table: an 2d list, must be str
header: turple or list, must be str
font: an ImageFont object
cell_pad: padding for cell, (top_bottom, left_right)
margin: margin for table, css-like shorthand
align: None or list, 'l'/'c'/'r' for left/center/right, length must be the max count of columns
colors: dict, as follows
stock: bool, set red/green font color for cells start with +/-
"""
_color = {
'bg': 'white',
'cell_bg': 'white',
'header_bg': 'gray',
'font': 'black',
'rowline': 'black',
'colline': 'black',
'red': 'red',
'green': 'green',
}
_color.update(colors)
_margin = position_tuple(*margin)
table = table.copy()
if header:
table.insert(0, header)
row_max_hei = [0] * len(table)
col_max_wid = [0] * len(max(table, key=len))
for i in range(len(table)):
for j in range(len(table[i])):
col_max_wid[j] = max(font.getsize(table[i][j])[0], col_max_wid[j])
row_max_hei[i] = max(font.getsize(table[i][j])[1], row_max_hei[i])
tab_width = sum(col_max_wid) + len(col_max_wid) * 2 * cell_pad[0]
tab_heigh = sum(row_max_hei) + len(row_max_hei) * 2 * cell_pad[1]
tab = Image.new('RGBA', (tab_width + _margin.left + _margin.right, tab_heigh + _margin.top + _margin.bottom), _color['bg'])
draw = ImageDraw.Draw(tab)
draw.rectangle([(_margin.left, _margin.top), (_margin.left + tab_width, _margin.top + tab_heigh)],
fill=_color['cell_bg'], width=0)
if header:
draw.rectangle([(_margin.left, _margin.top), (_margin.left + tab_width, _margin.top + row_max_hei[0] + cell_pad[1] * 2)],
fill=_color['header_bg'], width=0)
top = _margin.top
for row_h in row_max_hei:
draw.line([(_margin.left, top), (tab_width + _margin.left, top)], fill=_color['rowline'])
top += row_h + cell_pad[1] * 2
draw.line([(_margin.left, top), (tab_width + _margin.left, top)], fill=_color['rowline'])
left = _margin.left
for col_w in col_max_wid:
draw.line([(left, _margin.top), (left, tab_heigh +_margin.top)], fill=_color['colline'])
left += col_w + cell_pad[0] * 2
draw.line([(left, _margin.top), (left, tab_heigh + _margin.top)], fill=_color['colline'])
top, left = _margin.top + cell_pad[1], 0
for i in range(len(table)):
left = _margin.left + cell_pad[0]
for j in range(len(table[i])):
color = _color['font']
if stock:
if table[i][j].startswith('+'):
color = _color['red']
elif table[i][j].startswith('-'):
color = _color['green']
_left = left
if (align and align[j] == 'c') or (header and i == 0):
_left += (col_max_wid[j] - font.getsize(table[i][j])[0]) // 2
elif align and align[j] == 'r':
_left += col_max_wid[j] - font.getsize(table[i][j])[0]
draw.text((_left, top), table[i][j], font=font, fill=color)
left += col_max_wid[j] + cell_pad[0] * 2
top += row_max_hei[i] + cell_pad[1] * 2
return tab
view raw draw_table.py hosted with ❤ by GitHub

11. 调用其他 Function,再嫖个 NoSQL

调用其他 Function 其实也是用 HTTP API,一个示例:

1
2
3
4
5
6
7
8
9
10
11
APIHOST  = os.environ.get('__OW_API_HOST')
NAMESPACE = os.environ.get('__OW_NAMESPACE')
USER_PASS = os.environ.get('__OW_API_KEY').split(':')

def call_action(action, params = {}):
url = APIHOST + '/api/v1/namespaces/' + NAMESPACE + '/actions/' + action
response = requests.post(url, json=params, params={'blocking': 'true'}, auth=(USER_PASS[0], USER_PASS[1])).json()
if response['response']['success']:
return response['response']['result']
else:
raise Exception(str(response['response']['result']))

加上个 NoSQL,基本上已经可以解决大部分写 Bot 的需求了。

Cloudant 是基于 Apache CouchDB 的 NoSQL 数据库,用 RESTful API 访问,JSON 输出。免费层送 1GB 空间。

这是 CouchDB 的 API,Cloudant 因为要用 IBM Cloud 的认证,所以有所不同。

这是一个 Python 的 Client package.

12. 尾声

IBM Cloud 用了很多 Apache 的东西,Cloud Function 用的也是 Apache Openwhisk。

如果基础云服务都有通用的协议的话,可能才会真的会有全部「上云」的那天吧。


本文参考了:

[1] https://stackoverflow.com/questions/58698406/aws-lambda-python-so-module-modulenotfounderror-no-module-named-regex-rege

[2] https://github.com/pyenv/pyenv/issues/240