[GAS]リクナビのサイトからテレアポリストを作成するスクレイピングの制作過程
このような感じで作成していきます
似たようなものの作成をご希望の方はこちらからご連絡ください
https://menta.work/worker_plan/2245
要望
テレアポのためのリスト作成作業をスクレイピングで効率化したい
現状
現在手作業でおこなっている
要求
ページ内検索で医薬品で検索した一覧の中から電話番号、会社メール、社員数、会社名を抜き出しスプシに反映できること
仕様調査
リクナビの検索一覧urlの仕様を確認する
https://job.rikunabi.com/2021/s/10_0________/
業種の検索idの仕様を確認する
業種の検索条件が1つの場合は/s/番号_番号____/
業種の検索条件が複数の場合は一律/s/
-> 複数条件はurlに反映されていないので難易度が上がるかもしれない
検索一覧の詳細ページurlの要素を確認する
ul > div > div > a要素の中のhref
要素の取得がし易いxpathを確認する
//*[@id="cassette-r525430034"]/div[1]/div/a
詳細ページurlの仕様を確認する
https://job.rikunabi.com/2021/company/r525430034/
url一覧の要素のidと詳細urlのidが対応している
->検索一覧urlのidだけを一括取得して、url + idとしてアクセスすればスムーズかもしれない
ページ内の電話番号、会社メール、社員数、会社名の要素を確認する
電話番号
div #company-data04 > divの中の文字列
要素の取得がし易いxpathを確認する
//*[@id="company-data04"]/div
会社メール
電話番号と同様の要素
要素内に存在しているページと存在していないページがある
存在していない https://job.rikunabi.com/2021/company/r525430034/
存在している https://job.rikunabi.com/2021/company/r183110030/
社員数
#company-data03000006203752 の中の文字列
要素の取得がし易いxpathを確認する
/html/body/div[1]/div[2]/div[2]/div[4]/table/tbody/tr[4]/td
会社名
body > div.ts-h-l-root > div.ts-h-l-body > div.ts-h-company-upperArea > div.ts-h-company-upperArea-companyNameArea.ts-s-cf > div.ts-h-company-upperArea-companyNameArea-titleArea > h1 > aの中の文字列
要素の取得がし易いxpathを確認する
/html/body/div[1]/div[2]/div[1]/div[1]/div[1]/h1/a
調査結果
インターフェースは業種のid一覧をシート上に起こし、選択して実行するようにするのが一番容易
複数検索条件はurlにidが反映されていないので難易度が上がるかもしれない
実現する場合ヘッドレスブラウザの使用が必要になる
詳細ページurlは、検索一覧の詳細ページurlの要素のidと同一なので、idを一括取得して、url + idとして繰り返しアクセスすればスムーズかもしれない
要求の仕様化
- 業種の検索条件を一覧で表示する
- 検索条件を選択する
- 実行する
- 業種で検索した一覧のページにアクセスする
- 詳細ページurl一覧を取得する
- 詳細ページにアクセスする
- ページ内の電話番号、会社メール、社員数、会社名の要素を取得する
- 取得した要素の中の文字列をスプレッドシートに出力する
インプット
https://dividable.net/programming/python/python-scraping
https://dividable.net/programming/gas-scraping
https://developers.google.com/apps-script/reference/spreadsheet/sheet#getrangerow,-column
https://qiita.com/sakaimo/items/ba5594208c254fa528dc
https://www.d-wood.com/blog/2019/08/02_11423.html
https://www.d-wood.com/blog/2019/08/02_11423.html
https://tanuhack.com/gas-log/
https://www.google.com/search?q=xpath+javascript&oq=xpath+javascript&aqs=chrome..69i57j0l7.4890j0j7&sourceid=chrome&ie=UTF-8
https://shanabrian.com/web/javascript/xpath-evaluate.php
https://rinoguchi.hatenablog.com/entry/2019/09/19/134903
https://www.kotanin0.work/entry/2019/01/06/200000
https://qiita.com/takaito0423/items/259097b55b026800c875
https://teratail.com/questions/121482
https://jsprimer.net/basic/object/
https://rabbitfoot.xyz/gas-scraiping-with-parser/
https://qiita.com/diescake/items/70d9b0cbd4e3d5cc6fce#foreach--map
https://tonari-it.com/gas-regular-expression/
http://kito0039.hatenablog.com/entry/2016/09/11/004257
https://teratail.com/questions/98549
https://qiita.com/munieru_jp/items/101ee00c6906847df750
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
https://gray-code.com/javascript/remove-empty-element-for-array/
http://nevernoteit1419.blogspot.com/2012/07/blog-post_24.html
https://qiita.com/ssmxgo/items/c50d07ad6f53a5865034
https://gist.github.com/xl1/6d5f120c42be56b215f1
https://blog.katsubemakito.net/gas/enable-v8
https://jsprimer.net/use-case/nodecli/refactor-and-unittest/
https://www.monotalk.xyz/blog/google-app-script-の-urlfetchapp-の-例外ハンドリングについて/
https://postd.cc/a-response-to-why-most-unit-testing-is-waste/
筆記開示
// 業種で検索した一覧のページにアクセスする
industry = "インタフェースの仕様を決めて指定する"
url = "https://job.rikunabi.com/2021/s/" + industry
response = "htmlを取得する関数"
html = "htmlをパースする関数"
// 詳細ページurl一覧を取得する
urls = []
// ループ処理でidを抽出する
// 詳細ページのurlを作成する
url = "https://job.rikunabi.com/2021/company/" + id + "/"
// urlsにurlを詰める
// urlsの数だけ詳細ページにアクセスし、ページ内の電話番号、会社メール、社員数、会社名の要素を取得する
sales_list = []
columns = {}
response = "htmlを取得する関数"
html = "htmlをパースする関数"
columns['phone_number'] = "電話番号を抽出する"
columns['company_email'] = "会社メールを抽出する"
columns['employees'] = "社員数を抽出する"
columns['company_name'] = "会社名を抽出する"
// sales_listにcolumnsを詰める
// 取得した要素の中の文字列をスプレッドシートに出力する
// スプレッドシートを取得する
let ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName(sheetName);
// シートのオブジェクトを操作して取得した要素の文字列をループで設定する
sales_listをループ処理する
sheet.getRange(1, 1).setValue(elements['phone_number']);
sheet.getRange(1, 2).setValue(elements['company_email']);
sheet.getRange(1, 3).setValue(elements['employees']);
sheet.getRange(1, 4).setValue(elements['company_name']);
設計
function fetch() {
// 業種の検索条件を一覧で表示する
}
function execute() {
extract();
transform();
load();
}
function extract() {
// 業種で検索した一覧のページにアクセスする
// htmlを取得する
}
function transform() {
// 詳細ページurl一覧を取得する
// urlsの数だけ詳細ページにアクセスし、ページ内の電話番号、会社メール、社員数、会社名の要素を取得する
}
function load() {
// urlsの数だけ詳細ページにアクセスし、ページ内の電話番号、会社メール、社員数、会社名の要素を取得する
}
実装
function fetch() {
const URL = "https://job.rikunabi.com/2021/search/company/condition/";
let html = UrlFetchApp.fetch(URL).getContentText('UTF-8');
let industryElements = Parser.data(html).from('<span class="ts-h-_searchCondition-checkboxText">').to('</span>').iterate();
let industryColmns = industryElements.filter(industryElement => industryElement.indexOf("href") != -1).map(industriesHref => industriesHref.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,''));
let industryIds = industryElements.filter(industryElement => industryElement.indexOf("href") != -1).map(industriesHref => industriesHref.split("/2021/s/")[1].split("?")[0]).filter(Boolean);
let ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getActiveSheet();
let i = 0;
while (i < industryIds.length){
let rowNumber = i + 1;
sheet.getRange(rowNumber, 11).setValue(industryColmns[i]);
sheet.getRange(rowNumber, 12).setValue(industryIds[i]);
i++;
}
}
function execute() {
//アクティブなセルのidを取得する
let ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getActiveSheet();
let cell = sheet.getActiveCell();
let industry = sheet.getRange(cell.getRow(), 12).getValue();
let html = extract(industry);
let sales_list = transform(html);
load(sales_list);
}
function extract(industry) {
const URL = "https://job.rikunabi.com/2021/s/" + industry;
return UrlFetchApp.fetch(URL).getContentText('UTF-8');
}
function transform(html) {
// 詳細ページurl一覧を作成する
let urls = makeDetailUrls(html);
// urlsの数だけ詳細ページにアクセスし、ページ内の電話番号、会社メール、社員数、会社名の要素を取得する
return makeSalesList(urls);
}
function load(sales_list) {
// スプレッドシートを取得する
let ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName('シート1');
// シートのオブジェクトを操作して取得した要素の文字列をループで設定する
let i = 0;
while (i < sales_list.length){
let rowNumber = i + 1;
sheet.getRange(rowNumber, 1).setValue(sales_list[i]['url']);
sheet.getRange(rowNumber, 2).setValue(sales_list[i]['phone_number']);
sheet.getRange(rowNumber, 3).setValue(sales_list[i]['company_email']);
sheet.getRange(rowNumber, 4).setValue(sales_list[i]['employees']);
sheet.getRange(rowNumber, 5).setValue(sales_list[i]['company_name']);
i++;
}
}
function makeSalesList(urls) {
let sales_list = [];
const label = {
url: "URL",
phone_number: "電話番号",
company_email: "会社メール",
employees: "社員数",
company_name: "会社名"
};
sales_list.push(label);
urls.forEach(url => {
try {
let html = UrlFetchApp.fetch(url).getContentText('UTF-8');
let columns = {};
columns['url'] = url;
columns['phone_number'] = makePhoneNumber(html);
columns['company_email'] = makeCompanyEmail(html);
columns['employees'] = makeEmployees(html);
columns['company_name'] = Parser.data(html).from('<h1 class="ts-h-company-mainTitle">').to('</h1>').build().replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'');
sales_list.push(columns);
} catch (e) {
Logger.log("message:" + e.message + "\nfileName:" + e.fileName + "\nlineNumber:" + e.lineNumber + "\nstack:" + e.stack);
}
});
return sales_list;
}
function makeDetailUrls(html) {
let urls = [];
// 詳細ページのurlの要素を抽出する
let urlElements = Parser.data(html).from('<div class="ts-h-search-cassetteTitle">').to('</div>').iterate();
// 詳細ページのurlを作成する
urlElements.forEach(urlElement => {
let id = urlElement.split('/2021/company/')[1].split('" target="_blank"')[0];
urls.push("https://job.rikunabi.com/2021/company/" + id);
});
return urls;
}
function makePhoneNumber(html) {
let companyText = Parser.data(html).from('<div id="company-data04" class="ts-h-company-section">').to('</div>').build().replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'');
let phoneNumber;
if (companyText.indexOf("TEL") != -1) {
phoneNumber = companyText.split("TEL")[1].match(/(0\d{1,4})[-\(\s](\d{1,4})[-\)\s](\d{4})/)[0];
} else {
phoneNumber = "TELなし";
}
return phoneNumber;
}
function makeCompanyEmail(html) {
let companyText = Parser.data(html).from('<div id="company-data04" class="ts-h-company-section">').to('</div>').build().replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'');
let companyEmail;
if (companyText.indexOf("@") != -1) {
companyEmail = companyText.match(/[\w\-\.]+@[\w\-\.]+\.[a-zA-Z]+/)[0];
} else {
companyEmail = "emailなし";
}
return companyEmail;
}
function makeEmployees(html) {
let tableElement = Parser.data(html).from('<table class="ts-h-mod-dataTable02">').to('</table>').build();
let trElements = Parser.data(tableElement).from('<tr>').to('</tr>').iterate();
let employees;
trElements.forEach(trElement => {
if (trElement.indexOf("従業員数") != -1) {
employees = trElement.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'').split("従業員数")[1];
}
});
return employees;
}
テスト
var exports = GASUnit.exports
var assert = GASUnit.assert
function makeTest() {
exports({
'#makeCompanyEmailTest()': {
'emailのみのテキストを作成する': function () {
assert(makeCompanyEmail('<div>test@gmail.com</div>') === "test@gmail.com")
},
'emailがなかったらemailなし': function () {
assert(makeCompanyEmail('<div></div>') === "emailなし")
}
},
'#makeEmployeesTest()': {
'従業員数のみのテキストを作成する': function () {
assert(makeEmployees("<tr>従業員数10名</tr>") === "10名")
}
},
'#makePhoneNumberTest()': {
'TELがあったら電話番号のみのテキストを作成する': function () {
assert(makePhoneNumber("<div>TEL080-6512-1354</div>") === "080-6512-1354")
},
'TELがなかったらTELなし': function () {
assert(makePhoneNumber("<div>080-6512-1354</div>") === "TELなし")
}
},
'#makeDetailUrlsTest()': {
'htmlのurl一覧要素からurl一覧を作成する': function () {
const html = UrlFetchApp.fetch('https://job.rikunabi.com/2021/s/10_0________/').getContentText('UTF-8');
const urls = ["https://job.rikunabi.com/2021/company/r436500030/"];
assert(makeDetailUrls(html)[0] === urls[0])
}
},
'#makeSalesListTest()': {
'url一覧から出力するリストを作成する': function () {
const urls = ["https://job.rikunabi.com/2021/company/r436500030/"];
assert(makeSalesList(urls)[1]['url'].indexOf('https') != -1)
assert(makeSalesList(urls)[1]['phone_number'].match(/(0\d{1,4})[-\(\s](\d{1,4})[-\)\s](\d{4})/)[0])
assert(makeSalesList(urls)[1]['company_email'].match(/[\w\-\.]+@[\w\-\.]+\.[a-zA-Z]+/)[0])
assert(makeSalesList(urls)[1]['employees'].match(/[0-90-9]/)[0])
assert(makeSalesList(urls)[1]['company_name'] === "日新製薬株式会社・日新薬品株式会社");
}
}
})
}