[PYTHON] J'ai automatisé l'estampillage de Job Can avec du sélénium et l'ai déployé sur Google Cloud Functions, mais c'était assez difficile

introduction

J'ai utilisé du sélénium pour automatiser l'estampage des boîtes de travail et l'ai déployé sur Google Cloud Functions. Je vais décrire les points auxquels j'étais accro à ce moment-là.

Auto-introduction facile

A travaillé chez SIer à Tokyo Ingénieur RPA → Prise en charge de l'introduction PoC de solutions IA, etc.

Je fais aussi Twitter ↓ https://twitter.com/m_schna2

Représentation schématique

Il a la configuration suivante. Lorsque je l'ai essayé, il a fallu un certain temps pour associer Google Drive et Cloud Functions. フロー

Code entier

L'URL id pw est intégrée, mais veuillez la modifier en conséquence.

main.py


from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.chrome.options import Options
import jpholiday
import datetime
import requests, json
import os, time
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
import shutil

def punchClockWrapper(request):
  def isBizDay():
    dt_now = datetime.datetime.now()
    dt_now = utc_to_jst(dt_now)
    DATE = dt_now.strftime('%Y%m%d')
    Date = datetime.date(int(DATE[0:4]), int(DATE[4:6]), int(DATE[6:8]))
    if Date.weekday() >= 5 or jpholiday.is_holiday(Date):
      return 0
    else:
      return 1

  def punchClock():
    #constant
    url_jobcan = 'https://id.jobcan.jp/users/~~~~~~~~~~'
    id = '~~~~~~~~~~~~~'
    pw = '~~~~~~~~~~~~~'

    #Paramétrage des options
    options = Options()
    options.add_argument('--headless')
    options.add_argument('--disable-gpu')
    options.add_argument('--window-size=1280x1696')
    options.add_argument('--no-sandbox')
    options.add_argument('--hide-scrollbars')
    options.add_argument('--enable-logging')
    options.add_argument('--log-level=0')
    options.add_argument('--v=99')
    options.add_argument('--single-process')
    options.add_argument('--ignore-certificate-errors')

    #Fonctionnement backend de l'interface graphique
    options.binary_location = os.getcwd() + "/headless-chromium"    
    driver = webdriver.Chrome(os.getcwd() + "/chromedriver", options=options) #Chargez le pilote qui exécute Chrome

    #Ouvrir Job Can
    driver.get(url_jobcan)

    #Entrée e-mail
    text = driver.find_element_by_id("user_email")
    text.send_keys(id)

    #saisie du mot de passe
    text = driver.find_element_by_id("user_password")
    text.send_keys(pw)

    #Cliquez sur Connexion
    btn = driver.find_element_by_name("commit")
    btn.click()

    #Cliquez sur le bouton tampon
    btn = driver.find_element_by_id("adit-button-push")
    btn.click()

    #Attendez 8 secondes
    time.sleep(8)

    #ferme la fenêtre
    driver.quit()

  def toSlack():
    WEB_HOOK_URL = "https://hooks.slack.com/services/~~~~~~~~~"
    dt_now = datetime.datetime.now()
    dt_now = utc_to_jst(dt_now)
    dt_punch = dt_now.strftime('%Y année%m mois%jour j%H:%M:%S')
    requests.post(WEB_HOOK_URL, data = json.dumps({
      'text': str(dt_punch)+u'Estampage terminé',  #Contenu de la notification
      'username': '~~~~~~',  #Nom d'utilisateur
      'icon_emoji': u':smile_cat:',  #icône
      'link_names': 1,  #Noms de lien
    }))

  def utc_to_jst(timestamp_utc):
    timestamp_jst = timestamp_utc.astimezone(datetime.timezone(datetime.timedelta(hours=+9)))
    return timestamp_jst

  def writeLog(message):
    dt_now = datetime.datetime.now()
    dt_now = utc_to_jst(dt_now)
    dt_punch = dt_now.strftime('%Y année%m mois%jour j%H:%M:%S')
    with open(log_path, 'a') as f:
      print(str(dt_punch)+u'  '+message, file=f)

  def downloadLog():
    f = drive.CreateFile(drive_param)
    f.GetContentFile(log_path)

  def uploadLog():
    f = drive.CreateFile(drive_param)
    f.SetContentFile(log_path)
    f.Upload()

  #Réglage constant
  log_path = "/tmp/punch_clock.log"
  drive_param = {
    'parents': [{
        'id':'~~~~~~~~~~~'
    }],
    'id': '~~~~~~~~~~~',
    'title': 'punch_clock.log'
  }
  #déplacer dir(settings.Puisque yaml est lu à partir du répertoire d'exécution, la valeur par défaut est le répertoire actuel)
  #Doit être stocké dans un répertoire doté d'autorisations de lecture et d'écriture
  os.chdir(os.path.dirname(os.path.abspath(__file__)))
  shutil.copy2("credentials.json","/tmp/credentials.json")
  #Authentification
  gauth = GoogleAuth()
  gauth.CommandLineAuth()
  drive = GoogleDrive(gauth)
  #Sous le traitement principal
  downloadLog()#Télécharger les journaux depuis Google Drive
  writeLog('Commencer le traitement') #Démarrer le journal
  flg = isBizDay() #Jugement en semaine(Jours de semaine: 1, jours fériés: 0)
  if flg == 1 :
    punchClock() #Estampillage
    toSlack() #Notification Slack
    writeLog('Estampage terminé') #Fin du journal
  else :
    writeLog('Je ne l'ai pas tamponné car c'est un jour férié') #Fin du journal
  uploadLog() #Télécharger les journaux sur Google Drive
  return 'Terminé'

Points lisses

selenium Il s'agit du processus d'estampage principal (Punch Clock). Dans le code, je crains d'attendre 8 secondes après le marquage, mais je n'ai jamais manqué de tamponner, donc c'est probablement normal.

Notification Slack

Notifier à l'aide de l'API webhook. Il y a des étapes pour les paramètres d'API à l'URL suivante. https://slack.com/intl/ja-jp/help/articles/115005265063-Slack-%E3%81%A7%E3%81%AE-Incoming-Webhook-%E3%81%AE%E5%88%A9%E7%94%A8#incoming-webhook-u12398u35373u23450

Le point difficile

Déployer sur Google Cloud Functions (GCF)

En ce qui concerne la méthode de déploiement, mettez tout le code et les modules de bibliothèque nécessaires dans le même dossier et téléchargez avec le code suivant, etc.

gcloud functions deploy punchClockWrapper --runtime python37 --trigger-http --region asia-northeast1 --memory 512MB

Ici, reportez-vous à l'URL suivante. https://blowup-bbs.com/gcp-cloud-functions-python3/

Ajouter un journal à Google Drive (à l'aide de PyDrive)

Pour ajouter un journal à Google Drive Téléchargez le journal précédent → Ajouter → Télécharger Cependant, étant donné que Google Drive gère par identifiant de fichier, même si vous importez avec le même nom, il sera téléchargé en tant que fichier distinct avec le même nom au lieu d'être écrasé par défaut. Par conséquent, il peut être écrasé en spécifiant l'ID du dossier parent et l'ID du fichier journal, mais l'ID du fichier journal ne peut-il pas être obtenu par une opération GUI normale? J'ai tourné le code ci-dessous pour vérifier l'identifiant.

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
import pprint

#Authentification
gauth = GoogleAuth()
gauth.CommandLineAuth()
drive = GoogleDrive(gauth)

#acquisition de données de fichier
file_id = drive.ListFile({'q':'title="punch_clock.log"'}).GetList()[0]['id']
f = drive.CreateFile({'id':file_id})

#Affichage des métadonnées du fichier
f.FetchMetadata()
pprint.pprint(f)

En passant, vous pouvez vérifier l'identifiant du dossier à partir de la partie url en l'ouvrant avec un navigateur. Vous pouvez vérifier la méthode sur le lien ci-dessous. https://tonari-it.com/gas-google-drive-file-folder-id/

Combinaison de PyDrive et GCF

PyDrive est une bibliothèque d'encapsuleurs pour l'API Google Drive. Au début, une authentification interactive est requise, mais json est créé après l'authentification réussie, et le json est référencé à partir de la deuxième fois, de sorte qu'il peut être incorporé dans le code pour automatiser Google Drive. Il y a peu de situations de code et c'est très soigné.

#Authentification
gauth = GoogleAuth()
gauth.CommandLineAuth()
drive = GoogleDrive(gauth)

C'est juste le comportement au moment de l'authentification Voir settings.yaml dans le répertoire courant → Reportez-vous au chemin spécifié dans save_credentials_file → Si json existe, l'authentification réussit et écrase json. Sinon, l'authentification est demandée avec du code et si du code est entré, json est créé dans le même chemin spécifié. Il faut être conscient de ce point.

Bien sûr, vous ne pouvez pas vous authentifier de manière interactive après le déploiement sur GCF. Je m'authentifie donc localement à l'avance, crée un fichier json et le stocke ensemble dans le dossier à déployer, mais dans GCF, le répertoire inscriptible est limité à / tmp. Et comme json est écrit (écrasé) même lorsque l'authentification est réussie, vous devez spécifier le chemin /tmp/~~~.json dans save_credentials_file. Ainsi, dans le code (sous le code), credentials.json est copié du répertoire de déploiement vers / tmp. Et dans les paramètres.yaml déployés ensemble, /tmp/credentials.json est spécifié dans l'élément save_credentials_file. À propos, le fichier settings.yaml de PyDrive doit être dans le répertoire courant, il a donc été déplacé vers le répertoire d'exécution du fichier.

#déplacer dir(settings.Puisque yaml est lu à partir du répertoire d'exécution, la valeur par défaut est le répertoire actuel)
#Doit être stocké dans un répertoire doté d'autorisations de lecture et d'écriture
os.chdir(os.path.dirname(os.path.abspath(__file__)))
shutil.copy2("credentials.json","/tmp/credentials.json")

Par défaut, l'heure est désactivée

Étant donné que la région par défaut du travailleur démarré par GCF est us-central1, qui est à 9 heures de congé de l'heure du Japon, l'heure décrite dans la sortie du journal et la notification Slack est corrigée par la fonction suivante. (J'aurais dû me déployer avec asia-north1 cette fois, mais ça change encore pour une raison quelconque ...)

def utc_to_jst(timestamp_utc):
  timestamp_jst = timestamp_utc.astimezone(datetime.timezone(datetime.timedelta(hours=+9)))
  return timestamp_jst

Tâches futures

--embed id, pw, url C'est une faible priorité, mais j'aimerais l'améliorer un jour.

URL de référence

https://stackoverflow.com/questions/51487885/tmp-file-in-google-cloud-functions-for-python https://blowup-bbs.com/gcp-cloud-functions-python3/ https://dev.classmethod.jp/articles/python-time-string-timezone/ https://qiita.com/wezardnet/items/34b89276cdfc66607727

Recommended Posts

J'ai automatisé l'estampillage de Job Can avec du sélénium et l'ai déployé sur Google Cloud Functions, mais c'était assez difficile
Convertissez la feuille de calcul en CSV et importez-la dans Cloud Storage avec Cloud Functions
Le fichier édité avec vim était en lecture seule, mais je veux le sauvegarder
J'étais accro au grattage avec Selenium (+ Python) en 2020
[IOS] Animation GIF avec Pythonista3. J'en étais accro.
Le livre de PyTorch était difficile à comprendre, alors je l'ai complété
Importez et supprimez des fichiers dans Google Cloud Storages avec django-storage
Un mémorandum lors de l'acquisition automatique avec du sélénium
Je souhaite utiliser une bibliothèque externe avec IBM Cloud Functions
Comment se connecter à Cloud Firestore à partir de Google Cloud Functions avec du code Python
J'ai essayé de faire un processus d'exécution périodique avec Selenium et Python
J'ai essayé de résumer ce qui était sorti avec Qiita avec Word cloud
J'ai créé un robot musical en utilisant discord.py et l'API Google Drive (testé avec Docker → déployé sur Heroku)
J'ai créé une application pour les étrangers visitant le Japon à Hackason et j'ai gagné un prix, mais quand j'y réfléchissais attentivement, c'était inutile