2020/06/20

【Python】Google People API でGoogle連絡先の追加/削除する

前回に引き続き、Google People APIを触ってみます。
会社のデータベースや年賀状作成ソフトから出力した住所録のcsvをGoogle連絡先に同期させることを目標として設定しました。
本記事では、csvのファイルを読み込んで、Google連絡先に追加する部分を作成したので公開します。

住所録のcsvファイルの構成は、色々あるかと思いますが、Google連絡先からエクスポートされる際にも使用されているGoogle CSV 形式に事前に合わせてあると仮定して、プログラムを作成します。

Google CSV 形式について

Google CSV形式は恐らく以下のような構成になっているかと思います。

各列名各列の説明
Name氏名
Given Name
Additional Nameミドルネームなど
Family Name
Yomi Name氏名のよみがな
Given Name Yomi名のよみがな
Additional Name Yomiミドルネームなどのよみがな
Family Name Yomi姓のよみがな
Name Prefix敬称(名前の前);Mr.やMrs.、Dr.など
Name Suffix敬称(名前の後);様、先生など
Initialsイニシャル
Nicknameニックネーム
Short Nameショートネーム
Maiden Name旧姓
Birthday誕生日
Gender性別
Location
Billing Information請求情報
Directory Server
Mileage
Occupation職業
Hobby趣味
Sensitivity
Priority
Subject
Notesメモ
Language言語
Photo
Group Membershipグループ
E-mail 1 - TypeE-mail 1の種類;Home、Work、Mobileなど
E-mail 1 - ValueE-mail 1のアドレス
E-mail 2 - TypeE-mail 2の種類;Home、Work、Mobileなど
E-mail 2 - ValueE-mail 2のアドレス
Phone 1 - TypePhone 1の種類;Home、Work、Mobileなど
Phone 1 - ValuePhone 1の電話番号
Phone 2 - TypePhone 2の種類;Home、Work、Mobileなど
Phone 2 - ValuePhone 2の電話番号
Phone 3 - TypePhone 3の種類;Home、Work、Mobileなど
Phone 3 - ValuePhone 3の電話番号
Address 1 - TypeAddress 1の種類;Home、Workなど
Address 1 - FormattedAddress 1の住所
Address 1 - StreetAddress 1の番地・区画
Address 1 - CityAddress 1の市町村
Address 1 - PO BoxAddress 1の私書箱
Address 1 - RegionAddress 1の都道府県
Address 1 - Postal CodeAddress 1の郵便番号
Address 1 - CountryAddress 1の国
Address 1 - Extended AddressAddress 1のアパートの部屋番号など
Address 2 - TypeAddress 2の種類;Home、Workなど
Address 2 - FormattedAddress 2の住所
Address 2 - StreetAddress 2の番地・区画
Address 2 - CityAddress 2の市町村
Address 2 - PO BoxAddress 2の私書箱
Address 2 - RegionAddress 2の都道府県
Address 2 - Postal CodeAddress 2の郵便番号
Address 2 - CountryAddress 2の国
Address 2 - Extended AddressAddress 2のアパートの部屋番号など
Organization 1 - TypeOrganization 1の種類;会社、学校など
Organization 1 - NameOrganization 1の名前
Organization 1 - Yomi NameOrganization 1のよみがな
Organization 1 - TitleOrganization 1での役職
Organization 1 - DepartmentOrganization 1での部署
Organization 1 - Symbol
Organization 1 - LocationOrganization 1の場所
Organization 1 - Job DescriptionOrganization 1での仕事内容
Custom Field 1 - Typeカスタムフィールド(自由に設定可能な領域)
Custom Field 1 - Valueカスタムフィールド(自由に設定可能な領域)
参考:http://googleapis.github.io/google-api-python-client/docs/dyn/people_v1.people.html#createContact

と以上の様に多くの情報が、Google連絡先には登録可能です。
人によっては列の項目がこれよりも多かったりするかも知れませんが、上記の場合のcsvを、インポートするプログラムを組みます。
上記のカスタムフィールドは同期させる時に必要な「修正日」や「住所録内でのID」などを想定しています。
また、CP932やShift_JISで保存されたcsvを想定していません。事前に他のプログラムで処理してあるものとします。

問題点:削除用のバッチ処理コードが実装されていない


一時的に削除用のグループを作成して、そこに削除したい全てのメンバーを移動し、一時的なグループごと削除するという方法が掲示されていました。

しかし、contactGroups.members.modifyでは、連絡先全体(people/me)を指定することができない様なので、結局は、逐次のグループ移動処理が必要になると思います。

前回作成したコードを一部流用して作成したのが以下のものです。

作成したコード

必要なファイル:credentials.json、contacts.csv

import csv
import time
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
CONTACT_CSV_FILE = 'googlecontacts.csv'
# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/contacts']
CLIENT_SECRET_FILE = 'credentials.json'
class ImportContacts(object):
def read_contacts(self, file_name):
# Import the csv file of the address book and convert it into a list
with open(file_name, 'r') as f:
csv_reader = csv.reader(f)
csv_header = next(csv_reader) # Skip the header line
csv_contacts = []
for csv_contact in csv_reader:
csv_contacts.append(csv_contact)
return csv_contacts
def print_all_contacts(self, file_name):
csv_contacts = self.read_contacts(file_name)
for i, csv_contact in enumerate(csv_contacts):
print('no.%s %s' % (i+1, csv_contact))
class QuickstartMod(object):
def __init__(self):
creds = None
# The file token.pickle stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first time.
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE, SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.pickle', 'wb') as token:
pickle.dump(creds, token)
creds = creds
self.service = build('people', 'v1', credentials=creds)
def get_all_contacts(self):
# Keep getting 1000 connections until the nextPageToken becomes None
connections_list = []
next_page_token = ''
while True:
if not (next_page_token is None):
# Call the People API
results = self.service.people().connections().list(
resourceName = 'people/me',
pageSize = 1000,
personFields = 'names,emailAddresses',
pageToken = next_page_token
).execute()
connections_list = connections_list + results.get('connections', [])
next_page_token = results.get('nextPageToken')
else:
break
return connections_list
def print_all_contacts(self):
connections_list = self.get_all_contacts()
if connections_list:
for i, person in enumerate(connections_list):
resource_name = person.get('resourceName', [])
names = person.get('names', [])
if names:
display_name = names[0].get('displayName')
else:
display_name = None
print('no.%s %s %s' % (i+1, display_name, resource_name))
else:
print('No connections found.')
def create_contact_body(self, contact):
given_name = contact[1]
middle_name = contact[2]
family_name = contact[3]
notes = contact[25]
email_work = contact[30]
email_home = contact[32]
phone_mobile = contact[34]
phone_work = contact[36]
phone_home = contact[38]
address_home = contact[40] # The unstructured value of the address
p_code_home = contact[45]
address_work = contact[50] # The unstructured value of the address
p_code_work = contact[55]
org_name = contact[58]
org_title = contact[60]
department = contact[61]
record_id = contact[65] # the user defined data, intended to be used for synchronization
modified_day = contact[66] # the user defined data, intended to be used for synchronization
contact_body = {
'names':[{'givenName':given_name, 'familyName':family_name, 'middleName':middle_name,}],
'emailAddresses':[{'value':email_work, 'type':'work'}, {'value':email_home, 'type':'home'}],
'phoneNumbers':[{'value':phone_mobile, 'type':'mobile'},
{'value':phone_home, 'type':'home'},
{'value':phone_work, 'type':'work'}],
'addresses':[{'streetAddress':address_home, 'postalCode':p_code_home, 'type':'home'},
{'streetAddress':address_work, 'postalCode':p_code_work, 'type':'work'}],
'organizations':[{'name':org_name, 'title':org_title}, {'department':department}],
'biographies':[{'value':notes}],
'userDefined':[{'key':record_id, 'value':modified_day}]
}
return contact_body
# Upload contacts one by one.
def add_contact(self, contact):
try:
contact_body = self.create_contact_body(contact)
new_contact = self.service.people().createContact(body=contact_body).execute()
print('Uploaded id:%s %s %s' % (contact[65], contact[1], contact[3]))
except IndexError:
print('Error id:%s %s %s contains input errors' % (contact[65], contact[1], contact[3]))
pass
def create_contact_group(self, group_name):
results = self.service.contactGroups().create(body={'contactGroup':{'name':group_name}}).execute()
group_id = results.get('resourceName', [])
return group_id
def delete_all_contacts(self):
group_id = self.create_contact_group('temp')
print('making a temporary group to delete...')
time.sleep(5)
print('waiting ...')
time.sleep(5)
connections = self.get_all_contacts()
if connections:
for i, person in enumerate(connections):
resource_name = person.get('resourceName', [])
add_group = self.service.contactGroups().members().modify(
resourceName = group_id,
body = {'resourceNamesToAdd':[resource_name]}).execute()
print(resource_name +' moved to the temporary group')
else:
print('No connections found.')
self.service.contactGroups().delete(resourceName=group_id,deleteContacts=True).execute()
print('All contacts have been deleted.')
def main():
read_contacts = ImportContacts()
contacts_list = read_contacts.read_contacts(CONTACT_CSV_FILE)
sync_contacts = QuickstartMod()
data = []
for contact in contacts_list:
data.append(sync_contacts.add_contact(contact))
#sync_contacts.delete_all_contacts()
if __name__ == '__main__':
main()
以下のページにAPIの仕様がまとまっているので開発の上で役に立つかと思います。

続き

2020/06/19

【Python】Google People API でGoogle連絡先を取得する


Google連絡先の取得、追加、削除などを行うにはGoogle Contacts APIGoolge People APIが利用できます。
しかし、Contacts APIは、People APIに置き換わっていくそうなので、これからGoogle連絡先と連携するアプリケーションを作成する際はPeople APIの方を用いると良いかも知れません。
個人的な経験ですが、G SuiteのGoogle連絡先を更新する時Contacts APIでは上手く行きませんでしたが、People APIでは更新できました。ですが、今のところ、People APIではContacts APIの機能を全て互換出来ている訳ではない様です…

PythonからPeople APIを使う

Google People APIをPythonから使うためのサンプルコードとしては、公式ドキュメントに記載してあるPython Quickstartがあります。

本サイトでは、会社のデータベースや年賀状作成ソフトから出力した住所録のcsvをGoogle連絡先に同期させることを目標として、プログラムを作成しようと思います。
本記事では、Python Quickstart内のquickstart.pyを元にGoogle連絡先のデータを取得・表示するコードを作成したものを公開します。

People APIの有効化と必要なライブラリのインストール

まずは、Python Quickstartの内容に従って、サンプルコードquickstart.pyが動く状態にします。
下記のコードでは、ここでダウンロードしたcredentials.jsonをそのまま利用します。
また、People APIは、現時点ではPython 2.6以上に対応していますが、Python 2系統はサポートが終了し、Python 3系への移行が推奨されることから、
本サイト記載のコードは、Python 3系環境で実行することを想定しています。

問題点:people.connections.listのpageSizeの上限

公式ドキュメントによると、記事作成時(2019.06.19)時点では、pageSizeは1~1000の間しか設定出来ない様です。(少し前まで上限は2000だった様な…?)
この上限を超える件数の連絡先を取得したい場合は、nextPageTokenを利用すると良い様です。


作成したコード

必要なファイル:credentials.json

import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/contacts']
CLIENT_SECRET_FILE = 'credentials.json'
class QuickstartMod(object):
def __init__(self):
creds = None
# The file token.pickle stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first time.
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE, SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.pickle', 'wb') as token:
pickle.dump(creds, token)
creds = creds
self.service = build('people', 'v1', credentials=creds)
def get_all_contacts(self):
# Keep getting 1000 connections until the nextPageToken becomes None
connections_list = []
next_page_token = ''
while True:
if not (next_page_token is None):
# Call the People API
results = self.service.people().connections().list(
resourceName = 'people/me',
pageSize = 1000,
personFields = 'names,emailAddresses',
pageToken = next_page_token
).execute()
connections_list = connections_list + results.get('connections', [])
next_page_token = results.get('nextPageToken')
else:
break
return connections_list
def print_all_contacts(self):
connections_list = self.get_all_contacts()
if connections_list:
for i, person in enumerate(connections_list):
resource_name = person.get('resourceName', [])
names = person.get('names', [])
if names:
display_name = names[0].get('displayName')
else:
display_name = None
print('no.%s %s %s' % (i+1, display_name, resource_name))
else:
print('No connections found.')
def main():
sync_contacts = QuickstartMod()
sync_contacts.print_all_contacts()
if __name__ == '__main__':
main()
以下のページにAPIの仕様がまとまっているので開発の上で役に立つかと思います。

続き