使用 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. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Make sure that billing is enabled for your Google Cloud project.

  4. Enable the Firestore, Cloud Functions, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  5. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  6. Make sure that billing is enabled for your Google Cloud project.

  7. Enable the Firestore, Cloud Functions, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  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 进行异步翻译。
  • 由 Python 服务器使用现有译文填充的 HTML 模板。

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. 要翻译的文本字段中,输入一些要翻译的文本,例如 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. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

删除 App Engine 实例

  1. In the Google Cloud console, go to the Versions page for App Engine.

    Go to Versions

  2. Select the checkbox for the non-default app version that you want to delete.
  3. 如需删除应用版本,请点击删除

删除 Cloud Functions 函数

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

后续步骤