使用 Python 进行后台处理

许多应用都需要在网络请求的具体情境之外进行后台处理。本教程创建了一个 Web 应用,让用户输入要翻译的文本,然后显示之前的翻译的列表。翻译在后台进程中完成,避免阻止用户的请求。

下图说明了翻译请求过程。

架构图。

教程应用的工作原理的事件序列如下:

  1. 访问网页,查看存储在 Firestore 中的之前的翻译的列表。
  2. 通过输入 HTML 表单请求翻译文本。
  3. 翻译请求被发布到 Pub/Sub。
  4. 触发已订阅该 Pub/Sub 主题的 Cloud Functions 函数。
  5. Cloud Functions 函数使用 Cloud Translation 来翻译文本。
  6. Cloud Functions 函数将结果存储在 Firestore 中。

本教程适用于有兴趣了解如何使用 Google Cloud 进行后台处理的用户。无需任何有关 Pub/Sub、Firestore、App Engine 或 Cloud Functions 的先前经验。不过,具有一定程度的 Python、JavaScript 和 HTML 相关经验会有助于您了解所有代码。

目标

  • 了解并部署 Cloud Functions 函数。
  • 了解并部署 App Engine 应用。
  • 试用该应用。

费用

本教程使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用量来估算费用。 Google Cloud 新用户可能有资格申请免费试用

完成本教程后,您可以删除所创建的资源以避免继续计费。如需了解详情,请参阅清理

准备工作

  1. 登录您的 Google Cloud 帐号。如果您是 Google Cloud 新手,请创建一个帐号来评估我们的产品在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。
  2. 在 Google Cloud Console 的项目选择器页面上,选择或创建一个 Google Cloud 项目。

    转到“项目选择器”

  3. 确保您的 Cloud 项目已启用结算功能。 了解如何确认您的项目是否已启用结算功能

  4. 启用 Firestore, Cloud Functions, Pub/Sub, and Cloud Translation API。

    启用 API

  5. 在 Google Cloud Console 的项目选择器页面上,选择或创建一个 Google Cloud 项目。

    转到“项目选择器”

  6. 确保您的 Cloud 项目已启用结算功能。 了解如何确认您的项目是否已启用结算功能

  7. 启用 Firestore, Cloud Functions, Pub/Sub, and Cloud Translation API。

    启用 API

  8. 在 Google Cloud Console 中,通过 Cloud Shell 打开此应用。

    转到 Cloud Shell

    利用 Cloud Shell,您可以直接在浏览器中通过命令行访问云端资源。在浏览器中打开 Cloud Shell,然后点击继续下载示例代码并切换到应用目录。

  9. 在 Cloud Shell 中,配置 gcloud 工具以使用您的 Google Cloud 项目:
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID

了解 Cloud Functions 函数

  • 该函数首先导入多个依赖项,例如 Firestore 和 Translation。
    import base64
    import hashlib
    import json
    
    from google.cloud import firestore
    from google.cloud import translate_v2 as translate
  • 系统初始化全局 Firestore 和 Translation 客户端,以便可以在各函数调用之间重复使用它们。这样,您便无需针对每次函数调用初始化新的客户端(此操作会降低执行速度)。
    # Get client objects once to reuse over multiple invocations.
    xlate = translate.Client()
    db = firestore.Client()
  • Translation API 会将字符串翻译为您选择的语言。
    def translate_string(from_string, to_language):
        """ Translates a string to a specified language.
    
        from_string - the original string before translation
    
        to_language - the language to translate to, as a two-letter code (e.g.,
            'en' for english, 'de' for german)
    
        Returns the translated string and the code for original language
        """
        result = xlate.translate(from_string, target_language=to_language)
        return result['translatedText'], result['detectedSourceLanguage']
  • Cloud Functions 函数首先会解析 Pub/Sub 消息,以获取要翻译的文本和所需的目标语言。

    然后,Cloud Functions 函数会翻译文本并将其存储在 Firestore 中,并使用事务来确保没有重复的翻译。

    def document_name(message):
        """ Messages are saved in a Firestore database with document IDs generated
            from the original string and destination language. If the exact same
            translation is requested a second time, the result will overwrite the
            prior result.
    
            message - a dictionary with fields named Language and Original, and
                optionally other fields with any names
    
            Returns a unique name that is an allowed Firestore document ID
        """
        key = '{}/{}'.format(message['Language'], message['Original'])
        hashed = hashlib.sha512(key.encode()).digest()
    
        # Note that document IDs should not contain the '/' character
        name = base64.b64encode(hashed, altchars=b'+-').decode('utf-8')
        return name
    
    @firestore.transactional
    def update_database(transaction, message):
        name = document_name(message)
        doc_ref = db.collection('translations').document(document_id=name)
    
        try:
            doc_ref.get(transaction=transaction)
        except firestore.NotFound:
            return  # Don't replace an existing translation
    
        transaction.set(doc_ref, message)
    
    def translate_message(event, context):
        """ Process a pubsub message requesting a translation
        """
        message_data = base64.b64decode(event['data']).decode('utf-8')
        message = json.loads(message_data)
    
        from_string = message['Original']
        to_language = message['Language']
    
        to_string, from_language = translate_string(from_string, to_language)
    
        message['Translated'] = to_string
        message['OriginalLanguage'] = from_language
    
        transaction = db.transaction()
        update_database(transaction, message)

部署 Cloud Functions 函数

  • 在 Cloud Shell 的 function 目录中,使用 Pub/Sub 触发器部署 Cloud Functions 函数:

    gcloud functions deploy Translate --runtime=python37 \
    --entry-point=translate_message --trigger-topic=translate \
    --set-env-vars GOOGLE_CLOUD_PROJECT=YOUR_GOOGLE_CLOUD_PROJECT
    

    其中 YOUR_GOOGLE_CLOUD_PROJECT 是您的 Google Cloud 项目 ID。

了解应用

Web 应用具有两个主要组件:

  • 用于处理 Web 请求的 Python HTTP 服务器。该服务器具有以下两个端点:
    • /:列出所有现有翻译,并显示一个用户可以提交以请求新翻译的表单。
    • /request-translation:表单提交被发送到此端点,此端点将请求发布到 Pub/Sub 进行异步翻译。
  • 一个 HTML 模板,该模板由 Python 服务器使用现有译文进行填充。

HTTP 服务器

  • app 目录中,main.py 首先导入依赖项、创建 Flask 应用、初始化 Firestore 和 Translation 客户端以及定义支持的语言列表:

    import json
    import os
    
    from flask import Flask, redirect, render_template, request
    from google.cloud import firestore
    from google.cloud import pubsub
    
    app = Flask(__name__)
    
    # Get client objects to reuse over multiple invocations
    db = firestore.Client()
    publisher = pubsub.PublisherClient()
    
    # Keep this list of supported languages up to date
    ACCEPTABLE_LANGUAGES = ("de", "en", "es", "fr", "ja", "sw")
  • 索引处理程序 (/) 从 Firestore 获取所有现有翻译,并使用以下列表填充 HTML 模板:

    @app.route("/", methods=["GET"])
    def index():
        """The home page has a list of prior translations and a form to
        ask for a new translation.
        """
    
        doc_list = []
        docs = db.collection("translations").stream()
        for doc in docs:
            doc_list.append(doc.to_dict())
    
        return render_template("index.html", translations=doc_list)
    
    
  • 通过提交 HTML 表单来请求新的翻译。在 /request-translation 注册的请求翻译处理程序会解析表单提交,验证请求,并向 Pub/Sub 发布一条消息:

    @app.route("/request-translation", methods=["POST"])
    def translate():
        """Handle a request to translate a string (form field 'v') to a given
        language (form field 'lang'), by sending a PubSub message to a topic.
        """
        source_string = request.form.get("v", "")
        to_language = request.form.get("lang", "")
    
        if source_string == "":
            error_message = "Empty value"
            return error_message, 400
    
        if to_language not in ACCEPTABLE_LANGUAGES:
            error_message = "Unsupported language: {}".format(to_language)
            return error_message, 400
    
        message = {
            "Original": source_string,
            "Language": to_language,
            "Translated": "",
            "OriginalLanguage": "",
        }
    
        topic_name = "projects/{}/topics/{}".format(
            os.getenv("GOOGLE_CLOUD_PROJECT"), "translate"
        )
        publisher.publish(
            topic=topic_name, data=json.dumps(message).encode("utf8")
        )
        return redirect("/")
    
    

HTML 模板

HTML 模板是显示给用户的 HTML 页面的基础,以便他们可以看到之前的翻译和请求新的翻译。该模板由 HTTP 服务器使用现有翻译列表填充。

  • HTML 模板的 <head> 元素包含该页面的元数据、样式表和 JavaScript:
    <html>
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Translations</title>
    
        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
        <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
        <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
        <script>
            $(document).ready(function() {
                $("#translate-form").submit(function(e) {
                    e.preventDefault();
                    // Get value, make sure it's not empty.
                    if ($("#v").val() == "") {
                        return;
                    }
                    $.ajax({
                        type: "POST",
                        url: "/request-translation",
                        data: $(this).serialize(),
                        success: function(data) {
                            // Show snackbar.
                            console.log(data);
                            var notification = document.querySelector('.mdl-js-snackbar');
                            $("#snackbar").removeClass("mdl-color--red-100");
                            $("#snackbar").addClass("mdl-color--green-100");
                            notification.MaterialSnackbar.showSnackbar({
                                message: 'Translation requested'
                            });
                        },
                        error: function(data) {
                            // Show snackbar.
                            console.log("Error requesting translation");
                            var notification = document.querySelector('.mdl-js-snackbar');
                            $("#snackbar").removeClass("mdl-color--green-100");
                            $("#snackbar").addClass("mdl-color--red-100");
                            notification.MaterialSnackbar.showSnackbar({
                                message: 'Translation request failed'
                            });
                        }
                    });
                });
            });
        </script>
        <style>
            .lang {
                width: 50px;
            }
            .translate-form {
                display: inline;
            }
        </style>
    </head>

    该页面会拉取 Material Design Lite (MDL) CSS 和 JavaScript 资产。借助 MDL,您可以向您的网站添加 Material Design 外观和风格。

    该页面使用 JQuery 等待文档完成加载并设置表单提交处理程序。 提交请求翻译表单后,该页面会进行最小的表单验证,以检查值是否不为空,然后将异步请求发送到 /request-translation 端点。

    最后,系统会显示一个 MDL 信息提示控件,指示请求是否成功或是否出错。

  • 页面的 HTML 正文使用 MDL 布局和多个 MDL 组件来显示翻译列表和用于请求其他翻译的表单:
    <body>
        <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
            <header class="mdl-layout__header">
                <div class="mdl-layout__header-row">
                    <!-- Title -->
                    <span class="mdl-layout-title">Translate with Background Processing</span>
                </div>
            </header>
            <main class="mdl-layout__content">
                <div class="page-content">
                    <div class="mdl-grid">
                    <div class="mdl-cell mdl-cell--1-col"></div>
                        <div class="mdl-cell mdl-cell--3-col">
                            <form id="translate-form" class="translate-form">
                                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                                    <input class="mdl-textfield__input" type="text" id="v" name="v">
                                    <label class="mdl-textfield__label" for="v">Text to translate...</label>
                                </div>
                                <select class="mdl-textfield__input lang" name="lang">
                                    <option value="de">de</option>
                                    <option value="en">en</option>
                                    <option value="es">es</option>
                                    <option value="fr">fr</option>
                                    <option value="ja">ja</option>
                                    <option value="sw">sw</option>
                                </select>
                                <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent" type="submit"
                                    name="submit">Submit</button>
                            </form>
                        </div>
                        <div class="mdl-cell mdl-cell--8-col">
                            <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
                                <thead>
                                    <tr>
                                        <th class="mdl-data-table__cell--non-numeric"><strong>Original</strong></th>
                                        <th class="mdl-data-table__cell--non-numeric"><strong>Translation</strong></th>
                                    </tr>
                                </thead>
                                <tbody>
                                {% for translation in translations %}
                                    <tr>
                                        <td class="mdl-data-table__cell--non-numeric">
                                            <span class="mdl-chip mdl-color--primary">
                                                <span class="mdl-chip__text mdl-color-text--white">{{ translation['OriginalLanguage'] }} </span>
                                            </span>
                                        {{ translation['Original'] }}
                                        </td>
                                        <td class="mdl-data-table__cell--non-numeric">
                                            <span class="mdl-chip mdl-color--accent">
                                                <span class="mdl-chip__text mdl-color-text--white">{{ translation['Language'] }} </span>
                                            </span>
                                            {{ translation['Translated'] }}
                                        </td>
                                    </tr>
                                {% endfor %}
                                </tbody>
                            </table>
                            <br/>
                            <button class="mdl-button mdl-js-button mdl-button--raised" type="button" onClick="window.location.reload();">
                                Refresh
                            </button>
                        </div>
                    </div>
                </div>
                <div aria-live="assertive" aria-atomic="true" aria-relevant="text" class="mdl-snackbar mdl-js-snackbar" id="snackbar">
                    <div class="mdl-snackbar__text mdl-color-text--black"></div>
                    <button type="button" class="mdl-snackbar__action"></button>
                </div>
            </main>
        </div>
    </body>
    
    </html>

部署 Web 应用

通过 App Engine 标准环境,您可以构建和部署在繁重负载和大量数据的压力下仍能可靠运行的应用。

本教程使用 App Engine 标准环境来部署 HTTP 前端。

app.yaml 配置 App Engine 应用:

runtime: python37
  • app.yaml 文件所在目录中,将您的应用部署到 App Engine 标准环境:
    gcloud app deploy

测试应用

部署 Cloud Functions 函数和 App Engine 应用后,尝试请求翻译。

  1. 如需在浏览器中查看该应用,请输入以下网址:

    https://PROJECT_ID.REGION_ID.r.appspot.com

    替换以下内容:

    您将看到一个页面,其中包含一个空翻译列表和一个用于请求新翻译的表单。

  2. 要翻译的文本 (Text to translate) 字段中,输入一些要翻译的文本,例如 Hello, World
  3. 从下拉列表中选择一种要将文本翻译成的语言。
  4. 点击提交
  5. 如需刷新页面,请点击刷新 。翻译列表中具有一个新行。如果您未看到翻译,请再等待几秒钟,然后重试。如果您仍未看到翻译,请参阅有关调试应用的下一部分。

调试应用

如果您无法连接到 App Engine 应用或未看到新的翻译,请检查以下内容:

  1. 检查 gcloud 部署命令是否已成功完成,并且未输出任何错误。如果存在错误,请进行修复,然后再次尝试部署 Cloud Functions 函数App Engine 应用
  2. 在 Google Cloud Console 中,转到日志查看器页面。

    转到“日志查看器”页面
    1. 最近选择的资源下拉列表中,点击 GAE 应用,然后点击所有 module_id。您将看到访问您的应用时的请求列表,如果您未发现请求列表,请确认您是否已从下拉列表中选择所有 module_id。如果您发现 Cloud Console 出现错误消息,请检查应用代码是否与“了解应用”相关部分中的代码匹配。
    2. 最近选择的资源下拉列表中,点击 Cloud Functions 函数,然后点击所有函数名称 (All function name)。您将看到针对每个请求的翻译列出的函数。如果未看到,请检查 Cloud Functions 函数和 App Engine 应用是否使用相同的 Pub/Sub 主题:
      • background/main.py 文件中,检查 topic_name 是否为 "translate"
      • 部署 Cloud Functions 函数时,请务必包含 --trigger-topic=translate 标志。

清除数据

为避免因本教程中使用的资源导致您的 Google Cloud 帐号产生费用,请删除包含这些资源的项目,或者保留项目但删除各个资源。

删除 Cloud 项目

  1. 在 Cloud Console 中,转到管理资源页面。

    转到“管理资源”

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

删除 App Engine 实例

  1. 在 Cloud Console 中,转到 App Engine 的版本页面。

    转到“版本”

  2. 选中要删除的非默认应用版本对应的复选框。
  3. 如需删除应用版本,请点击删除

删除 Cloud Functions 函数

  • 删除您在本教程中创建的 Cloud Functions 函数:
    gcloud functions delete Translate

后续步骤