Python跨服务传递作用域的坑

背景 在一个古老的系统中,有这样一段代码: scope = dict(globals(), **locals()) exec( """ global_a = 123 def func_a(): print(global_a) """ , scope) exec("func_a()", scope) 第一段用户代码定义了函数,第二段用户代码执行函数(不要问为什么这么做,因为用户永远是正确的)。第一个代码段执行后,func_a 和 global_a 都会被加入作用域 scope,由于第二个代码段也使用同一个 scope,所以第二个代码段调用 func_a 是可以正确输出 123 的。 但是使用 exec 执行用户代码毕竟不优雅,也很危险,于是把 exec 函数封装在了一个 Python 沙箱环境中(简单理解就是另一个 Python 服务,将 code 和 scope 传给这个服务后,服务会在沙箱环境调用 exec(code,scope)执行代码),相当于每一次对 exec 调用都替换成了对沙箱服务的 RPC 请求。 于是代码变成了这个样子: scope = dict(globals(), **locals()) scope = call_sandbox( """ global_a = 123 def func_a(): print(global_a) """ , scope) call_sandbox("func_a()", scope) 作用域跨服务传递问题 由于多次 RPC 调用需要使用同一个作用域,所以沙箱服务返回了新的 scope,以保证下次调用时作用域不会丢失。但是执行代码会发现第二次 call_sandbox 调用时候,会返回错误: ...

十一月 6, 2021 · 2 分钟 · Zhiya

利用AWS Lambda和iOS捷径实现手机一键开小区门禁

我住的小区使用了一个叫守望领域的智能门禁系统,可以通过手机 App 开小区门禁和单元门,但是用 App 开门需要经过四五步:打开 App→ 进入开门界面 → 找到需要开的门 → 点击开门。 加上戴口罩时候解锁手机需要输入密码,导致整个流程非常耗时,经常需要站在小区门口和单元门口操作半天,有一段时间我甚至养成了携带实体门禁卡的习惯,实体门禁卡开门要快很多。 最近又开始忘带门禁卡,苦恼之余发现 iOS 在锁屏界面右划可以免解锁直接进入 spotlight 界面,这个界面可以添加捷径,如果能写一个捷径去调用守望领域 App 的 API 开门,就可以实现手机免解锁一键开门。 查找 API 首先需要通过 Charles 之类的软件查找 App 调用的 API,配置 Charles 查看 App 请求的方式不再赘述,Google 一下可以看到很多教程。直接看结果 Charles 的结果,可以看到 api.lookdoor.cn 是这个软件所请求的 API 域名。 打开软件发的请求非常多,经过操作和请求的对比可以看到,发送开门指令调用的 API 是:/func/hjapp/house/v1/pushOpenDoorBySn.json?equipmentId=xxxxxx 这个路径。 详细查看这个请求可以发现,equipmentId 指的就是小区门的 Id,接口使用 cookie 做认证,只要将 cookie 带上就可以模拟开门指令。 第一次尝试 打开 iOS 捷径 App,创建一个新捷径,App 调用 API 使用了 POST 请求,搜索 Get contents of 这个动作来实现发送 POST 请求。 ...

十月 19, 2021 · 2 分钟 · Zhiya

gRPC 跨进程使用引发的问题

问题描述 在 Python 项目中使用 gRPC 进行通信,跨进程使用时,会出现阻塞或报错的情况(根据 gRPC.io 的版本不同,现象不同)。下面代码展示了一个跨进程使用的 DEMO,主进程向 30001 端口上的 gRPC 服务器发送请求,子进程也向相同的服务器发送请求。 def send(): channel = grpc.insecure_channel('localhost:30001') stub = message_pb2_grpc.GreeterStub(channel) response = stub.SayHello(message_pb2.HelloRequest(name='you')) print(f"Greeter client received 1: " + response.message) def main(): channel = grpc.insecure_channel('localhost:30001') stub = message_pb2_grpc.GreeterStub(channel) response = stub.SayHello2(message_pb2.HelloRequest(name='you')) print("Greeter client received 2: " + response.message) p = multiprocessing.Process(target=send) p.start() p.join() if __name__ == '__main__': main() 使用 gRPC.io 1.28.1 的情况下,会发生报错,主进程可以正常收到服务器的返回,但是子进程报 Socket operation on non-socket。 raise _InactiveRpcError(state) grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with: status = StatusCode.UNAVAILABLE details = "Socket operation on non-socket" debug_error_string = "{"created":"@1587481625.192071231","description":"Error received from peer ipv6:[::1]:50051","file":"src/core/lib/surface/call.cc","file_line":1056,"grpc_message":"Socket operation on non-socket","grpc_status":14}" > 排查过程 根据代码,主进程和子进程分别创建了自己的 Channel,看上去逻辑没什么问题,没有什么思路,所以多尝试几种情况先测试一下吧。首先尝试了一下主进程和子进程请求不同的server,在 30001 和 30002 端口分别启动两个 gRPC Server,然后将客户端代码改为主进程请求 30001 端口,子进程请求 30002 端口,代码可以正常运行。测试到这里就更摸不着头脑了,代码明明写的是主进程子进程分别创建 Channel,现在的现象看上去像是在请求相同服务器的情况下,子进程复用了主进程的socket连接。gRPC 底层使用的是 HTTP2,而 HTTP2 使用了长连接,会不会是这个原因? ...

四月 23, 2020 · 3 分钟 · Zhiya

使用Pipfile代替reqirements.txt

很多语言都提供了环境隔离的支持,例如nodejs的node_module,golang的go mod,python也有virtualenv和pyvenv等机制。为了建立依赖快照,通常会用pip freeze > requirements.txt 命令生成一个requirements.txt文件,在一些场景下这种方式就可以满足需求,但是在复杂场景下requirements.txt就力不从心了。 requirements.txt appdirs==1.4.3 astroid==2.3.3 attrs==19.3.0 black==19.3b0 certifi==2019.11.28 chardet==3.0.4 click==7.1.1 et-xmlfile==1.0.1 Flask==1.1.1 gevent==1.4.0 greenlet==0.4.15 idna==2.9 isort==4.3.21 itsdangerous==1.1.0 jdcal==1.4.1 Jinja2==2.11.1 lazy-object-proxy==1.4.3 MarkupSafe==1.1.1 mccabe==0.6.1 numpy==1.18.2 openpyxl==3.0.3 pandas==1.0.3 pylint==2.4.4 python-dateutil==2.8.1 pytz==2019.3 requests==2.23.0 six==1.14.0 tinydb==3.15.2 toml==0.10.0 typed-ast==1.4.1 urllib3==1.25.8 Werkzeug==1.0.0 wrapt==1.11.2 requirements.txt文件中只记录了依赖的版本,所以如果遇到官方的pypi源下载速度慢,需要使用更快的国内镜像下载,通常只能使用pip install -i安装或者修改全局的pip.conf文件。 当某个项目使用确定的python版本,这个版本也并不能在requirements.txt中体现,只能通过readme或者文档来记录,并且需要在创建虚拟环境时手动调用正确的python版本。 项目需要使用flake8、pylint、black等代码优化工具时,这些依赖也会被pip freeze命令写入requirements.txt中,然而这些依赖是不需要出现在生产环境的。 Pipfile Pipenv的出现,一举解决了上面的问题,Pipenv是Kenneth Reitz在2017年1月发布的Python依赖管理工具,他所基于的Pipfile则用来替代requirements.txt。 [[source]] name = "pypi" url = "https://pypi.doubanio.com/simple" verify_ssl = false [dev-packages] isort = "*" black = "==19.3b0" pylint = "*" [packages] flask = "*" tinydb = "*" pandas = "*" requests = "*" gevent = "*" openpyxl = "*" [requires] python_version = "3.6" 好处1:记录内容更详细 相比于requirements.txt,Pipfile多了pip源的设置,可以针对不同项目使用不同环境。并且将依赖分为dev和默认环境,例如pylint、flake8、black等依赖,可以将他们放入dev依赖中。 ...

三月 31, 2020 · 1 分钟 · Zhiya

探究 Pandas 读取 Excel 文件报错问题

问题描述 使用 Pandas 的 read_excel 方法读取一个 16 万行的 Excel 文件报 AssertionError 错误: "/Users/XXX/excel_test/venv/lib/python3.7/site-packages/xlrd/xlsx.py", line 637, in do_row assert 0 <= self.rowx < X12_MAX_ROWS AssertionError 背后原理 Excel 文件有两种默认格式,在 Excel 2007 以前,使用扩展名为 .xls 格式的文件,这种文件格式是一种特定的二进制格式,最多支持 65,536 行(在 Excel 97 之前支持的最大行数是 16,384),256 列表格。从 Excel 2007 版开始,默认采用了基于 XML 的新的文件格式 .xlsx,支持的表格行数达到了 1,048,576,列数达到了 16,384。需要注意的是,将 .xlsx 格式的文件转换为 .xls 格式的文件时,65,536 行和 256 列之后的数据都会被丢弃。 版本 最大行数 最大列数 文件格式 Excel 97 之前 16,384 256 .xls Excel 97 到 Excel 2003 65,536 256 .xls Excel 2007 及以后版本 1,048,576 16,384 .xlsx Pandas 读取 Excel 文件的引擎是 xlrd,xlrd 在读取 Excel 文件时,xlrd/xlsx.py 文件的 637 行会对行号做断言,判断行号是否在 0 - 1,048,576(Excel支持的最大行数) 的范围内。这段代码是这样的: ...

八月 22, 2019 · 2 分钟 · Zhiya