PAYATHON 2026

AWS Lambda 与 API Gateway 创建 API 时初始化失败

技术老齐

结论

module initialization error 出现在 Lambda 加载 Python 模块时,不是 API Gateway 的响应映射错误。代码中最直接的问题是模块末尾的这行调用:

CurrentPortfolioValue()

CurrentPortfolioValue 要求传入两个参数:

def CurrentPortfolioValue(event, context):

模块导入时调用它却没有传参,因此 Python 会立即抛出:

TypeError: CurrentPortfolioValue() missing 2 required positional arguments: 'event' and 'context'

此时 Lambda 还没有正式调用 handler,模块初始化就已经失败。删除这行调用,并把 Lambda handler 配置为:

ShareScraper.CurrentPortfolioValue

同时确保 ShareScraper.py 位于 ZIP 包的根目录。

当前实现还混用了两套运行方式:

  • Flask/Connexion 是 WSGI Web 应用。
  • Lambda handler 是 handler(event, context) 形式的函数。

API Gateway 无法直接把请求交给普通的 Flask/Connexion 应用,除非中间加入 WSGI/Lambda 适配层。最简单的处理方式是暂时移除 Flask 和 Connexion,由 API Gateway 直接调用 Lambda。

推荐实现:API Gateway 直接调用 Lambda

可以将 ShareScraper.py 调整为下面的结构:

import json
from datetime import date

import requests
from bs4 import BeautifulSoup


def CurrentPortfolioValue(event, context):
    initial_portfolio_price = 100.0
    initial_date = date(2018, 8, 19)
    current_value = initial_portfolio_price

    headers = {
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) "
            "Gecko/20100101 Firefox/50.0"
        )
    }

    try:
        vanguard_response = requests.get(
            "https://www.bloomberg.com/quote/VAN1579:AU",
            headers=headers,
            timeout=10,
        )
        vanguard_response.raise_for_status()

        soup = BeautifulSoup(vanguard_response.content, "html.parser")
        vanguard_element = soup.find("span", class_="priceText__1853e8a5")

        if vanguard_element is None:
            raise ValueError("Vanguard price element was not found")

        vanguard_price = parse_price(vanguard_element.get_text())

        ampnz_response = requests.get(
            "https://www.ampcapital.com/nz/en/investments/funds/"
            "index-funds/nz-shares-index-fund",
            headers=headers,
            timeout=10,
        )
        ampnz_response.raise_for_status()

        soup = BeautifulSoup(ampnz_response.content, "html.parser")
        ampnz_elements = soup.find_all(
            "div",
            class_="ht-module_title ht-highlight_color",
        )

        if len(ampnz_elements) < 2:
            raise ValueError("AMP NZ price element was not found")

        ampnzshares_price = parse_price(ampnz_elements[1].get_text())

        days_difference = (date.today() - initial_date).days
        count = days_difference

        while count > 14:
            current_value += (
                initial_portfolio_price
                + 50.0 * vanguard_price
                + 50.0 * ampnzshares_price
            )
            count -= 14

        return {
            "statusCode": 200,
            "headers": {
                "Content-Type": "application/json"
            },
            "body": json.dumps(
                {"portfolio_value": current_value}
            ),
        }

    except Exception as exc:
        print(f"Failed to calculate portfolio value: {exc}")

        return {
            "statusCode": 500,
            "headers": {
                "Content-Type": "application/json"
            },
            "body": json.dumps(
                {"error": "Unable to calculate portfolio value"}
            ),
        }


def parse_price(value):
    cleaned = (
        value.strip()
        .replace(",", "")
        .replace("$", "")
        .replace("AUD", "")
        .replace("NZD", "")
    )
    return float(cleaned)

不要在文件末尾主动调用 handler:

# 不要这样做
CurrentPortfolioValue()

Lambda 服务会自动传入 event 和 context。

如果需要在本地手动测试,可以显式传入模拟值,并将测试代码放在保护条件中:

if __name__ == "__main__":
    print(CurrentPortfolioValue({}, None))

这个代码块只会在直接运行文件时执行。Lambda 导入模块时不会执行它。

Lambda handler 应如何配置

假设 ZIP 包中的文件结构如下:

ShareScraper.py
requests/
bs4/
soupsieve/
urllib3/
certifi/
charset_normalizer/
idna/

Lambda 的 Handler 配置应为:

ShareScraper.CurrentPortfolioValue

它的格式是:

Python文件名.函数名

不要包含 .py。还要注意,Linux 文件系统区分大小写。例如:

sharescraper.CurrentPortfolioValue

与:

ShareScraper.CurrentPortfolioValue

指向的不是同一个模块名。

ZIP 解压后,根目录必须直接包含 ShareScraper.py。下面的结构不正确:

deployment.zip
└── my_project/
    └── ShareScraper.py

除非同时调整 handler 和 Python 导入路径。通常应按以下结构打包:

deployment.zip
├── ShareScraper.py
├── requests/
├── bs4/
└── ...

正确打包依赖

并非所有 Lambda Python 运行时都保证提供 requests 和 beautifulsoup4,因此应将它们一并放入部署包。不要只复制项目源代码,也不要只复制本机虚拟环境的外层目录。

常见做法是先把依赖安装到部署目录:

mkdir package
python -m pip install \
  --target package \
  requests beautifulsoup4
cp ShareScraper.py package/
cd package
zip -r ../deployment.zip .

打包时需要注意:

  • 使用的 Python 主次版本应尽量与 Lambda runtime 一致。
  • 如果依赖包含 C 扩展,必须针对 Lambda 的 Linux 环境和对应 CPU 架构构建,不能直接上传 macOS 或 Windows 的二进制包。
  • requests 和默认的 html.parser 通常不涉及复杂的本地编译。如果改用 lxml,则要特别留意平台兼容性。
  • 也可以使用 Lambda Layer、容器镜像或 CI 中的 Linux 构建环境管理依赖。

Flask/Connexion 与 Lambda 的关系

当前的 operationId:

operationId: "ShareScraper.CurrentPortfolioValue"

属于 Connexion 的路由机制。Connexion 会根据 HTTP 请求和 OpenAPI 定义调用 Python 函数,不会像 Lambda 那样自动传入 event 和 context。

因此,下面的函数签名适合作为 Lambda handler,但通常不适合作为 Connexion 的 operation function:

def CurrentPortfolioValue(event, context):

这里应从两种方案中选择一种。

方案一:直接使用 Lambda

这是当前场景中最简单的方案:

API Gateway
    → Lambda proxy integration
        → ShareScraper.CurrentPortfolioValue(event, context)

这种情况下不需要 Flask、Connexion 或 Connexion YAML 文件。只需在 API Gateway 中配置 GET /value,使用 Lambda proxy integration,然后部署 stage。

方案二:保留 Flask/Connexion

如果必须保留完整的 Flask/Connexion 应用,需要:

  1. 创建并导出 Flask/Connexion WSGI application。
  2. 使用能够处理 API Gateway/Lambda 事件的 WSGI 适配层或 Lambda Web Adapter。
  3. 将 Lambda handler 指向适配器导出的 handler,而不是普通的 Connexion operation function。
  4. 确认所选方案支持当前 API Gateway 类型、payload format 和 Python runtime。

整体调用关系如下:

API Gateway event
    → Lambda handler
        → WSGI adapter
            → Flask/Connexion application
                → operation function

不能把本地 Flask 开发服务器直接上传到 Lambda,然后期待 Lambda 自动启动并监听端口。

YAML 中的问题

如果展示的 YAML 缩进与实际文件一致,那么它不符合正确的 OpenAPI 路径结构。至少应写成:

swagger: "2.0"

info:
  description: This is the swagger file that goes with our server code
  version: "1.0.0"
  title: Swagger REST Article

consumes:
  - application/json

produces:
  - application/json

basePath: /api

paths:
  /value:
    get:
      operationId: ShareScraper.CurrentPortfolioValue
      tags:
        - Portfolio
      summary: Read the current portfolio value
      description: Read the current portfolio value
      responses:
        "200":
          description: Successful response
          schema:
            type: object
            properties:
              portfolio_value:
                type: number
                format: double

原定义同时使用了:

type: "string"
items:

items 用于数组 schema,不应放在普通字符串 schema 下。实际返回 JSON 对象时,也不应将类型声明为 string。

如果选择让 API Gateway 直接调用 Lambda,这份 Connexion YAML 不会参与 Lambda 模块初始化。YAML 错误可能导致 Connexion 启动或路由失败,但无法解释代码末尾无参调用 Lambda handler 引起的初始化异常。

如何排查 module initialization error

API Gateway 中显示的错误通常只是 Lambda 错误的外层表现。真正的 Python traceback 需要到 Lambda 的 CloudWatch Logs 中查看,重点搜索:

TypeError
Runtime.ImportModuleError
ModuleNotFoundError
SyntaxError
IndentationError
Unable to import module

可以按以下顺序排查:

  1. 先在 Lambda 控制台直接测试函数,暂时绕过 API Gateway。
  2. 查看该次调用对应的 CloudWatch 日志流。
  3. 确认已经删除模块级的 CurrentPortfolioValue() 调用。
  4. 确认 handler 配置为 ShareScraper.CurrentPortfolioValue。
  5. 确认 ShareScraper.py 位于 ZIP 根目录。
  6. 确认 requests、bs4 及其传递依赖都已打包。
  7. 检查源代码缩进。
  8. 等 Lambda 可以单独调用成功后,再检查 API Gateway integration 和 stage 部署。

API Gateway 的 mapping template 或 response transformation 只能处理 Lambda 已经返回的结果。如果 Lambda 在导入阶段就失败,修改 API Gateway 的响应映射无法解决问题。

初始化成功后还可能遇到的问题

修复模块初始化错误后,抓取逻辑仍可能遇到以下运行期问题:

  • 外部网站更改 HTML class 后,find_all(...)[0] 或 [1] 可能抛出 IndexError。
  • 网站可能返回验证码、403、429 或不同地区的页面。
  • 价格可能包含货币符号、逗号或空白,不能直接传给 float()。
  • 如果 requests.get() 没有设置超时时间,Lambda 可能一直等待,直到函数超时。
  • 如果 Lambda 位于没有 NAT 出口的 VPC 私有子网,它将无法访问这些公网网站。
  • Lambda timeout 需要覆盖多次外部 HTTP 请求的耗时,但不能只靠增加 timeout 掩盖网络或页面解析问题。
  • 抓取目标网站前,还应确认其使用条款、robots 规则和数据授权要求。

当前应先删除模块末尾的无参调用,并正确配置 handler,然后在 Lambda 控制台中单独测试。等 Lambda 能稳定返回标准 proxy response 后,再处理 API Gateway、Connexion 或 OpenAPI 配置。

备注:内容仅供参考。