Googleマップを検索して「訪問指示書」を自動作成

2020年5月17日

Excelの「受付一覧表」を元に、Wordで「訪問指示書」を自動作成します。
一覧表の住所情報から、Googleマップで地図を取得して、Wordに貼り付けます。
ファイル名でソートしやすいように、訪問指示書のファイル名には「担当者」と「日付」を含めます。

自動操作の動画はこちら

作業手順

  1. 受付一覧表のフォーマット確定
  2. 訪問指示書のフォーマット確定
  3. GoogleマップのXPath取得
    XPath取得ツール、XPath解析ツールを用いて、地図の取得に必要なXPath情報を確定します。
  4. Pythonスクリプトの作成
    Seleniumのスクリプトを作成します。
  5. Seleniumスクリプトの実行
  6. デバッグ作業
  7. 評価・考察

受付一覧表

業種や企業によって異なると思いますが、顧客を訪問するための基本的な内容として以下のようにします。

項目 内容
受付番号 一意となるID
顧客名  
住所 Googleマップ検索対象
電話番号  
訪問希望日  
担当者 訪問する担当者
備考 訪問担当者に伝えておく事項

住所をGoogleマップで検索して、地図を取得します。
その他の項目は、訪問指示書にそのまま転記します。

受付一覧表

  • 住所は鉄道の駅にしています。
  • No1~No3:n丁目n番n号の部分をパターンに分けています。3パターンとも正しく検索できれば実用に耐えると判断できます。
  • No10:存在しない住所の場合、Googleマップで地図取得時にエラーになるかを確認します。
  • No11:番地まで正しく、番号のみ違うような場合でも、果たして地図検索時にエラーと判定できるかを確認します。

訪問指示書

訪問指示書
訪問指示書(フォーマット)
項目 内容
日付 作成日を記載
タイトル 固定値
受付番号 受付一覧表から転記
顧客名
住所
電話番号
訪問希望日
担当者
備考
地図タイトル 固定値
地図 住所情報をもとにGoogleマップから取得

GoogleマップのXPath取得

入力項目洗い出し

まず入力項目を洗い出します。

Googleマップ

住所入力欄(青枠)、検索ボタン(赤枠)が入力対象です。

XPathの取得

住所欄と検索ボタンのXPathを、XPath取得ツールを用いて取得します。

XPath取得ツール起動

  1. XPath取得ツールを起動し、起動したブラウザでGoogleマップを開きます。
  2. 「XPath取得」ボタンを押して、入力欄を右クリックします。
  3. 「解析」ボタンを押して、出力されたXPathをコピーします。
    (出力欄をクリックすると、自動的に内容がクリップボードにコピーされます)
Googleマップ「住所欄」のXPath取得
Googleマップ「検索ボタン」のXPath取得

取得したXPath

項目 XPath
住所欄 //input[@id="searchboxinput"]
検索ボタン //button[@id="searchbox-searchbutton"]

XPathの検証

取得したXPathで実際に検索できるかをXPath検証ツールを用いて確認します。
ここで、ひと手間かけることで、XPathが原因でシナリオが動かないという要素を排除できます。
あわせて、XPath検証ツールが生成するSeleniumのスクリプトを取得します。

XPath検証ツール起動

  1. XPath検証ツールを起動し、起動したブラウザでGoogleマップを開きます。
  2. XPath検証ツールの「ロケータ」欄に、住所欄のXPathを入力し「XPath」ボタンを押します。
  3. 「Sendkey」ボタンを押して、文字入力ができることを確認します。
  4. 同じく「ロケータ」欄に、検索ボタンのXPathを入力し「XPath」ボタンを押します。
  5. 「Click」ボタンを押して、検索できることを確認します。
  6. 「生成コード」欄に出力されたPythonコードをコピーします。
    (出力欄をクリックすると、自動的に内容がクリップボードにコピーされます)
取得したXPathで「住所欄」を入力
取得したXPathで「検索ボタン」をクリック

住所欄に「hoge」と入力し、検索ボタンをクリックした結果、千葉県の「法花」が検索されました。

出力コード

項目 操作 生成コード
住所 text入力 driver.find_element_by_xpath
('//input[@id="searchboxinput"]').send_keys("hoge")
検索ボタン クリック driver.find_element_by_xpath
('//button[@id="searchbox-searchbutton"]').click()

Seleniumスクリプトの作成

入力項目(受付一覧表)

受付番号、顧客名、住所、電話番号、訪問希望日、担当者、備考

出力項目(訪問指示書)
項目 出力内容
日付 システムの現在日
タイトル 訪問指示書
受付番号 受付一覧表の受付番号
顧客名 受付一覧表の顧客名
住所 受付一覧表の住所
電話番号 受付一覧表の電話番号
訪問希望日 受付一覧表の訪問希望日
担当者 受付一覧表の担当者
備考 受付一覧表の備考
地図タイトル 【周辺地図】
地図 住所をGoogleマップで検索した地図を貼り付け

実行環境の準備

必要なライブラリをインストールします。

  1. Python実行環境
    Pythonのインストール
  2. Seleniumの 実行環境
    Seleniumのインストール
  3. Excelライブラリ
    xlwingsのインストール
  4. Wordライブラリ
    python-docxのインストール
  5. 画像加工ライブラリ
    Pillowのインストール
  6. 使用するブラウザのドライバを導入します。
    Google Chrome
    Firefox
    Microsoft Edge
    Internet Explorer

スクリプト構成

Seleniumスクリプトのプログラム構成(フローチャート)を考えます。

  1. 利用するライブラリを使う準備(import)
  2. ブラウザを操作するWebドライバの起動
  3. ブラウザでGoogleマップを開く
  4. Excelシート(一覧表)を開く
  5. Word文書(訪問指示書)を新規作成
  6. 現在日付をWordに出力
  7. Excelからコピーした情報をWordに出力
  8. ブラウザ操作
    1. Googleマップで住所を検索
    2. 地図画像の取得(スクリーンショット)
  9. 画像の切り抜き(トリミング)
  10. Wordに画像を貼り付け
  11. Word文書の保存

1~4は前準備ですので、1回だけ実行します。
5~11は入力シートの件数分だけ繰り返しの処理となります。

1.ライブラリのインポート

Pythonライブラリ(モジュール)を使用するために、前もって使用するライブラリを import文で読み込みます。

#Seleniumライブラリ
from selenium import webdriver
#Excel操作ライブラリ
import xlwings as xw
#Word操作ライブラリ
from docx import Document
#画像加工ライブラリ
from PIL import Image

2.Webドライバの起動

操作するブラウザのWebドライバを起動します。

#Chrome
driver_path = (Webドライバの配置先)
driver = webdriver.Chrome(executable_path=driver_path)
#Firefox
driver_path = (Webドライバの配置先)
driver = webdriver.Firefox(executable_path=driver_path)
#Microsoft Edge
driver_path = (Webドライバの配置先)
driver = webdriver.Edge(executable_path=driver_path)
#Intenet Explorer
driver_path = (Webドライバの配置先)
driver = webdriver.Ie(executable_path=driver_path)

3.Googleマップを開く

2で起動したWebドライバでGoogleマップを開きます。
まずはGoogleマップのURLを調べます。

倍率 12倍 17倍
  Googleマップ(倍率12倍) Googleマップ(倍率17倍)
URL https://www.google.com/maps/
@35.710067,139.8085117,12z?hl=ja
https://www.google.com/maps/
@35.710067,139.8085117,17z?hl=ja

試しに東京スカイツリーを検索したところ、URLは上記になりました。
このことからURLは以下のようになります。

https://www.google.com.maps/@【緯度】,【経度】,【ズーム倍率(数値+Z)】?hl=ja

倍率は17倍(17z)にしておきます。
(なお、?以降の「hl=ja」は日本語の指定です)

driver.get('https://www.google.co.jp/maps/@35.710067,139.8085117,17z?hl=ja')

4.Excelシートを開く

Excelシート(受付一覧表)を開きます。インポートしたxlwingsを利用し、Excelシートのオブジェクトを「ws1」という変数に格納します。こうすることで、セルを ws1.cells(i, j).value という形式で扱うことができます。

import xlwings as xw

book_path = r'excel\受付一覧表.xlsx'
wb1 = xw.Book(book_path) 
ws1 = wb1.sheets[0]    #シート1枚目

ファイル名は、カレントフォルダ下のexcelフォルダ内、受付一覧表.xlsxとします

5.Word文書を新規作成

Word文書(訪問指示書)を新規で作成します。インポートしたpython-docxを利用します。

python-docxの使い方

#新規作成
from docx import Document
document = Document()

#見出しの追加
heading = document.add_heading('見出し', level=見出しレベル)
#文字サイズ
from docx.shared import Pt
heading.runs[0].font.size = Pt(文字サイズ)

#段落の追加
paragraph = document.add_paragraph()
#文字の右寄せ
from docx.enum.text import WD_ALIGN_PARAGRAPH
paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT

#画像の貼り付け
document.add_picture(ファイル名, width=画像の幅, height=画像の高さ)

#ページ設定
from docx.shared import Mm
section = document.sections[0]
#用紙サイズ(A4タテ)
section.page_width = Mm(210)
section.page_height = Mm(297)
#余白(標準)
section.top_margin = Mm(12.7)
section.bottom_margin = Mm(12.7)
section.left_margin = Mm(12.7)
section.right_margin = Mm(12.7)

#保存
document.save(ファイル名)

訪問指示書の作成

#Word操作ライブラリ
from docx import Document
from docx.shared import Mm
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH

#Wordを開く
document = Document()
#ページ設定
section = document.sections[0]
#用紙サイズ(A4タテ)
section.page_width = Mm(210)
section.page_height = Mm(297)
#余白(標準)
section.top_margin = Mm(12.7)
section.bottom_margin = Mm(12.7)
section.left_margin = Mm(12.7)
section.right_margin = Mm(12.7)

6.現在日付を出力

ページの最上部右端に日付を出力します。datetimeライブラリを利用します。

import datetime

Wordに段落を追加し、現在日付を出力します(右寄せ)

paragraph = document.add_paragraph()
paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT   #右寄せ
paragraph.add_run(str(datetime.date.today()))    #現在日付

7.Excelからの転記

受付一覧表の内容をWordにコピーします。見出しと表を作成し、表の中に値を入れていきます。

見出しの作成

#見出し
heading = document.add_heading('訪問指示書', level=2)
heading.alignment = WD_ALIGN_PARAGRAPH.CENTER  #中央寄せ
heading.runs[0].font.size = Pt(18)    #文字サイズ18

表の作成

Wordで表を作成します。

python-docx 表の編集方法
#表の作成
table = document.add_table(rows=行数, cols=列数, style=表のスタイル)

row = table.rows[0]    #1行目
col = table.columns[0]  #1列目

#値を記入
rows[0].cells[0].text = "1行1列目"
#表の作成(7行2列、枠線Grid)
table = document.add_table(rows=7, cols=2, style='Table Grid')

Excelの情報をコピー

表の各セルにExcelシートの内容をコピーします。
Excel(xlwings)のセル番号は1から始まるのに対し、Word(python-docx)の表番号は0から始まるので注意が必要です。

# jを1から7まで繰り返す
for j in range(1, 8):
    row = table.rows[j - 1]
    #セルの幅を指定
    row.cells[0].width = Mm(30)
    row.cells[1].width = Mm(155)
    #1列目は見出し:受付一覧表の1行目をコピー
    row.cells[0].text = str(ws1.cells(1, j).value)
    #2列目に一覧表の各行をコピー
    row.cells[1].text = str(ws1.cells(i, j).value)

表に記入する場合は 、str()関数で値を文字に変換して入力します。
これがないと、数値や日付を入力した場合エラーになります。

8.ブラウザ操作

Googleマップで一覧表の住所を検索し、表示された地図のスクリーンショットを取得します。

住所検索

XPath検証ツールが出力したコードを利用します。
住所欄は2件目以降、前回の値をクリアするために clear()関数を用います。

#住所欄の入力
element = driver.find_element_by_xpath('//input[@id="searchboxinput"]')
element.clear()    #クリア処理
element.send_keys(ws1.cells(i, 3).value)    #Excelシート3列目
#検索ボタンのクリック
driver.find_element_by_xpath('//button[@id="searchbox-searchbutton"]').click()

地図画像の取得(スクリーンショット)

検索ボタンクリック後、表示された地図のスクリーンショットを取得します。
Seleniumの画面キャプチャ機能を利用します。

driver.save_screenshot(r'tmp\map1.png')    #カレントフォルダ下のtmpフォルダに保存

最大サイズで地図画像を取得できるよう、ブラウザの画面サイズを最大にしておきます。

#画面サイズ最大
driver.maximize_window()

9.画像のトリミング

取得した地図画像から、目的地の周辺部分だけを切り抜きます(トリミング)
インポートしたPillow(PLI)を利用します。

#画像加工ライブラリ
from PIL import Image

#加工対象の画像を読み込み
im1 = Image.open(r'tmp\map1.png')
#トリミング処理
im2 = im1.crop((, , , ))    #ピクセル指定
#ファイル名map2に出力
im2.save(r'tmp\map2.png',  quality=90)   #quality:画質を指定

10.画像の貼り付け

トリミングした地図画像をWordに貼り付けます。

見出し部

heading = document.add_heading('【周辺地図】', level=3)
heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
heading.runs[0].font.size = Pt(18)

地図の貼り付け

画像の貼り付け
document.add_picture(r'tmp\map2.png', width=Mm(185))    #画像の幅を185ミリに設定

Word文書の保存

作成した訪問指示書を保存します。

ファイル名の編集

ファイル名は 【訪問指示書(担当者名_日付_受付番号).docx】とします。

# ファイル名の編集
name = ws1.cells(i, 6).value    #担当者名:6列目
day = ws1.cells(i, 5).value    #訪問希望日:5列目
no = ws1.cells(i, 1).value    #受付番号:1列目
filename = '訪問指示書(' + str(name) + '_' + str(day) + '_' + str(no) + ').docx'
Word文書の保存

保存先はカレントフォルダ下のwordフォルダにします。
ディレクトリを指定するためにpythonのosライブラリをインポートします。

import os

#Word文書の保存
document.save(os.path.join('word', filename))

操作の繰り返し

受付一覧表の件数分、操作を繰り返します。
件数ごとの処理の先頭に繰り返しの判定を入れ、Excelシート1列目の受付番号に対し、 2行目~空欄になるまでの間、処理を継続します。

# 「受付番号」がある間、処理を繰り返す
i = 2
while ws1.cells(i, 1).value is not None:
    (繰り返し対象の操作)
    i += 1    #繰り返しの最後にiを加算(i=i+1)

完成スクリプト

各構成ごとのスクリプトを1つにまとめます。
(81行目:トリミング範囲の指定は、お使いのディスプレイの解像度に合わせて、適切な値を指定してください)

# -*- coding:utf-8 -*-
import os
import datetime

# Seleniumライブラリ
from selenium import webdriver

# Excel操作ライブラリ
import xlwings as xw
# Word操作ライブラリ
from docx import Document
from docx.shared import Mm
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH

# 画像加工ライブラリ
from PIL import Image

# Chromeドライバ起動
driver_path = r'D:\SeleniumCodeChecker\driver\chromedriver.exe'
driver = webdriver.Chrome(executable_path=driver_path)
# 画面サイズ最大
driver.maximize_window()
# Googleマップを開く【@緯度,経度,ズーム倍率z】
driver.get('https://www.google.co.jp/maps/@35.710067,139.8085117,17z?hl=ja')

# Excelオープン
book_path = r'excel\受付一覧表.xlsx'
wb1 = xw.Book(book_path)
ws1 = wb1.sheets[0]

#受付番号がある間、処理を繰り返す
i = 2
while ws1.cells(i, 1).value is not None:
    # Wordを開く
    document = Document()
    # ページ設定
    section = document.sections[0]
    # 用紙サイズ(A4タテ)
    section.page_width = Mm(210)
    section.page_height = Mm(297)
    # 余白(標準)
    section.top_margin = Mm(12.7)
    section.bottom_margin = Mm(12.7)
    section.left_margin = Mm(12.7)
    section.right_margin = Mm(12.7)

    # 作成日を出力
    paragraph = document.add_paragraph()
    paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    paragraph.add_run(str(datetime.date.today()))

    # タイトル
    heading = document.add_heading('訪問指示書', level=2)
    heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
    heading.runs[0].font.size = Pt(18)

    # 表の作成
    table = document.add_table(rows=7, cols=2, style='Table Grid')
    # 表の1行目から7行目まで繰り返す
    for j in range(1, 8):
        # !!Word表:0始まり(j-1)、Excel:1始まり(j)
        row = table.rows[j - 1]
        # セルの幅
        row.cells[0].width = Mm(30)
        row.cells[1].width = Mm(155)
        #項目の編集
        row.cells[0].text = str(ws1.cells(1, j).value)  # 項目名
        row.cells[1].text = str(ws1.cells(i, j).value)  # 項目内容

    # 地図の取得
    element = driver.find_element_by_xpath('//input[@id="searchboxinput"]')
    element.clear()
    element.send_keys(ws1.cells(i, 3).value)
    driver.find_element_by_xpath('//button[@id="searchbox-searchbutton"]').click()
    # 画面キャプチャ
    driver.save_screenshot(r'tmp\map1.png')

    # 画像のトリミング
    im1 = Image.open(r'tmp\map1.png')
    im2 = im1.crop((520, 100, 1670, 895))  # 左, 上, 右, 下 (括弧は二重です)
    im2.save(r'tmp\map2.png', quality=90)  # quality:90%

    # 画像の貼り付け
    heading = document.add_heading('【周辺地図】', level=3)
    heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
    heading.runs[0].font.size = Pt(18)
    document.add_picture(r'tmp\map2.png', width=Mm(185))  # 幅185ミリ

    # 文書の保存
    name = ws1.cells(i, 6).value  # 担当者名
    day = ws1.cells(i, 5).value  # 訪問希望日
    no = ws1.cells(i, 1).value  # 受付番号
    filename = '訪問指示書(' + str(name) + '_' + str(day) + '_' + str(no) + ').docx'
    document.save(os.path.join('word', filename))
    i += 1

スクリプトの実行

フォルダ作成、一覧表作成

1.作成スクリプトのフォルダに以下のフォルダを作成します。

  • excel
    受付一覧表の保存
  • tmp
    地図画像用作業フォルダ
  • word
    訪問指示書の保存

2.受付一覧表をダウンロードして、excelフォルダに保存します。
 

スクリプトの実行

Pythonで作成したSeleniumのスクリプトを実行します。

コマンドプロンプトから実行する場合
  • Windows10の場合
    「スタートメニュー」「 Windows システムツール」「コマンドプロンプト」
  • Windows8の場合
    「すべてのアプリ」 「 Windows システムツール」「コマンドプロンプト」
  • Windows7の場合
    「スタートメニュー」「すべてのプログラム」「アクセサリ」「コマンドプロンプト」

コマンドプロンプトの画面で

python 実行ファイル名
python gmap_tutrial1.py

Excelが開き、受付一覧表の住所でGoogleマップを検索します。

Googleマップ画面

1件目、ファイル保存中に例外(エラー)が発生しました。

エラー対処、スクリプトの改良

エラーメッセージ
OSError: [Errno 22] Invalid argument: 'word\\訪問指示書(鈴木_2020-03-23 00:00:00_1.0).docx'

エラー原因

ファイル名は、訪問指示書(担当者名_日付_受付番号).docxでした。

  • 日付に時間(00:00:00)が含まれています。
  • 受付番号が小数(1.0)になっています。

最初、Excelのフォーマット形式の問題かと思いましたが、いくらか試行錯誤したところ、Excelライブラリ(xlwings)が原因でした。

xlwingsのマニュアルを見たところ、

By default cells with numbers are read as float, but you can change it to int:sht.range('A1’).options(numbers=int).value

By default cells with dates are read as datetime.datetime, but you can change it to datetime.date:sht.range('A1’).options(dates=dt.date).value

Empty cells are converted per default into None, you can change this as follows:
sht.range('A1′).options(empty=’NA’).value

https://docs.xlwings.org/_/downloads/ja/latest/pdf/
第15章 Converters and Options

とあり、数値はfloat(浮動小数点型)、日付はdatetime(日付+時刻)で取得されるようです。
オプションを指定することで、整数や日付で取得できると記載されています。

また空白セルは’None’に変換されますので、空文字(empty='')に置き換えます。

解決策

Excelシートから値を取得する時にオプション指定します。

対象コード
#項目の編集
row.cells[0].text = str(ws1.cells(1, j).value)  # 項目名
row.cells[1].text = str(ws1.cells(i, j).value)  # 項目内容
# 地図の取得(住所入力)
driver.find_element_by_xpath('//input[@id="searchboxinput"]').send_keys(ws1.cells(i, 3).value)
# 文書の保存
name = ws1.cells(i, 6).value  # 担当者名
day = ws1.cells(i, 5).value  # 訪問希望日
no = ws1.cells(i, 1).value  # 受付番号
filename = '訪問指示書(' + str(name) + '_' + str(day) + '_' + str(no) + ').docx'
修正コード

住所、担当者名、訪問希望日、受付番号は再利用するので、最初の値の取得時に変数に保持するようにします。

#項目の編集
row.cells[0].text = str(ws1.cells(1, j).options(dates=datetime.date, numbers=int, empty='').value)
row.cells[1].text = str(ws1.cells(i, j).options(dates=datetime.date, numbers=int, empty='').value)
#後で使用する項目を変数で保持
if ws1.cells(1, j).value == '住所':
    address = str(ws1.cells(i, j).value)
if ws1.cells(1, j).value == '担当者':
    name = str(ws1.cells(i, j).value)
if ws1.cells(1, j).value == '訪問希望日':
    day = str(ws1.cells(i, j).options(dates=datetime.date).value)
if ws1.cells(1, j).value == '受付番号':
    no = str(ws1.cells(i, j).options(numbers=int).value)
# 地図の取得(住所入力)
driver.find_element_by_xpath('//input[@id="searchboxinput"]').send_keys(address)
# 文書の保存
filename = '訪問指示書(' + name + '_' + day + '_' + no + ').docx'

修正スクリプト

# -*- coding:utf-8 -*-
import os
import datetime

# Seleniumライブラリ
from selenium import webdriver

# Excel操作ライブラリ
import xlwings as xw
# Word操作ライブラリ
from docx import Document
from docx.shared import Mm
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH

# 画像加工ライブラリ
from PIL import Image

# Chromeドライバ起動
driver_path = r'D:\SeleniumCodeChecker\driver\chromedriver.exe'
driver = webdriver.Chrome(executable_path=driver_path)
# 画面サイズ最大
driver.maximize_window()
# Googleマップを開く【@緯度,経度,ズーム倍率z】
driver.get('https://www.google.co.jp/maps/@35.710067,139.8085117,17z?hl=ja')

# Excelオープン
book_path = r'excel\受付一覧表.xlsx'
wb1 = xw.Book(book_path)
ws1 = wb1.sheets[0]

#受付番号がある間、処理を繰り返す
i = 2
while ws1.cells(i, 1).value is not None:
    # Wordを開く
    document = Document()
    # ページ設定
    section = document.sections[0]
    # 用紙サイズ(A4タテ)
    section.page_width = Mm(210)
    section.page_height = Mm(297)
    # 余白(標準)
    section.top_margin = Mm(12.7)
    section.bottom_margin = Mm(12.7)
    section.left_margin = Mm(12.7)
    section.right_margin = Mm(12.7)

    # 作成日を出力
    paragraph = document.add_paragraph()
    paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    paragraph.add_run(str(datetime.date.today()))

    # タイトル
    heading = document.add_heading('訪問指示書', level=2)
    heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
    heading.runs[0].font.size = Pt(18)

    # 表の作成
    table = document.add_table(rows=7, cols=2, style='Table Grid')
    # 表の1行目から7行目まで繰り返す
    for j in range(1, 8):
        # !!Word表:0始まり(j-1)、Excel:1始まり(j)
        row = table.rows[j - 1]
        # セルの幅
        row.cells[0].width = Mm(30)
        row.cells[1].width = Mm(155)
        #項目の編集
        row.cells[0].text = str(ws1.cells(1, j).options\
                    (dates=datetime.date, numbers=int, empty='').value)  # 項目名
        row.cells[1].text = str(ws1.cells(i, j).options\
                    (dates=datetime.date, numbers=int, empty='').value)  # 項目内容

        # 後で使用する項目を変数で保持
        if ws1.cells(1, j).value == '住所':
            address = str(ws1.cells(i, j).value)
        if ws1.cells(1, j).value == '担当者':
            name = str(ws1.cells(i, j).value)
        if ws1.cells(1, j).value == '訪問希望日':
            day = str(ws1.cells(i, j).options(dates=datetime.date).value)
        if ws1.cells(1, j).value == '受付番号':
            no = str(ws1.cells(i, j).options(numbers=int).value)

    # 地図の取得
    element = driver.find_element_by_xpath('//input[@id="searchboxinput"]')
    element.clear()
    element.send_keys(address)
    driver.find_element_by_xpath('//button[@id="searchbox-searchbutton"]').click()
    # 画面キャプチャ
    driver.save_screenshot(r'tmp\map1.png')

    # 画像のトリミング
    im1 = Image.open(r'tmp\map1.png')
    im2 = im1.crop((520, 100, 1670, 895))  # 左, 上, 右, 下
    im2.save(r'tmp\map2.png', quality=90)  # quality:90%

    # 画像の貼り付け
    heading = document.add_heading('【周辺地図】', level=3)
    heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
    heading.runs[0].font.size = Pt(18)
    document.add_picture(r'tmp\map2.png', width=Mm(185))  # 幅185ミリ

    # 文書の保存
    filename = '訪問指示書(' + name + '_' + day + '_' + no + ').docx'
    document.save(os.path.join('word', filename))
    i += 1

スクリプトの実行

コマンドプロンプトから、修正したSeleniumスクリプトを実行します。

python gmap_tutrial2.py

今回は無事に動いているようです。

Googleマップ画面

スクリプトが終了したら、wordフォルダの中を確認します。

作成されたWord文書

文書が作成されていますので、中身を確認します。
(最初の2件分のWord文書を、1ファイルに統合して、PDF2ページとして貼り付けています)

訪問指示書a1

初期表示の東京スカイツリーが1件目の訪問指示書、1件目の鷺ノ宮駅が2件目の訪問指示書に出力されてしまっています。どういうことでしょうか…

Ajax対応

発生事象

Seleniumで、Googleマップを住所検索すると、1件前の地図画像が取得される

エラー原因

通常のWebサイト

 通常のWebサイトは、画面入力して送信ボタンを押すと、サーバーにリクエストを送信して、サーバーからレスポンスが返されます。
通信している間、レスポンスが返ってくるまで、ブラウザは待ち状態となり、レスポンスが戻ってくると描画を始めます。
 ブラウザが待ち状態の間は、Seleniumも待機していて、通信が完了すると再始動します。
Seleniumが始動した時は、すでにサーバーからレスポンスが返って来ていますので、Seleniumは新たな情報を取得することができます。

通常のWebサイト
通常のWebサイト(同期通信)

Ajax(エイジャックス)を用いたサイト

 一方、Googleマップなどは、Ajax(Asynchronous JavaScript + XML)という技術が使われています。
Ajaxでは、画面入力して送信ボタンを押し、サーバーにリクエストを送信しても、ブラウザは待ち状態とはなりません
 よってSeleniumも待機せず、送信ボタンを押した後、すぐに地図画像を取得しに行きます。
しかしながら、サーバーからレスポンス(新しい地図)が戻ってきていないため、画面は古い地図のまま変わっていません。
 だから1件前の古い地図画像が取得されてしまうのです。

Ajaxを用いたサイト
Ajaxを用いたサイト(非同期通信)

なお、Ajaxを用いることで、ユーザーは通信の間も待つことなく操作ができ、ストレスを感じずにサイトを利用することができます。

解決策

 Seleniumには、このような場合の待機関数が用意されています。例えば、指定した画面項目が表示されるまで待機する「visibility_of_element_located()」という関数があります。
もっとも、この関数を使用するには、Googleマップ上で検索ボタンを押した時と、新しい地図が表示された時で、画面項目に変化が生じていることが前提になります。

ということで、画面の変化を調べるために、Googleマップを手動で操作して、住所を検索します。

Googleマップ(住所入力)
検索ボタンを押した瞬間
Googleマップ(地図表示)
新しい地図が表示された時

検索が完了すると、左側に領域が作成され、住所が表示されることが分かります。
「×ボタン」をクリックすると、領域が非表示になり、住所入力欄がクリアされます。

住所表示欄の表示/非表示で、通信の完了が判断できそうです。

XPathの取得

住所表示欄と「×ボタン」のXPathを取得します。

XPath取得ツール起動

XPath取得ツールで、XPathを取得/解析します。

Xpath取得ツールで住所表示欄のXpathを取得します
XPath取得ツール(住所表示欄の解析)

解析結果(候補1)に住所がそのまま編集されています。
住所はケースごとに異なるので、このXPathは利用できません。

XPath取得ツールは、XPath解析時に対象属性の設定ができます。text属性を除外して解析します。

Xpath取得ツールの設定で、Xpath解析対象からtext属性を除外します
設定でtext属性を除外

再度、解析ボタンを押します。

Xpath取得ツールで、住所表示欄のXpathをtext属性を除外して取得します
XPath取得ツール(text属性を除外して解析)

今度は、class属性でXPathが編集されました。

XPath検証ツール起動

住所表示欄を検証します。

Xpath検証ツールで住所表示欄のXpathを検証します
XPath検証ツール(住所表示欄を検証)

住所が取得できています。

同様に、XPath取得ツールで「×ボタン」のXPathを取得し、XPath検証ツールで実際にクリックできるかを検証します。

取得したXPath
項目 XPath
住所表示欄 //h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5″]
×ボタン //span[@id="sb_cb50″]

ちなみにChromeで住所表示欄のXPathを取得すると
 //*[@id="pane"]/div/div[1]/div/div/div[2]/div[1]/div[1]/h1
でした。

XPath検証結果
項目 操作 生成コード
住所 text入力 driver.find_element_by_xpath('//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]')
×ボタン クリック driver.find_element_by_xpath('//span[@id="sb_cb50"]').click()

待機処理の実装

説明や事前準備が多かったので、お忘れかもしれませんが、話を戻します。
スクリプトがエラーとなるのは、通信が完了する前に、Seleniumが地図画像を取得するのが原因でした。
通信の完了は、住所表示欄の表示/非表示で判断できます。

なので、住所を入力して検索ボタンをクリック後、住所表示欄が表示されるまでSeleniumが待機すればよいことになります。
そして、それを実現するのが「visibility_of_element_located()」という関数でした。

待機関数の使い方
#Selenium待機ライブラリ
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

#タイムアウト値を指定
wait=WebDriverWait(driver, 10)  #10秒

wait.until(expected_conditions.visibility_of_element_located((By.XPATH, 要素のXPath)))
(後続処理)

こうすることで、指定した要素が表示されるまで最大10秒間待機し、表示されると後続処理を行います。
なおタイムアウト値を超えた場合は、Seleniumが例外(TimeoutException)を発生させます。

スクリプトの修正

スクリプトの冒頭で、待機用ライブラリの読み込み、タイムアウト値の設定を行います。

#Selenium待機ライブラリ
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

#タイムアウト値を指定
wait=WebDriverWait(driver, 10)  #10秒

Googleマップで検索ボタンクリックした直後に、待機処理を入れます。

#検索ボタンクリック
driver.find_element_by_xpath('//button[@id="searchbox-searchbutton"]').click()
#地図が取得されるまで待機 wait.until(expected_conditions.visibility_of_element_located((By.XPATH,\
'//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]')))

# 画面キャプチャ driver.save_screenshot(r'tmp\map1.png')

住所表示領域のクリア

地図を取得後、左側の領域が表示されたままだと、次の地図を検索した時にすでに住所表示欄が表示されているので、Seleniumは待機しません。
検索前に、毎回「×ボタン」で住所表示欄を非表示にしておく必要があります。

Googleマップ(地図表示)

左側の領域が表示されていない場合(初回表示)「×ボタン」は非表示になるので、表示されている場合のみクリックします。

初回表示
#地図の取得
if driver.find_element_by_xpath('//span[@id="sb_cb50"]').is_displayed():    #「×ボタン」
    driver.find_element_by_xpath('//span[@id="sb_cb50"]').click()
driver.find_element_by_xpath('//input[@id="searchboxinput"]').send_keys(address)
driver.find_element_by_xpath('//button[@id="searchbox-searchbutton"]').click()

#地図が取得されるまで待機
wait.until(expected_conditions.visibility_of_element_located((By.XPATH,\
'//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]')))
# 画面キャプチャ driver.save_screenshot(r'tmp\map1.png')

修正スクリプト

修正を反映します。

# -*- coding:utf-8 -*-
import os
import datetime

# Seleniumライブラリ
from selenium import webdriver
#Selenium待機ライブラリ
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

# Excel操作ライブラリ
import xlwings as xw
# Word操作ライブラリ
from docx import Document
from docx.shared import Mm
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH

# 画像加工ライブラリ
from PIL import Image

# Chromeドライバ起動
driver_path = r'D:\SeleniumCodeChecker\driver\chromedriver.exe'
driver = webdriver.Chrome(executable_path=driver_path)
# 画面サイズ最大
driver.maximize_window()
#タイムアウト値を指定
wait=WebDriverWait(driver, 10)  #10秒
# Googleマップを開く【@緯度,経度,ズーム倍率z】
driver.get('https://www.google.co.jp/maps/@35.710067,139.8085117,17z?hl=ja')

# Excelオープン
book_path = r'excel\受付一覧表.xlsx'
wb1 = xw.Book(book_path)
ws1 = wb1.sheets[0]

#受付番号がある間、処理を繰り返す
i = 2
while ws1.cells(i, 1).value is not None:
    # Wordを開く
    document = Document()
    # ページ設定
    section = document.sections[0]
    # 用紙サイズ(A4タテ)
    section.page_width = Mm(210)
    section.page_height = Mm(297)
    # 余白(標準)
    section.top_margin = Mm(12.7)
    section.bottom_margin = Mm(12.7)
    section.left_margin = Mm(12.7)
    section.right_margin = Mm(12.7)

    # 作成日を出力
    paragraph = document.add_paragraph()
    paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    paragraph.add_run(str(datetime.date.today()))

    # タイトル
    heading = document.add_heading('訪問指示書', level=2)
    heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
    heading.runs[0].font.size = Pt(18)

    # 表の作成
    table = document.add_table(rows=7, cols=2, style='Table Grid')
    # 表の1行目から7行目まで繰り返す
    for j in range(1, 8):
        # !!Word表:0始まり(j-1)、Excel:1始まり(j)
        row = table.rows[j - 1]
        # セルの幅
        row.cells[0].width = Mm(30)
        row.cells[1].width = Mm(155)
        #項目の編集
        row.cells[0].text = str(ws1.cells(1, j).options\
                    (dates=datetime.date, numbers=int, empty='').value)  # 項目名
        row.cells[1].text = str(ws1.cells(i, j).options\
                    (dates=datetime.date, numbers=int, empty='').value)  # 項目内容

        # 後で使用する項目を変数で保持
        if ws1.cells(1, j).value == '住所':
            address = str(ws1.cells(i, j).value)
        if ws1.cells(1, j).value == '担当者':
            name = str(ws1.cells(i, j).value)
        if ws1.cells(1, j).value == '訪問希望日':
            day = str(ws1.cells(i, j).options(dates=datetime.date).value)
        if ws1.cells(1, j).value == '受付番号':
            no = str(ws1.cells(i, j).options(numbers=int).value)

    # 地図の取得
    if driver.find_element_by_xpath('//span[@id="sb_cb50"]').is_displayed():
        driver.find_element_by_xpath('//span[@id="sb_cb50"]').click()    # ×ボタン
    element = driver.find_element_by_xpath('//input[@id="searchboxinput"]')
    element.clear()
    element.send_keys(address)
    driver.find_element_by_xpath('//button[@id="searchbox-searchbutton"]').click()
    # 地図が取得されるまで待機
    wait.until(expected_conditions.visibility_of_element_located((By.XPATH, \
        '//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]')))
    # 画面キャプチャ
    driver.save_screenshot(r'tmp\map1.png')

    # 画像のトリミング
    im1 = Image.open(r'tmp\map1.png')
    im2 = im1.crop((520, 100, 1670, 895))  # 左, 上, 右, 下
    im2.save(r'tmp\map2.png', quality=90)  # quality:90%

    # 画像の貼り付け
    heading = document.add_heading('【周辺地図】', level=3)
    heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
    heading.runs[0].font.size = Pt(18)
    document.add_picture(r'tmp\map2.png', width=Mm(185))  # 幅185ミリ

    # 文書の保存
    filename = '訪問指示書(' + name + '_' + day + '_' + no + ').docx'
    document.save(os.path.join('word', filename))
    i += 1

スクリプトの実行

コマンドプロンプトから、改良したSeleniumスクリプトを実行します。

python gmap_tutrial3.py
Googleマップ画面

「×ボタン」をクリックした結果、住所表示エリアが隠れています。

Googleマップ画面

一番最後(エラー住所)のケースで例外が発生して、スクリプトが異常終了しました。

エラー発生

例外発生時の対応

エラーメッセージ
Traceback (most recent call last):
  File "gmap_tutrial3.py", line 98, in <module>
    '//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]')))
  File "C:\Program Files\Python37\lib\site-packages\selenium\webdriver\support\wait.py", line 80, in until
    raise TimeoutException(message, screen, stacktrace)
selenium.common.exceptions.TimeoutException: Message:

98行目、追加した、住所表示欄の表示を待機する部分でタイムアウトが発生しています。
visibility_of_element_located()関数は、指定時間内に要素が表示されないとTimeoutExceptionを発生させる仕様でした。

実際に画面を見ると、住所表示欄が表示されていません。
あり得ない住所を入れた場合、Googleマップは地図が検索できず、住所を出力しないようです。
もっとも、地図が検索できないこと自体は正しい動きです。
スクリプトが異常終了して、処理が続行できないことが問題なので、例外発生時のハンドリングを入れて解決します。

修正コード
try:
    # 地図が取得されるまで待機
    wait.until(expected_conditions.visibility_of_element_located((By.XPATH, \
        '//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]')))
    # 画面キャプチャ
    driver.save_screenshot(r'tmp\map1.png')
except:
    例外発生時の処理

もし、待機時に例外が発生すると、画面キャプチャは行われず、except句以下の処理が行われます。

正常ケースの対応

作成されたWord文書を確認します。

訪問指示書(正常ケース)

今回は正しい地図が取得できています。

訪問指示書(地図が不完全)

ところが、一部のケースで地図が不完全なものがあります。左側の部分で目印となる情報が読み込まれていません。

エラー原因

地図が読み込まれる条件として、住所表示欄で判定しましたが、完全に表示されるまで、どうやらタイムラグがあるようです。住所表示欄が表示されてから、さらに2秒ほど待機するようにします。

修正コード
#スクリプトの冒頭
import time
try:
    # 地図が取得されるまで待機
    wait.until(expected_conditions.visibility_of_element_located((By.XPATH, \
        '//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]')))
    time.sleep(2)
    # 画面キャプチャ
    driver.save_screenshot(r'tmp\map1.png')
except:
    例外発生時の処理

エラーケースの対応

10件目「1丁目1-99」の訪問指示書を見てみましょう。

訪問指示書(1丁目1-99)

99号地は存在しないため、1丁目1番地の地図が表示されています。
これでは正常なのかエラーなのか判断がつきません。

一方、Googleマップではこのように表示されます。

Googleマップ(田無町1丁目1-99)
Googleマップ(1丁目1-99)

住所表示欄は「1丁目1」までしか表示されていません。
このことから、住所表示欄の出力内容で正常/エラーの判定ができることが分かります。

そこで、Googleマップが出力する住所を、入力元の受付一覧表に記載して、住所が正しく検索できたかの確認ができるようにします。あわせて地図画像の取得結果を出力します。

受付一覧表(改訂版)

修正コード
try:
    # 地図が取得されるまで待機
    wait.until(expected_conditions.visibility_of_element_located((By.XPATH, \
        '//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]')))
    #住所表示欄の住所を保存
    gmap_add = driver.find_element_by_xpath( \
        '//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]').text
    time.sleep(2)
    # 画面キャプチャ
    flg = driver.save_screenshot(r'tmp\map1.png')    #戻り値:True/False
except:
    flg = False
if flg:
    # 画像のトリミング
    im1 = Image.open(r'tmp\map1.png')
    im2 = im1.crop((520, 100, 1670, 895))  # 左, 上, 右, 下
    im2.save(r'tmp\map2.png', quality=90)  # quality:90%

    # 画像の貼り付け
    heading = document.add_heading('【周辺地図】', level=3)
    heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
    heading.runs[0].font.size = Pt(18)
    document.add_picture(r'tmp\map2.png', width=Mm(185))  # 幅185ミリ

    #ファイル名の編集
    filename = '訪問指示書(' + name + '_' + day + '_' + no + ').docx'

    #Excelに結果を記入
    ws1.cells(i, 8).value = 'OK'
    ws1.cells(i, 9).value = gmap_add
else:
    #エラーケース
    filename = 'NG_' + name + '_' + day + '_' + no + '.docx'
    ws1.cells(i, 8).value = 'NG'
    

# 文書の保存
document.save(os.path.join('word', filename))

修正スクリプト

修正を反映します。
Excelのシート名を「受付一覧表(改訂版).xlsx」に変更しています。

# -*- coding:utf-8 -*-
import os
import time
import datetime

# Seleniumライブラリ
from selenium import webdriver
#Selenium待機ライブラリ
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

# Excel操作ライブラリ
import xlwings as xw
# Word操作ライブラリ
from docx import Document
from docx.shared import Mm
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH

# 画像加工ライブラリ
from PIL import Image

# Chromeドライバ起動
driver_path = r'D:\SeleniumCodeChecker\driver\chromedriver.exe'
driver = webdriver.Chrome(executable_path=driver_path)
# 画面サイズ最大
driver.maximize_window()
#タイムアウト値を指定
wait=WebDriverWait(driver, 10)  #10秒
# Googleマップを開く【@緯度,経度,ズーム倍率z】
driver.get('https://www.google.co.jp/maps/@35.710067,139.8085117,17z?hl=ja')

# Excelオープン
book_path = r'excel\受付一覧表(改訂版).xlsx'
wb1 = xw.Book(book_path)
ws1 = wb1.sheets[0]

#受付番号がある間、処理を繰り返す
i = 2
while ws1.cells(i, 1).value is not None:
    # Wordを開く
    document = Document()
    # ページ設定
    section = document.sections[0]
    # 用紙サイズ(A4タテ)
    section.page_width = Mm(210)
    section.page_height = Mm(297)
    # 余白(標準)
    section.top_margin = Mm(12.7)
    section.bottom_margin = Mm(12.7)
    section.left_margin = Mm(12.7)
    section.right_margin = Mm(12.7)

    # 作成日を出力
    paragraph = document.add_paragraph()
    paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    paragraph.add_run(str(datetime.date.today()))

    # タイトル
    heading = document.add_heading('訪問指示書', level=2)
    heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
    heading.runs[0].font.size = Pt(18)

    # 表の作成
    table = document.add_table(rows=7, cols=2, style='Table Grid')
    # 表の1行目から7行目まで繰り返す
    for j in range(1, 8):
        # !!Word表:0始まり(j-1)、Excel:1始まり(j)
        row = table.rows[j - 1]
        # セルの幅
        row.cells[0].width = Mm(30)
        row.cells[1].width = Mm(155)
        #項目の編集
        row.cells[0].text = str(ws1.cells(1, j).options\
                    (dates=datetime.date, numbers=int, empty='').value)  # 項目名
        row.cells[1].text = str(ws1.cells(i, j).options\
                    (dates=datetime.date, numbers=int, empty='').value)  # 項目内容

        # 後で使用する項目を変数で保持
        if ws1.cells(1, j).value == '住所':
            address = str(ws1.cells(i, j).value)
        if ws1.cells(1, j).value == '担当者':
            name = str(ws1.cells(i, j).value)
        if ws1.cells(1, j).value == '訪問希望日':
            day = str(ws1.cells(i, j).options(dates=datetime.date).value)
        if ws1.cells(1, j).value == '受付番号':
            no = str(ws1.cells(i, j).options(numbers=int).value)

    # 地図の取得
    if driver.find_element_by_xpath('//span[@id="sb_cb50"]').is_displayed():
        driver.find_element_by_xpath('//span[@id="sb_cb50"]').click()   #×ボタン
    element = driver.find_element_by_xpath('//input[@id="searchboxinput"]')
    element.clear()
    element.send_keys(address)
    driver.find_element_by_xpath('//button[@id="searchbox-searchbutton"]').click()
    try:
        # 地図が取得されるまで待機
        wait.until(expected_conditions.visibility_of_element_located((By.XPATH, \
            '//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]')))
        # 住所表示欄の住所を保存
        gmap_add = driver.find_element_by_xpath( \
            '//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]').text
        time.sleep(2)
        # 画面キャプチャ
        flg = driver.save_screenshot(r'tmp\map1.png')  # 戻り値:True/False
    except:
        flg = False
    if flg:
        # 画像のトリミング
        im1 = Image.open(r'tmp\map1.png')
        im2 = im1.crop((520, 100, 1670, 895))  # 左, 上, 右, 下
        im2.save(r'tmp\map2.png', quality=90)  # quality:90%

        # 画像の貼り付け
        heading = document.add_heading('【周辺地図】', level=3)
        heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
        heading.runs[0].font.size = Pt(18)
        document.add_picture(r'tmp\map2.png', width=Mm(185))  # 幅185ミリ

        # ファイル名の編集
        filename = '訪問指示書(' + name + '_' + day + '_' + no + ').docx'

        # Excelに結果を記入
        ws1.cells(i, 8).value = 'OK'
        ws1.cells(i, 9).value = gmap_add
    else:
        # エラーケース
        filename = 'NG_' + name + '_' + day + '_' + no + '.docx'
        ws1.cells(i, 8).value = 'NG'

    # 文書の保存
    document.save(os.path.join('word', filename))
    i += 1

スクリプトの実行

コマンドプロンプトから、修正したSeleniumスクリプトを実行します。

python gmap_tutrial4.py

例外で異常終了することなく、スクリプトが完了しました。

スクリプト実行結果

wordフォルダの中を確認します。

作成されたWord文書

ファイルを開きます。
(全選択してEnterキーを押せば、まとめて開くことができます)

訪問指示書(実行結果)
PDF:11ページ(画面に入りきらない場合はブラウザの表示倍率を下げることをお勧めします)

続いて受付一覧表を確認します。

地図が検索エラーとなった場合は、取得結果がNGとなっています。
地図が取得できた場合も、Googleマップの住所表示が記載されていますので、正常/エラーの判定が可能となります。

なお編集結果から分かるように、Googleマップの住所は「〇丁目〇-○」で統一されているようです。

フォントの変更

ほぼ完成ですが、タイトルと見出し(【周辺地図】)が青色になっていますので黒にします。

フォントカラー変更

フォントカラーの設定はRGBで設定します。

#ライブラリのインポート
from docx.shared import RGBColor

#フォントカラーの変更
run.font.color.rgb = RGBColor(0, 0, 0)    #黒
書体の変更

デフォルトはMSゴシックが指定されています。このままでも構いませんが、あえてMS Pゴシックにしたいと思います。

こちらを参考にさせていただきました。
pythonのメモ

#ライブラリのインポート
from docx.oxml.ns import qn

#フォントの変更
run.font.name = "MS Pゴシック"
run._element.rPr.rFonts.set(qn('w:eastAsia'), run.font.name)

完成スクリプト

スクリプトに変更を反映させます。

# -*- coding:utf-8 -*-
import os
import time
import datetime

# Seleniumライブラリ
from selenium import webdriver
#Selenium待機ライブラリ
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

# Excel操作ライブラリ
import xlwings as xw
# Word操作ライブラリ
from docx import Document
from docx.shared import Mm
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.shared import RGBColor

# 画像加工ライブラリ
from PIL import Image

# Chromeドライバ起動
driver_path = r'D:\SeleniumCodeChecker\driver\chromedriver.exe'
driver = webdriver.Chrome(executable_path=driver_path)
# 画面サイズ最大
driver.maximize_window()
#タイムアウト値を指定
wait=WebDriverWait(driver, 10)  #10秒
# Googleマップを開く【@緯度,経度,ズーム倍率z】
driver.get('https://www.google.co.jp/maps/@35.710067,139.8085117,17z?hl=ja')

# Excelオープン
book_path = r'excel\受付一覧表(改訂版).xlsx'
wb1 = xw.Book(book_path)
ws1 = wb1.sheets[0]

#受付番号がある間、処理を繰り返す
i = 2
while ws1.cells(i, 1).value is not None:
    # Wordを開く
    document = Document()
    # ページ設定
    section = document.sections[0]
    # 用紙サイズ(A4タテ)
    section.page_width = Mm(210)
    section.page_height = Mm(297)
    # 余白(標準)
    section.top_margin = Mm(12.7)
    section.bottom_margin = Mm(12.7)
    section.left_margin = Mm(12.7)
    section.right_margin = Mm(12.7)

    # 作成日を出力
    paragraph = document.add_paragraph()
    paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    paragraph.add_run(str(datetime.date.today()))

    # タイトル
    heading = document.add_heading('訪問指示書', level=2)
    heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = heading.runs[0]
    run.font.size = Pt(18)
    run.font.color.rgb = RGBColor(0, 0, 0)
    run.font.name = "MS Pゴシック"
    run._element.rPr.rFonts.set(qn('w:eastAsia'), run.font.name)

    # 表の作成
    table = document.add_table(rows=7, cols=2, style='Table Grid')
    # 表の1行目から7行目まで繰り返す
    for j in range(1, 8):
        # !!Word表:0始まり(j-1)、Excel:1始まり(j)
        row = table.rows[j - 1]
        # セルの幅
        row.cells[0].width = Mm(30)
        row.cells[1].width = Mm(155)
        #項目の編集
        row.cells[0].text = str(ws1.cells(1, j).options\
                    (dates=datetime.date, numbers=int, empty='').value)  # 項目名
        row.cells[1].text = str(ws1.cells(i, j).options\
                    (dates=datetime.date, numbers=int, empty='').value)  # 項目内容

        # 後で使用する項目を変数で保持
        if ws1.cells(1, j).value == '住所':
            address = str(ws1.cells(i, j).value)
        if ws1.cells(1, j).value == '担当者':
            name = str(ws1.cells(i, j).value)
        if ws1.cells(1, j).value == '訪問希望日':
            day = str(ws1.cells(i, j).options(dates=datetime.date).value)
        if ws1.cells(1, j).value == '受付番号':
            no = str(ws1.cells(i, j).options(numbers=int).value)

    # 地図の取得
    if driver.find_element_by_xpath('//span[@id="sb_cb50"]').is_displayed():
        driver.find_element_by_xpath('//span[@id="sb_cb50"]').click()   #×ボタン
    element = driver.find_element_by_xpath('//input[@id="searchboxinput"]')
    element.clear()
    element.send_keys(address)
    driver.find_element_by_xpath('//button[@id="searchbox-searchbutton"]').click()
    try:
        # 地図が取得されるまで待機
        wait.until(expected_conditions.visibility_of_element_located((By.XPATH, \
            '//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]')))
        # 住所表示欄の住所を保存
        gmap_add = driver.find_element_by_xpath( \
            '//h1[@class="section-hero-header-title-title GLOBAL__gm2-headline-5"]').text
        time.sleep(2)
        # 画面キャプチャ
        flg = driver.save_screenshot(r'tmp\map1.png')  # 戻り値:True/False
    except:
        flg = False
    if flg:
        # 画像のトリミング
        im1 = Image.open(r'tmp\map1.png')
        im2 = im1.crop((520, 100, 1670, 895))  # 左, 上, 右, 下
        im2.save(r'tmp\map2.png', quality=90)  # quality:90%

        # 画像の貼り付け
        heading = document.add_heading('【周辺地図】', level=3)
        heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = heading.runs[0]
        run.font.size = Pt(18)
        run.font.color.rgb = RGBColor(0, 0, 0)
        run.font.name = "MS Pゴシック"
        run._element.rPr.rFonts.set(qn('w:eastAsia'), run.font.name)
        document.add_picture(r'tmp\map2.png', width=Mm(185))  # 幅185ミリ

        # ファイル名の編集
        filename = '訪問指示書(' + name + '_' + day + '_' + no + ').docx'

        # Excelに結果を記入
        ws1.cells(i, 8).value = 'OK'
        ws1.cells(i, 9).value = gmap_add
    else:
        # エラーケース
        filename = 'NG_' + name + '_' + day + '_' + no + '.docx'
        ws1.cells(i, 8).value = 'NG'

    # 文書の保存
    document.save(os.path.join('word', filename))
    i += 1

スクリプトの実行

コマンドプロンプトでスクリプトを実行します。

python gmap_turrial5.py

動画

実行結果

受付一覧表

訪問指示書

見出しのフォントが「MS Pゴシック-黒」になっていることを確認します。

訪問指示書d1
PDF:11ページ(画面に入りきらない場合はブラウザの表示倍率を下げることをお勧めします)

考察、評価

考察

今回はデバッグ作業(スクリプトの修正)が多かったです。4回修正することとなりました。

修正内容

  1. 受付一覧表から取得した値のうち、受付番号が小数、訪問希望日が日付+時刻、空白セルが「None」で編集される。
    ⇒取得時にオプションを指定して回避。
  2. Googleマップで検索ボタンを押した直後の地図が取得される。
    ⇒住所が表示されるまで待機して、通信が完了してから地図を取得する。
  3. 地図が検索できない場合に、例外が発生してに異常終了する。
    ⇒例外発生時のハンドリングを行い、エラー扱いとする。
  4. 存在しない住所(1丁目1-99)でも、地図が検索できた場合、画像を取得するため正常/エラーの判断ができない。
    ⇒住所表示欄の住所で判定を行う。
  5. タイトル、見出しのフォントを変更

原因

1. Excel用ライブラリ(xlwings)の仕様によるもので、xlwingsには変換オプションが用意されていました。
2. GoogleマップがAjaxという技術を用いているため、通信の完了を待たずにSeleniumが検索結果を取得します。Seleniumに用意されている待機関数を用いて、通信完了を待つようにしました。
3. Googleマップで地図が検索できない場合は、住所表示欄が非表示になり例外が発生します。例外のハンドリングを追加しました。
4. 地図画像からは判断できないため、検索欄に入力した住所とGoogleマップが返す住所を比較することで、エラー判定することにしました。
5. Word用ライブラリ(python-docx)の仕様によるもので、別途フォントを指定する必要がありました。

考察1(Pythonライブラリ)

1と5はPythonライブラリの問題です。

Excelライブラリ(xlwings)は、前回の「Yahoo!路線案内を自動化」でも使用しましたが、想定したデータ形式にならないという事象は今回初めて発生しました。。幸いオプション指定するだけで解決しました。

Wordライブラリ(python-docx)については、今回初めて使用しました。
文章や表の編集など、基本的な機能は備えているようですが、日本語フォントの指定はやや煩雑でした。

少し試してみたところ

印刷の向きを指定する場合は、用紙サイズも横向きにする必要がある
from docx.enum.section import WD_ORIENT
#A4横
section.page_width = Mm(297)
section.page_height = Mm(210)
section.orientation = WD_ORIENT.LANDSCAPE

といったこともあり、Excelよりも使いにくいかな?という印象を持ちました。

考察2(Googleマップ)

2,3,4はGoogleマップを使用したために起きた問題です。

Ajaxを用いたサイトでは、ブラウザは通信の完了を待ちませんので、新しい地図が表示される前にSeleniumが画像を取得します。
これを避けるためには、自分で通信が完了する条件を見つけて、シナリオに組み込む必要があります。

正常/エラーの判定についても、画像でエラーを判定することは不可能なので、代わりに住所表示の内容を見て判断する必要がありました。

これらの部分はGoogleマップの動作に依存しますので、WinActorやUiPathなどの商用ソフトであっても変わらないと思います。

評価

前回にならい「1.Seleniumの性能評価」と「2.自動操作で業務を置き換えることができるか」の2点で評価を行います。

1.SeleniumはRPAソフトとして使えるか?

今回、Excelの一覧表から個別のWord文書を作成する処理を行いました。

  • Word文書を新規作成
  • Excelシートの内容をWordに出力
  • 住所からGoogleマップを検索
  • 地図画像を取得
  • 取得した画像を切り抜き(トリミング)してWordに貼り付け

これらについて、問題なくSeleniumuで自動操作できました。

RPAソフトとして必要な性能は発揮できたと思います。

2.自動操作は業務を置き換えるか?

一連の操作に加え、

  • Googleマップで検索エラーとなった場合のハンドリング
  • 存在しない住所の場合、一覧表の出力情報にてエラー判定が可能

Googleマップの仕様によるものですが、

  • 住所入力で、「1丁目1番1号」「1丁目1-1」「1-1-1」それぞれに対応。

ファイル名に「担当者」と「訪問日」を含めることで、

  • 担当者ごとにファイルを並べることができ、メール送信時に便利
  • 担当者単位では、訪問日ごとに並んでいるので、作業計画を立てやすい

というようなメリットがあります。

今回は、正しく地図が検索できていない場合、Googleマップが出力する住所から判断できる点が大きいと思います。それがなければ、地図画像を1枚1枚、人間の目で判断する必要があります。

結論としては、業務での使用に耐えうると判断してよいでしょう。