どうもこんにちは!Google大好きマダラです!
今回はGoogle App Scriptでスクレイピングを実装したので、ぜひコピペしていってください!
そしてコードを紹介していこうと思います!
具体的なサイトやHTMLの分解などは本質的じゃないので省いていきます!
ではさっそく紹介していきましょう!
なお、勉強には以下の本を使いました!
是非是非ポチッとしていってください!
まずはコピペ!
まずは、今描いてる最中でとっととコピペさせてくれ!というせっかちさんのためにコードをばこっとはっておきますね
ちょこちょこ身バレ防ぎ?で、実行先隠しで帰るのでそのままで使用できませんが、、、
// Parser: 1Mc8BthYthXx6CoIz90-JiSzSafVnT6U3t0z_W3hLTAX5ek4w0G_EIrNw
var SHEET_ID = "";
var SITE_URL = "";
var PATH = "";
var PARAM = "?page=";
var CATEGORY_LIST = ["hoge"];
/**
* 同名トリガーがあったら削除
*
*/
function deleteTrigger(funcName) {
var triggers = ScriptApp.getProjectTriggers();
triggers.forEach(function(trigger){
if (trigger.getHandlerFunction() == funcName) {
ScriptApp.deleteTrigger(trigger);
}
});
}
/**
* HTML用の空白除去
*/
function replaceBreak(data){
var br = /[\r\n]+/g; //改行
var rep = ""; //置換文字列
return data.replace(br,rep).replace(" ", rep).replace(" ", rep).replace(/[\s\t\n]/g,"").trim();
}
/**
* 商品一覧に商品データを格納
*/
function setItemData(ないしょだよ) {
// 空欄になっている行から挿入するため最終行を取得
var lastRow = itemsSheet.getLastRow();
itemsSheet.getRange(lastRow+1,1).setValue(a);
itemsSheet.getRange(lastRow+1,2).setValue(a);
itemsSheet.getRange(lastRow+1,3).setValue(a);
itemsSheet.getRange(lastRow+1,4).setValue(a.toString());
itemsSheet.getRange(lastRow+1,5).setValue(a.toString());
itemsSheet.getRange(lastRow+1,6).setValue(a);
}
/**
* 現在時刻を管理シートにセット
*/
function setNowDate(adminSheet) {
var date = new Date();
date = Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss');
adminSheet.getRange(1, 2).setValue(date);
}
/**
* 引数のURLのHTMLを取得
*/
function getHTML(url) {
var response = UrlFetchApp.fetch(url);
var html = response.getContentText("UTF-8");
html = html.replace(/\\u003c/g, "<");
html = html.replace(/\\u003e/g, ">");
html = html.replace(/\\\//g, "/");
html = html.replace(/\\\"/g, "\"");
html = html.replace(/\\n/g, "\n");
return html;
}
/**
* 指定カテゴリーの〇〇リンクリストを取得
*/
function getItemUrlListOf(category) {
// 〇〇リンクを格納する配列
var itemUrlList = [];
for (var i = 1; i < 100; i++) {
var url = SITE_URL+PATH+category+PARAM+String(i);
// console.log("URL:"+url);
var html = getHTML(url);
// ページが超過したら処理を抜ける
if (html.includes('見つかりません')) {
break;
}
var productGrid = Parser.data(html).from('id=\"hoge').to('ul>').build();
var items = Parser.data(hoge).from('<li class=\"joge\">').to('</li>').iterate();
for (var j = 0; j < items.length; j++) {
var card = Parser.data(items[j]).from('class').to('</h3>').build();
var itemPath = Parser.data(card).from('href=\"').to('\" class=').build();
itemUrlList.push(SITE_URL+itemPath);
}
}
return itemUrlList;
}
/**
* スプレッドシートの新規作成
*/
function createNewSpreadSheet(folder) {
//【手順1】新規スプレッドシートをマイドライブに作成する
const newSheet = SpreadsheetApp.create('hoge');
/*addFile,removeFileメソッドのパラメータはFileオブジェクトを指定するため、【手順1】で作成したスプレッドシートをFileオブジェクトとして取得する */
const file = DriveApp.getFileById(newSheet.getId());
//【手順2】手順1で作成したスプレッドシートを指定フォルダに「追加」する
folder.addFile(file);
//【手順3】手順1で作成したスプレッドシートをマイドライブから「削除」する
DriveApp.getRootFolder().removeFile(file);
return newSheet;
}
/**
* 初期セットアップ
*/
function setupUp(targetFolderName) {
newFolder = DriveApp.getRootFolder().createFolder(targetFolderName);
imageFolder = newFolder.createFolder("hoge");
ss = createNewSpreadSheet(newFolder);
// 管理用シート
adminSheet = ss.insertSheet();
adminSheet.setName("管理用");
adminSheet.getRange(1,1).setValue("最終日時");
adminSheet.getRange(2,1).setValue("エラー");
adminSheet.getRange(3,1).setValue("実行済みカテゴリ");
// 一覧シート
itemSheet = ss.insertSheet();
itemSheet.setName("一覧");
itemSheet.getRange(1,1).setValue("a");
itemSheet.getRange(1,2).setValue("a");
itemSheet.getRange(1,3).setValue("a");
itemSheet.getRange(1,4).setValue("a");
itemSheet.getRange(1,5).setValue("a");
itemSheet.getRange(1,6).setValue("a");
// 初期シートは削除
var defaultSheet = ss.getSheetByName("シート1");
ss.deleteSheet(defaultSheet);
}
/**
* 1. カテゴリーごとに詳細リンクを取得
* 2. 詳細リンクから各種情報を取得
* 3. 画像を保存する
* 4. スプレッドシートに情報を保存する
*/
function myFunction() {
deleteTrigger("myFunction");
// 現在の時間を取得
var startTime = new Date();
var date = new Date();
date = Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy_MM_dd');
let ss;
let itemSheet;
let adminSheet;
let newFolder;
let imageFolder;
// その日にフォルダがなければ作成
const targetFolderName = "hoge"+date;
// 指定した名前のフォルダを取得
const folderIterator = DriveApp.getRootFolder().getFoldersByName(targetFolderName);
// 存在する場合
if (folderIterator.hasNext()) {
newFolder = folderIterator.next();
var file = newFolder.getFilesByName("hoge");
ss = SpreadsheetApp.open(file.next());
adminSheet = ss.getSheetByName("管理用");
itemSheet = ss.getSheetByName("一覧");
imageFolder = newFolder.getFoldersByName("gem_images").next();
// 存在しない場合
} else {
setupUp(targetFolderName)
}
// カテゴリーごとに実行
for (var i = 0; i < CATEGORY_LIST.length; i++) {
var category = CATEGORY_LIST[i];
// 完了済みのカテゴリはスルー
var categoryFinderCells = adminSheet.createTextFinder(category)
.matchCase(false) //[大文字と小文字の区別]無効
.matchEntireCell(true)
.findAll(); //[完全に一致するセルを検索]有効
if (categoryFinderCells.length > 0) {
continue;
}
// カテゴリ2週目以降の時、1つ前のカテゴリを完了済みとして登録する
if (i>0) {
adminSheet.getRange(3,1+i).setValue(CATEGORY_LIST[i-1]);
}
var itemUrlList = getItemUrlListOf(category);
for (var j = 0; j < itemUrlList.length; j++) {
// 処理実行から何分経過したのかを取得
var diff = parseInt((new Date() - startTime) / (1000 * 60));
// 6分でGAS自動終了するため1分後にトリガーを仕込んで処理を終了
if(diff >= 5){
var dt = new Date();
dt.setMinutes(dt.getMinutes() + 1); //1分後に再実行
ScriptApp.newTrigger('myFunction').timeBased().at(dt).create();
return;
}
var itemUrl = itemUrlList[j];
// 商品がすでに登録されている場合スルー
var textFinder = itemSheet.createTextFinder(itemUrl)
.matchCase(false) //[大文字と小文字の区別]無効
.matchEntireCell(true); //[完全に一致するセルを検索]有効
var cells = textFinder.findAll();
if (cells.length > 0) {
continue;
}
var itemId = itemUrl.split("/products/",2)[1];
var html = getHTML(itemUrl);
var body = Parser.data(html).from('hoge\">').to('hoge').build();
// タイトル
var title = Parser.data(body).from('title').to('h1>').build();
var title = title.replace(/[\t\n]/g,"").replace(/\".*?>/g,"").replace(/<\/.*/,"").trim();
var title = title.replace("hoge","").trim();
console.log(title);
setItemData(a, a, a, a, a, a, a, a, a);
for (var k = 0; k < imageUrlList.length; k++) {
var imageName = "hoge"+itemId + "_" + k.toString().padStart(2, "0") // 2桁で0埋め
getImage(imageUrlList[k], imageFolder, imageName);
}
}
}
// 管理シートに終了時刻を設定
setNowDate(adminSheet);
return;
}
/**
* 画像取得
*/
function getImage(url, imageFolder, imageName){
var option = {
method:"get"
}
var res = UrlFetchApp.fetch(url, option);
const image = res.getBlob();
var imageFile = imageFolder.createFile(image);
imageFile.setName(imageName);
}
要所要所、消してしまったり適当な文字列を入れてしまいましたが大体こんな感じです!
処理の流れとしては
- サイト内の欲しい情報一覧の全ての詳細URLを取得
- そのURLに個別にアクセスして、特定の情報をHTMLから抜き取って保村します
- 処理が5分以上たったら次簿トリガーを設定します
これはGASの使用上6分以上かかる処理はタイムアウトしてしまうのでその対策ですね!
スクレイピングで6分で終わるってことはないかと思うのでたいてい対応が必要です - 最後に画像も集めといて終わりです
画像部分のコード消してしまいましたが、HTMLから画像URLを取得してリストとして保持しています - 収集した情報のスプレッドシートや画像は、ディレクトリを作成してGoogleドライブに自動で保存していきます。この点がスムーズなのもGoogleの利点ですね
- 2回目に実行した段階では、その日のうちなら続きから実行できるようになっております
Google App Scriptでスクレイピングした感想
正直Pyhtonの方がHTMLを分解して欲しい情報を取るのが簡単でした!
Google App Scriptだとあまり情報なかったので、replaceや正規表現を駆使してなんとかHTMLから無駄な部分を削除して欲しい情報だけを切り取りました!
それでも、ここの抜き取り部分で非常に時間もかかり苦労したなぁという印象です
Pythonなら、タグ指定ができたりclass指定ができたり簡単なんですよね
Google App Scriptでやるメリット
何と言っても環境構築が要らない点でしょう!
実装はめんどくさいですが、作成したプログラムを非エンジニアに渡しても、実行だけしてもらえれば簡単に動かすことができます
環境構築は非エンジニアにとっては超難易度ですからこのメリットは管理でかいです
副業を行う上で納品も楽なんですよね
情報集めた上で、プログラムも提供してって感じで
さらにさらに、クライアントに渡しておくことで、サイトのフォーマットが変わってスクレイピングできなくなったら再度依頼してきます笑
そうゆう意味でも誰でも簡単に実行できるようにして送ってはいいですね
まとめ
Google App Scriptならサクッとプログラムを作成して共有できる点が非常に強力だと感じました!
スクレイピングとしては結構きつかったですが、できなくはないです
気軽にいろんなことができるので副業としてもハードルが低そうですね
是非勉強してみてください!
ではでは