見出し画像

【GAS】Google Apps Script 活用事例 採用管理ツールからスプレッドシートへの転記を自動化する方法

HRMOSという採用管理ツールから、応募者情報などをスプレッドシートに繰り返しコピペするのが面倒だったので自動化しました。

今回、ご紹介するのは、HRMOS経由で、Gmailに届いた書類選考の通知メールをスプレッドシートに書き出すスクリプトです。

2020.12.11 変数名などの命名規則を統一、同じような記述の削除、Switch文などを使った記述に変更しました。その結果、複雑になってしまいました。

各スクリプトの役割

1. getMessageFromHrmos Gmailのスレッドから取得した2次元配列を、一時的に、書き出し専用に書き出します。ほぼリアルタイムで書き出せるように、トリガーで、5分か10分おきで書き出しているため、後述する重複処理が必要になってきます。

2. findSameRecords_ 応募者管理表(本番環境)に書き出す前に、HRMOS IDで検索して、同じIDがあるかどうかを検索します。もしある場合は、登録済と転記します。

3. removeDuplicates_ シート上で動作する重複処理。HRMOS IDで同じかどうかを判断して、シートから削除します。filterメソッドのように配列操作をしないため、簡単に実装可能で、一番楽チンです。

4. getGmail_ 今回のスクリプトの一番の肝となるGmailの検索。ここから応募者名や、大学名、学科名、応募職種などの情報を正規表現で抽出します。

5. getApplicantInfo_ getUniversityName_ getUniversityName_ 
もそれぞれ、応募者情報、大学名、新卒学生のインターンの希望職種を、正規表現で情報を抽出するためのスクリプトで、記事執筆時は一緒くたにしていたものを、別関数として切り分けました。

改めて振り返ってみると、結構難しい事、やっているかもしれませんね。

スクリプト全文は長いですが.....こんな感じです。

/*
* 
* HRMOS経由で通知された 『書類選考が設定されました』 の文言に一致するメールが対象
* sample@gmail.com宛に受信したメールかつ、上記の条件に一致するメールをを5分おきに抽出する。
* 現状の設定は、25通
*
* HRMOS   → メールの取得
* setValues → 選考管理表に書き込みを行う
* 
* トリガーの設定者 sample@gmail.com
* 最終更新 2020/12/02
*
*/

function getMessageFromHrmos() {
 const spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
 const sheet       = spreadSheet.getSheetByName('書き出し専用');
 const searchWords = "書類選考が設定されました";
 
 //Gmailを検索して、データを2次元配列で取得する。
 const values = getGmail_(searchWords, 0, 25);
 
 if(0 < values.length){
   sheet.getRange(2, 1, values.length, values[0].length).setValues(values);
 }//if
 
 //選考管理表に登録済みのデータかどうかを判定する
 findSameRecords_(sheet);
 
 //重複を判定
 removeDuplicates_(sheet, 1)
 
}//end



/*
* 応募者管理表のHRMOS IDを取得して、indexOfで比較する事で、応募者情報が記載済みかどうかを判定する。
* 同じ応募者情報が書き出されないようにするための処理
*
* @param {Object} シートオブジェクト
*
*/

function findSameRecords_(sheet) {
 const values      = sheet.getDataRange().getValues();
 const columnA     = sheet.getRange(2, 1, sheet.getLastRow()-1, 1).getValues().flat();
 const targetArray = getHrmosId_();
 
 console.log(`HRMOS管理シートの配列 ${columnA}`);
 console.log(`選考管理表の配列: ${targetArray}`);
 
 let results = [];
 
 for(let i = 0; i < columnA.length; i++){
   
   //応募者管理表にあるHRMOSIDを取得して、一致したらステータスを登録済とする
   //不一致だったら、応募者管理表に書き出される対象になる。
   if(targetArray.indexOf(columnA[i]) !== -1){
     results.push(['登録済']);
   }else if(targetArray.indexOf(columnA[i]) === -1){
     results.push(['']);
   }
 }//for
 
 //見出し行を検索して、書き出す位置を決定する。
 const cloumn = getColumnNumber_(values, 0, '応募者名');
 sheet.getRange(2, cloumn, results.length, results[0].length).setValues(results);
 
}//end



/*
* 選考管理表に記載済みのHRMOSのIDかを検索するために、1次元配列で取得する。
* 
* @return {object} 1次元配列
*
*/

function getHrmosId_() {
 const spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
 const sheet       = spreadSheet.getSheetByName('応募者管理表');
 const values      = sheet.getDataRange().getValues();
 
 //D列の取得 列を挿入される危険性があるため
 const targetColumn = getColumnNumber_(values, 0, 'HRMOS ID');
 const range        = sheet.getRange(2, targetColumn, sheet.getLastRow(), 1);
 const columnA      = range.getDisplayValues();
 
 //空白行なしの1次元配列を作成
 const targetArray = generateArray_(columnA, 0);
 
 console.log(range.getA1Notation());
 //console.log(headerRow);
 //console.log(`選考管理表の配列: ${targetArray}`);
 
 return targetArray
}




/*
* シート上の重複を判定し、重複を削除する。
*
* @param {Object} シートオブジェクト
* @param {number} シートの列(0以上のインデックス)
*
*/

function removeDuplicates_(sheet, column) {
 const range = sheet.getRange('A2:Z');
 range.removeDuplicates([column]);
}



/*
* 書類選考の依頼した際、HRMOSから届いたメッセージを検索し、2次元配列で結果を返す
* 
* @param  {string } Gmailのスレッドを検索する際のキーワード
* @param  {number} 検索の開始位置 通常は0
* @param {number} 検索するスレッド数
* @return {object [[]]} 2次元配列
*
*/

function getGmail_(searchWords, start, quantity) {
 const query       = `${searchWords}`;
 const threads     = GmailApp.search(query, start, quantity);
 
 let arrayMessages = [];
 console.log(threads);
 
 //スレッドを取得する
 for (const thread of threads){
   
   const messages = thread.getMessages();
   
   //一つ一つのスレッドにある各メッセージを取得する。
   for(const message of messages){
     
     const date       = message.getDate();
     const plainText  = message.getPlainBody();
     const htmlText   = message.getBody();
     
     const applicantInfo = getApplicantInfo_(plainText, htmlText);
     const jobs          = getInternJobName_(htmlText);
     
     arrayMessages.push([applicantInfo.hrmosId,
                         date,
                         applicantInfo.name,
                         applicantInfo.university[0],
                         applicantInfo.university[1],
                         applicantInfo.status,
                         applicantInfo.confluenceUrl,
                         applicantInfo.hrmosUrl,
                         jobs[0],
                         jobs[1]
                        ]);
   }//for
 }//for
 console.log('応募者情報を格納した配列', arrayMessages);
 return arrayMessages
}//end



/*
* 書類選考の依頼した際、HRMOSから届いたメッセージの中から応募者情報や大学名などを取得する。
* 
* @param  {string} Gmailのメッセージをプレーンテキストとして取得したもの
* @param  {string} GmailのメッセージをHTML形式のテキストとして取得したもの
* @return {object} オブジェクト形式で返す
*
*/

function getApplicantInfo_(plainText, htmlText){
 const details = {
   name :          htmlText.match(/<td.*\/td>/g)[0].replace(/.*">/,'').replace(/<\/.*/, ''),
   university :    getUniversityName_(plainText),
   status :        plainText.match(/.*選考/g)[2],
   hrmosUrl :      plainText.match(/https:\/\/hrmos.co.*/),
   hrmosId :       plainText.match(/[0-9]{19}/)[0],
   confluenceUrl : plainText.match(/https:\/\/zozo.rickcloud.jp.*/),
 };
 
 console.log('書類選考時の応募者の情報', details);
 return details
 
}//end




/*
* 書類選考の依頼した際、HRMOSから届いたメッセージの中からインターンの第一希望と、第二希望の職種を取得
* 正規表現が一致しない場合は、空文字を含んだ配列を返す
* 
* @param  {string} GmailのメッセージをHTML形式のテキストとして取得したもの
* @return {object} 正規表現で一致した場合は、インターンの第一希望と、第二希望の職種を配列で返す
*
*/

function getInternJobName_(htmlText){
 const array      = htmlText.match(/【.*/g);
 let firstChoice  = '';
 let secondChoice = '';
 
 //正規表現に一致しない場合は、空の配列を返す
 if(array === null){
   const emptyArray = ['', ''];
   return emptyArray
 }
 //一致するかは、配列の要素数で判別する。0ではなかったら... 
 else if(0 < array.length){
   
   firstChoice  = array[0];
   secondChoice = array[1];
   
 }//if
 const jobs = [firstChoice, secondChoice];
 return jobs
}//end



/*
* 書類選考の依頼した際、HRMOSから届いたメッセージの中から大学名を取得
* 正規表現が一致しない場合は、空文字を返す
* 
* @param  {string} Gmailのメッセージをプレーンテキストとして取得したもの
* @return {object} 大学名と学部名を配列で返す
*
*/

function getUniversityName_(plainText){
 let university = '';
 
 switch (true) {
   case /.*大学院.*/.test(plainText):
     university = plainText.match(/.*大学院.*/)[0].split('/');
     console.log(university)
     break;
     
   case /.*大学.*/.test(plainText):
     university = plainText.match(/.*大学.*/)[0].split('/');
     console.log(university)
     break;  
     
   case /.*専門学校.*/.test(plainText):
     university = plainText.match(/.*専門学校.*/)[0].split('/');
     console.log(university)
     break;  
   case /.*university.*/.test(plainText):
     university = plainText.match(/.*university.*/)[0].split('/');
     console.log(university)
     break;  
     
   case /.*University.*/.test(plainText):
     university = plainText.match(/.*University.*/)[0].split('/');
     console.log(university)
     break;  
     
   default:
     university = ['', '']; 
     console.log('どのパターンにも該当しませんでした。')
     break;
 }//switch
 
 return university
}//end​

メッセージをプレインテキストか、HTMLかで取得できる。

const plainText = message.getPlainBody();
const htmlText  = message.getBody();

ミニマムでテスト


function myFunction(){
 const thread  = GmailApp.getInboxThreads(0,1)[0];
 const message = thread.getMessages()[0];
 console.log(message.getPlainBody());
}

最初から完璧にやろうとするのではなく、小さくテストしてみると良いかなと思います。

getPlainBody()

[image: Google]
お使いの Google アカウントへのアクセスが 無題のプロジェクト に許可されました
sample@gmail.com

アクセスを許可した覚えがない場合は、このアクティビティをご確認のうえ、アカウ ントを保護してください。
アクティビティを確認

getBody()

..........<div style="font-size: 24px;">お使いの Google アカウントへのアクセスが <a>無題のプロジェクト</a> に許可されました </div><table align="center" style="margin-top:8px;"><tr style="line-height: normal;"><td align="right" style="padding-right:8px;"><img width="20" height="20" style="width: 20px; height: 20px; vertical-align: sub; border-radius: 50%;;" src="https://lh3.googleusercontent.com/a-/AOh14Giv8cTvROu7VZZwnI84EnPxV-IPE4fW7mSgFbLk5A=s96" alt="">...........こんな感じで続く

aタグに挟まれたものを抽出したいとか、そういう場面では使えるかもしれませんね。今回のスクリプトでは、上記2つを利用しています。希望職種を抽出しやすいように、すみカッコ→【】で囲っています。

【サーバサイドエンジニア】とか、こんな感じで。

当初は、IF文で書いていました。

else ifがたくさんある場合だと、Switch文の方が読みやすいように感じます。

let university   = '';
     
if(message.getPlainBody().match(/.*大学.*/)){
  university = message.getPlainBody().match(/.*大学.*/)[0].split('/');
 }
else if(message.getPlainBody().match(/.*university.*/)){
  university = message.getPlainBody().match(/.*university.*/)[0].split('/');
}
else if(message.getPlainBody().match(/.*University.*/)){
  university = message.getPlainBody().match(/.*University.*/)[0].split('/');
}else{
  university = '';
}

補足ですが、僕が担当しているのは、新卒採用の補助なので、応募者の所属が大学である事がほとんどです。専門学校や、University、カレッジ、転職中で株式会社だったりすると処理がコケてしまいます。

Switch文を使う場合

2020.12.11のリライトでは、IF文から、Switch文に変更しています。大学院や専門学校の場合でも抽出ができるようにしました。

function getUniversityName_(plainText){
 let university = '';
 
 switch (true) {
   case /.*大学院.*/.test(plainText):
     university = plainText.match(/.*大学院.*/)[0].split('/');
     console.log(university)
     break;
     
   case /.*大学.*/.test(plainText):
     university = plainText.match(/.*大学.*/)[0].split('/');
     console.log(university)
     break;  
     
   case /.*専門学校.*/.test(plainText):
     university = plainText.match(/.*専門学校.*/)[0].split('/');
     console.log(university)
     break;  
   case /.*university.*/.test(plainText):
     university = plainText.match(/.*university.*/)[0].split('/');
     console.log(university)
     break;  
     
   case /.*University.*/.test(plainText):
     university = plainText.match(/.*University.*/)[0].split('/');
     console.log(university)
     break;  
     
   default:
     university = ['', '']; 
     console.log('どのパターンにも該当しませんでした。')
     break;
 }//switch
 
 return university
}//end

前職で、ジョブカンWFの抽出についても書いています。

ネットで探し当てたスクリプトに自己流を追加して書き上げたスクリプトでしたが、変数への再代入を至るところで繰り返していたり、あまり良くなかったので、リライトしました。


この記事が気に入ったらサポートをしてみませんか?