限りなき知の探訪

45年間、『知の探訪』を続けてきた。いま座っている『人類四千年の特等席』からの見晴らしをつづる。

百論簇出:(第279回目)『シニア・エンジニアのPython事始(その5)』

2024-04-14 09:04:10 | 日記
前回

長年GmailをメインのEmailとして使ってきた。その理由は、私のように、所属がしばしば替わるような状況では、退職した途端に、組織の独自のメールボックスに入っているデータはアクセスできなくなるので、どこからでもアクセスできるGmailのようなツールは使い勝手がよいからだ。

これに加えて、Gmailはつい最近まで、簡易形式のメールシステムを提供してくれていたのも長年使っていた理由の一つだ。簡易形式の利点は、とにかく、立ち上げが早いことだ。それに加え、私のように基本的にデータを自分のHD(オンプレミス)に格納するための必要データ(発信者メールアドレス、CC受信者メールアドレス、本文など)が簡単に取れたからである。

簡単というのはGmailの受信画面全体をコントロールA+コントロールCで、バッファーにいれて、それをテキストエディターに吐き出し、それを一旦格納してから、固定不要文字部分を取り除くとメールデータが格納できるからだ。言葉で説明すると長たらしく何だか難しそうに聞こえるかもしれないがが、Windowsのバッチコマンドで処理すると、1本あたり僅か数秒で処理できる(というか、できていた。)

ところが、昨年(2023年)の暮(12月)ごろから、簡易形式は廃止するという警告がGoogleから何度も出されていた。無視して使い続けていたが、とうとう最近、2024年3月になって強制的に簡易Gmailから標準Gmailに切り替わってしまった。標準Gmailに切替したくなかったのは、上述のテクニック、つまり、「受信画面全体をコントロールA+コントロールCで、バッファーにいれて、それをテキストエディターに吐き出す」とメールの一部の情報が欠落するからだ。具体的には、送り先のメールアドレスが脱落し、送信者の名前しか分からない。さらに悪いことにCCのアドレスもとれない。そうなると、後になってメールを見返して、送信者に連絡を取ろうとしても、オンプレミスに蓄積されてデータからでは不可能で、Gmailのデータにアクセスしないといけない。一般のGmail契約では数年経つとメールは規定バイト数を超過して古いメールは削除されてしまうであろう。

この問題を解決する方法としては、少なくとも次の2つの方法が考えられる。
1.かつての簡易Gmailのようなメールサービスを見つけ、Gmailを転送し、転送されたデータから必要情報を取り出して保存する。
2.Gmailのデータから直接、送信メールアドレス、CCメールアドレスを取り出す。

今回、Pythonの本やWeb情報を見ていると2.の方法で、私の望む処理が出来そうなことが分かった。

ところが、いざ始めてみるとWeb上の記事のタイトルにもなっているが、
 「Gmail API を使ってメールを取得すると結構大変だった話(前編)」
とまさに同じ状態に陥った。そう、Gmailのデータを取り出すのは、そう簡単ではないのだ。

技術的に正しいか自信はないが推測で言うと、Gmailのデータを取り出すのが難しいのは通常のPOP3のコマンドではGmailの受信データにはアクセスできないからだ。Gmailのセキュリティが堅いのである。Gmailのメールボックスにアクセスするには、予め認証(Certificate)の token を取得しておく必要がある。このtokenを持っているユーザーだけがアクセスできる。このtoken を得るまでが一苦労であったが、何とか達成した。

次の難関は、Gmailだけでなく、現在のメールシステムというのはどうやらスパゲッティらしく、本文の場所が、何ヶ所かに別れているという。Web上のPythonコードを実行させてみると、本文の取り出しが場合によっては中途半端で終わってしまうようなこともしばしばあった。そうこうして、20ヶ所近くのWebサイトのPythonコードをチェックして、ようやく私が所望する処理ができるコードを次のサイトで見つけた。
PythonでGmailを取得し、件名や受信日をPandasを用いてExcelに書き込む方法
これを実装すると、ドンピシャ動いた。ただし、私の要望に合わせて、エクセルへの格納部分や添付ファイル取得部分などは削除した上に、若干変更したものを、以下に示す。

このコードでは、送信者データ、CCデータ、本文など、基本部分はきちんと取り出せたのであるが、いろいろと改造した。
1.HTMLファイルは、全体ではなく、文字部分のみを抽出する。
2.蓄積データの文字コードをUTF-8からShift-JISに変換する。
3.UTF-8 からShift-JISに変換するときに、Windowsの改行マーク(0x0d+0x0a)に 0x0d が1つ分余計に加わり、0x0d+0x0d+0x0a となるので、Windowsの平文に変換すると空白行が1行余計に出来てしまう。それで0x0d を1つ削除する。

 ************

今回は、Gmail の方針の変更で、簡易形式が使えなくなったので、その代用のシステムをWeb上の情報を基にして、すきま時間を使って20日ばかりで作成できた。(真剣にすれば数日の話かもしれないが。)この過程で、つくづく数十年前であれば、到底一人では、数ヶ月はかかったのではなかろうか、と感じた。私は以前にプロのシステムエンジニア・ソフトウェアエンジニアとして仕事をしていたので、このようなシステムの構築の難しさをよく分かっている。つまり、メールシステムのような複雑でスパゲティのシステムからデータを取り出すには仕様の細部まで十分に理解していないといけない。そのためには、かなりの量のドキュメントを丹念に読まないといけない。それには、数人(あるいは数十人)が取り組んで少しづつ理解している事柄を集積するのが一番の早道だ。ソフトウェアハウスでは、プロのソフトウェアエンジニアが周りに数多くいるので、聞いて回ることができるが、私のように一人で仕事をしている人間にはそういう情報入手ルートが全くない。それを救ってくれるどころか、それよりも幾倍も有益な情報を与えてくれる環境がWebだ。

私はプログラミング言語としては、Python自体は、別に好きでも嫌いでもない。それで、客観的立場から評価できる。PythonはWeb上に掲載される情報の多さとモジュールの豊富さで他の言語を遥かに凌駕している(と感じる)。それゆえ、Python は現代人が真剣に取り組むべき言語だと考えている。
     ----------------------------

# -*- coding: Shift-JIS -*-
## Gmail データを取得する。
## 基本構造は、下記のサイトのコードを使った。
## https://teratail.com/questions/8wl34hy9x3iwi8

from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
import json
import base64
import pandas as pd
import sys

def get_header(headers, name):
  for h in headers:
    if h['name'].lower() == name:
      return h['value']

def base64_decode(data):
  return base64.urlsafe_b64decode(data).decode()

def base64_decode_file(data):
  return base64.urlsafe_b64decode(data.encode('UTF-8'))

def get_body(body):
  if body['size'] > 0:
    return base64_decode(body['data'])


def get_parts_body(body):
  if (body['size'] > 0
      and 'data' in body.keys()
      and 'mimeType' in body.keys()
      and body['mimeType'] == 'text/plain'):
    return base64_decode(body['data'])

def get_parts(parts):
  for part in parts:
    if part['mimeType'] == 'text/plain':
      b = base64_decode(part['body']['data'])
      if b is not None:
        return b
    if 'body' in part.keys():
      b = get_parts_body(part['body'])
      if b is not None:
        return b
    if 'parts' in part.keys():
      b = get_parts(part['parts'])
      if b is not None:
        return b

def get_attachment_id(parts):
  for part in parts:
    if part['mimeType'] == 'image/png':
      return part['body']['attachmentId'], 'png'
  return None, None



### ?100
def main():
  if len(sys.argv) < 1:
    print("gmail_getm.bat ");

  dir_gmaildata = "c:\xxx";
  ss_tmplst = dir_gmaildata +"tmplst.lst"
  ffout =open(ss_tmplst, "w", encoding='utf-8');

  dir_dosbin= "d:\yyy";
  tokenPath = dir_dosbin + "token.json"
  Read_maxResults=30; 

  scopes = ['https://mail.google.com/']
  creds = Credentials.from_authorized_user_file(tokenPath, scopes)
  service = build('gmail', 'v1', credentials=creds)
  output = []
  messages = service.users().messages().list(
    userId='me',
    labelIds='INBOX',
    maxResults=Read_maxResults,
    ).execute().get('messages')

  msg_counter = 0;  
  for message in messages:
    msg_counter += 1;
    print(" ");
    ss_tmptxt = dir_gmaildata + "tmp%02d.txt" %msg_counter;
    ff_tmptxt = open(ss_tmptxt, "w", encoding='utf-8');

    ffout.write("\n==== File [ aa%02d.txt ] ====\n"  %msg_counter);
    m_data = service.users().messages().get(
      userId='me',
      id=message['id'],
    ).execute()

    # ヘッダー情報
    headers = m_data['payload']['headers']

    # 日付
    message_date = get_header(headers, 'date')
    print(f'Date: {message_date}')
    ffout.write( f'Date: {message_date}' + '\n');
    ff_tmptxt.write( f'Date: {message_date}' + '\n');

    # 差出人
    from_date = get_header(headers, 'from')
    print(f'From: {from_date}')
    ffout.write( f'From: {from_date}' + '\n');
    ff_tmptxt.write( f'From: {from_date}' + '\n');

    # 宛先
    to_date = get_header(headers, 'to')
    print(f'To: {to_date}')
    ffout.write( f'To: {to_date}' + '\n');
    ff_tmptxt.write( f'To: {to_date}' + '\n');

    # 宛先 -- CC
    cc_date = get_header(headers, 'cc')
    print(f'Cc: {cc_date}')
    ffout.write( f'Cc: {cc_date}' + '\n');
    ff_tmptxt.write( f'Cc: {cc_date}' + '\n');

    # 件名
    sub_date = get_header(headers, 'subject')
    print(f'Subject: {sub_date}')
    ffout.write( f'Subject: {sub_date}' + '\n');
    ff_tmptxt.write( f'Subject: {sub_date}' + '\n');

    body = m_data['payload']['body']
    body_data = get_body(body)

    parts_data = None
    if 'parts' in m_data['payload'].keys():
      parts = m_data['payload']['parts']
      parts_data = get_parts(parts)

    body_result = body_data if body_data is not None else parts_data
    ff_tmptxt.write(str(body_result).rstrip());
    ff_tmptxt.close();

  ffout.close();
    
if __name__ == '__main__':
   main()
</code>

     ----------------------------
続く。。。
コメント    この記事についてブログを書く
  • Twitterでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする
« 智嚢聚銘:(第53回目)『中... | トップ | 智嚢聚銘:(第54回目)『中... »
最新の画像もっと見る

コメントを投稿

日記」カテゴリの最新記事