使用 Docs API 進行郵件合併

本指南說明如何使用 Google 文件 API 執行郵件合併。

簡介

合併列印會從試算表或其他資料來源的資料列中擷取值,並插入範本文件。這樣就能建立單一主要文件 (範本),並從中產生許多類似文件,每份文件都可透過合併資料自訂。結果不一定會用於郵件或範本信件,但可用於任何用途,例如產生一批客戶發票。

自從有了試算表和文書處理器,郵件合併功能就應運而生,如今已成為許多商務工作流程的一部分。慣例是將資料整理成每列一筆記錄,並以資料欄代表資料中的欄位,如下表所示:

名稱 地址 可用區
1 UrbanPq 123 1st St. 西
2 Pawxana 456 2nd St.

本頁的範例應用程式說明如何使用 Google 文件、試算表和雲端硬碟 API,抽象化郵件合併的執行細節,讓使用者不必擔心實作問題。如要進一步瞭解這個 Python 範例,請參閱範例的 GitHub 存放區

應用程式範例

這個範例應用程式會複製主要範本,然後將指定資料來源的變數合併到每個副本中。如要試用這個範例應用程式,請先設定範本:

  1. 建立 Google 文件檔案。 選擇要使用的範本。
  2. 請記下新檔案的文件 ID。詳情請參閱文件 ID
  3. DOCS_FILE_ID 變數設為文件 ID。
  4. 將聯絡資訊替換為範本預留位置變數,應用程式會將這些變數與所選資料合併。

以下是範本信件,內含預留位置,可與純文字或 Google 試算表等來源的實際資料合併。範本如下所示:

接著,使用 SOURCE 變數選擇純文字或試算表做為資料來源。範例預設為純文字,也就是說,範例資料會使用 TEXT_SOURCE_DATA 變數。如要從 Google 試算表取得資料,請將 SOURCE 變數更新為 'sheets',並透過設定 SHEETS_FILE_ID 變數,將其指向我們的範例試算表 (或您自己的試算表)。

以下是試算表範例,可供您參考格式:

您可以先使用範例資料試用應用程式,然後根據自己的資料和用途進行調整。指令列應用程式的運作方式如下:

  • 設定
  • 從資料來源擷取資料
  • 逐一查看每列資料
    • 複製範本
    • 將副本與資料合併
    • 新合併文件的輸出連結

所有新合併的信件也會顯示在使用者的「我的雲端硬碟」中。合併後信件的範例如下:

原始碼

Python

docs/mail-merge/docs_mail_merge.py
import time  import google.auth from googleapiclient.discovery import build from googleapiclient.errors import HttpError  # Fill-in IDs of your Docs template & any Sheets data source DOCS_FILE_ID = "195j9eDD3ccgjQRttHhJPymLJUCOUjs-jmwTrekvdjFE" SHEETS_FILE_ID = "11pPEzi1vCMNbdpqaQx4N43rKmxvZlgEHE9GqpYoEsWw"  # authorization constants  SCOPES = (  # iterable or space-delimited string     "https://www.googleapis.com/auth/drive",     "https://www.googleapis.com/auth/documents",     "https://www.googleapis.com/auth/spreadsheets.readonly", )  # application constants SOURCES = ("text", "sheets") SOURCE = "text"  # Choose one of the data SOURCES COLUMNS = ["to_name", "to_title", "to_company", "to_address"] TEXT_SOURCE_DATA = (     (         "Ms. Lara Brown",         "Googler",         "Google NYC",         "111 8th Ave\nNew York, NY  10011-5201",     ),     (         "Mr. Jeff Erson",         "Googler",         "Google NYC",         "76 9th Ave\nNew York, NY  10011-4962",     ), )  # fill-in your data to merge into document template variables merge = {     # sender data     "my_name": "Ayme A. Coder",     "my_address": "1600 Amphitheatre Pkwy\nMountain View, CA  94043-1351",     "my_email": "http://google.com",     "my_phone": "+1-650-253-0000",     # - - - - - - - - - - - - - - - - - - - - - - - - - -     # recipient data (supplied by 'text' or 'sheets' data source)     "to_name": None,     "to_title": None,     "to_company": None,     "to_address": None,     # - - - - - - - - - - - - - - - - - - - - - - - - - -     "date": time.strftime("%Y %B %d"),     # - - - - - - - - - - - - - - - - - - - - - - - - - -     "body": (         "Google, headquartered in Mountain View, unveiled the new "         "Android phone at the Consumer Electronics Show. CEO Sundar "         "Pichai said in his keynote that users love their new phones."     ), }  creds, _ = google.auth.default() # pylint: disable=maybe-no-member  # service endpoints to Google APIs  DRIVE = build("drive", "v2", credentials=creds) DOCS = build("docs", "v1", credentials=creds) SHEETS = build("sheets", "v4", credentials=creds)   def get_data(source):   """Gets mail merge data from chosen data source."""   try:     if source not in {"sheets", "text"}:       raise ValueError(           f"ERROR: unsupported source {source}; choose from {SOURCES}"       )     return SAFE_DISPATCH[source]()   except HttpError as error:     print(f"An error occurred: {error}")     return error   def _get_text_data():   """(private) Returns plain text data; can alter to read from CSV file."""   return TEXT_SOURCE_DATA   def _get_sheets_data(service=SHEETS):   """(private) Returns data from Google Sheets source. It gets all rows of   'Sheet1' (the default Sheet in a new spreadsheet), but drops the first   (header) row. Use any desired data range (in standard A1 notation).   """   return (       service.spreadsheets()       .values()       .get(spreadsheetId=SHEETS_FILE_ID, range="Sheet1")       .execute()       .get("values")[1:]   )   # skip header row   # data source dispatch table [better alternative vs. eval()] SAFE_DISPATCH = {k: globals().get(f"_get_{k}_data") for k in SOURCES}   def _copy_template(tmpl_id, source, service):   """(private) Copies letter template document using Drive API then   returns file ID of (new) copy.   """   try:     body = {"name": f"Merged form letter ({source})"}     return (         service.files()         .copy(body=body, fileId=tmpl_id, fields="id")         .execute()         .get("id")     )   except HttpError as error:     print(f"An error occurred: {error}")     return error   def merge_template(tmpl_id, source, service):   """Copies template document and merges data into newly-minted copy then   returns its file ID.   """   try:     # copy template and set context data struct for merging template values     copy_id = _copy_template(tmpl_id, source, service)     context = merge.iteritems() if hasattr({}, "iteritems") else merge.items()      # "search & replace" API requests for mail merge substitutions     reqs = [         {             "replaceAllText": {                 "containsText": {                     "text": "{{%s}}" % key.upper(),  # {{VARS}} are uppercase                     "matchCase": True,                 },                 "replaceText": value,             }         }         for key, value in context     ]      # send requests to Docs API to do actual merge     DOCS.documents().batchUpdate(         body={"requests": reqs}, documentId=copy_id, fields=""     ).execute()     return copy_id   except HttpError as error:     print(f"An error occurred: {error}")     return error   if __name__ == "__main__":   # get row data, then loop through & process each form letter   data = get_data(SOURCE)  # get data from data source   for i, row in enumerate(data):     merge.update(dict(zip(COLUMNS, row)))     print(         "Merged letter %d: docs.google.com/document/d/%s/edit"         % (i + 1, merge_template(DOCS_FILE_ID, SOURCE, DRIVE))     )

詳情請參閱 README 檔案,以及範例應用程式 GitHub 存放區中的完整應用程式原始碼。