スクレイピングとは
ブラウザで画面が表示されるまで
(要求されたデータを受信 - レスポンス)
サーバー
⇅
ブラウザ
(欲しい情報を要求 - リクエスト)
ブラウザがHTMLを解析して画面を作成.
データ: HTML, CSS, JavaScript, 画像など
HTMLとは
Webページの構造をタグを使って定義する書き方
タグごとに意味が異なる
例
- h1~6 -> ページの見出しに使う
- p -> 文章等に使う
- a -> リンクを貼る時に使う
- img -> 画像を挿入するときに使う
準備
{ "scripts": { "dev": "next dev", "build": "next build", "export": "next export", "start": "next start", "build-start": "npm-run-all -s build start", "lint": "next lint" } }
- npm-run-all 後に記載するコマンドを全て実行する
- -s シーケンシャル(直列)で実行する
Playwrightを使用する
postinstall
→ npm install後に行うinstall
{ "scripts": { "postinstall": "npx playwright install" } }
npm run build-start
でNext.jsを起動してから下記コードを実行する
import { chromium } from "@playwright/test" ;async () => { // slowMo -> delayを指定する const browser = await chromium.launch({ headless: false, slowMo: 500 }) const page = await browser.newPage() await page.goto("http://localhost:3000") const htmlStr = await page.content() console.log(htmlStr) await browser.close() }
Locatorで取得したい要素を特定する
import { chromium } from "@playwright/test" ;(async () => { // slowMo -> delayを指定する const browser = await chromium.launch({ headless: false, slowMo: 500 }) const page = await browser.newPage() await page.goto("http://localhost:3000") // CSSセレクター const pageTitleLocator = await page.locator(".navbar-brand") const pageTitle = await pageTitleLocator.innerText() // console.log(pageTitle) // 文字列で要素を取得 const textLocator = await page.locator("text=名刺管理アプリ") const pageText = await textLocator.innerText() // console.log(pageText) // XPathで要素を取得 const xpathLocator = await page.locator( 'xpath=//*[@id="__next"]/div/div[2]/div/ul/li[1]' ) const xpathText = await xpathLocator.innerText() console.log(xpathText) await browser.close() })()
Locatorの詳しい使い方
import { chromium } from "@playwright/test" // @see セレクターのチェーンの利用方法(>>) // https://playwright.dev/docs/selectors#chaining-selectors ;(async () => { const browser = await chromium.launch({ headless: true, slowMo: 500 }) const page = await browser.newPage() await page.goto("http://localhost:3000") // CSS セレクターで要素を取得 const pageTitleLocator1 = page.locator( ".cards.list-group-item:nth-child(3) > a" ) const pageTitle1 = await pageTitleLocator1.innerText() // 0からカウントするインデックスで取得 // `>>` -> 複合的に条件を指定することが出来る const pageTitleLocator2 = page.locator(".cards.list-group-item > a >> nth=2") const pageTitle2 = await pageTitleLocator2.innerText() // 親要素が欲しい場合 `>>` const pageTitleLocator3a = page.locator(".cards.list-group-item >> ..") const pageTitle3a = await pageTitleLocator3a.innerText() // or const pageTitleLocator3b = page.locator(".cards.list-group-item") const parentLocator = pageTitleLocator3b.locator("..") debugger const pageTitle3b = await parentLocator.innerText() console.log(pageTitle3b) await browser.close() })()
デバッグでコードを確認する
VSCodeの実行とデバッグ
でawait
の必要性などが確認できる。
UIイベントをスクリプトで記述する
import { chromium } from "@playwright/test" // @see セレクターのチェーンの利用方法(>>) // https://playwright.dev/docs/selectors#chaining-selectors ;(async () => { const browser = await chromium.launch({ headless: false, slowMo: 500 }) const page = await browser.newPage() await page.goto("http://localhost:3000") // inputに「美」を入力 const inputLocator = page.locator('//*[@id="__next"]/div/div[1]/label/input') // await inputLocator.type("美穂") // typeメソッド <- @playwright/test@1.40.0で非推奨 await inputLocator.fill("美") await page.waitForTimeout(1000) // 指定ページに移動する const page3Locator = page.locator(".page-link.page-number >> nth=-1") await page3Locator.click() await page.waitForTimeout(1000) // ページ内の人物の名前の数 const cardLocator = page.locator(".cards.list-group-item") const cardCount = await cardLocator.count() console.log(cardCount) await page.waitForTimeout(2000) await browser.close() })()
textContentとinnerTextの違い
- textContent タグの中の文字列を取得する
- innerText タグの中で表示されているものを取得する (display: none; や visibility: hidden; が付与されている場合、取得できない
ファイルへの書き込み処理
import { chromium } from "@playwright/test" import * as fs from "fs" ;(async () => { const browser = await chromium.launch({ headless: false, slowMo: 500 }) const page = await browser.newPage() await page.goto("http://localhost:3000") const cardLocator = page.locator(".cards.list-group-item >> nth=1") const cardText = await cardLocator.textContent() await browser.close() fs.writeFileSync("./text-data.csv", cardText) })()
CSV形式でデータ保存
import { chromium } from "@playwright/test" import * as fs from "fs" import { Parser } from "json2csv" ;(async () => { const browser = await chromium.launch({ headless: false, slowMo: 500 }) const page = await browser.newPage() await page.goto("http://localhost:3000") const cardLocators = page.locator(".cards.list-group-item") const cardCount = await cardLocators.count() const fetchedCards = [] for (let i = 0; i < cardCount; i++) { const cardLocator = cardLocators.locator(`nth=${i}`) const cardText = await cardLocator.textContent() fetchedCards.push({ name: cardText, }) } await browser.close() console.log(fetchedCards) // JSON形式のデータをCSV形式に変換する const parser = new Parser() const csv = parser.parse(fetchedCards) fs.writeFileSync("./text-data.csv", csv) })()
CSVに追加情報を書き込む
import { chromium } from "@playwright/test" import * as fs from "fs" import { Parser } from "json2csv" ;(async () => { const browser = await chromium.launch({ headless: false, slowMo: 500 }) const page = await browser.newPage() await page.goto("http://localhost:3000") const cardLocators = page.locator(".cards.list-group-item") const cardCount = await cardLocators.count() const fetchedCards = [] for (let i = 0; i < cardCount; i++) { const cardLocator = cardLocators.locator(`nth=${i} >> a`) const cardText = await cardLocator.textContent() await cardLocator.click() const companyLocator = page.locator(".card-title.company") const companyText = await companyLocator.textContent() fetchedCards.push({ company: companyText, name: cardText, }) const backLocator = page.locator("text=戻る") await backLocator.click() } await browser.close() console.log(fetchedCards) // JSON形式のデータをCSV形式に変換する const parser = new Parser() const csv = parser.parse(fetchedCards) fs.writeFileSync("./text-data.csv", csv) })()
Google Spread Sheetの操作
Google Cloud初期設定
最初にGoogle Cloud → APIとサービスでプロジェクトを作成
プロジェクト内でGoogle Sheets API, Google Drive APIを有効化
IAMと管理→サービスアカウントでキーを作成
スプレッドシートを作成したら共有で作成したキーのemailを入力
【重要】Sheetのアクセスに必要な設定
1. プロジェクトの作成
2. APIとサービスの有効化
3. サービスアカウントの作成
セルの値の取得
シートの操作に必要なパッケージのインストール
npm install googleapis@105 @google-cloud/local-auth@2.1.0 --save
JSONファイルは直接、importでは読み込めない
Node.jsでJSONがimportで読み込めないときの対処法
import { GoogleSpreadsheet } from "google-spreadsheet" import env from "dotenv" env.config() // Google Cloudのサービス アカウント キー(JSON)を取得する // import { createRequire } from "module" // const require = createRequire(import.meta.url) // const secrets = require("../../../google_secrets.json") // or import { readFile } from "fs/promises" const secrets = JSON.parse(await readFile("./google_secrets.json")) ;(async () => { // スプレッドシートのURLにIDが含まれている // https://docs.google.com/spreadsheets/d/1a8hWW0I5X4AOz3pjwZHCa3F41FDbcE7hojOlIAkpkvo/edit#gid=0 const doc = new GoogleSpreadsheet(process.env.GOOGLE_SHEET_ID) await doc.useServiceAccountAuth({ client_email: secrets.client_email, private_key: secrets.private_key, }) await doc.loadInfo() const sheet = doc.sheetsByIndex[0] await sheet.loadCells("A1:C4") // getCell(行, 列) const a1 = sheet.getCell(0, 0) const b1 = sheet.getCell(0, 1) const b2 = sheet.getCellByA1("B2") const b4 = sheet.getCell(3, 1) console.log("a1", a1.value) console.log("b1", b1.value) console.log("b2", b2.value) console.log("b4", b4.value) })()
.envを読み込んでいるコードのデバッグ方法
{ // ********* デバッグの設定ファイル ********* // IntelliSense を使用して利用可能な属性を学べます。 // 既存の属性の説明をホバーして表示します。 // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Run Current File", "skipFiles": ["<node_internals>/**"], // `${file}` -> 現在開いているファイルをデバッグする "program": "${file}" }, { "type": "node", "request": "launch", "name": "Launch 04_automation Program File", "skipFiles": ["<node_internals>/**"], "program": "${file}", // .envファイルの場所を指定している "envFile": "${workspaceFolder}/04_automation/.env" }, ... }
環境変数を.envファイルに抽出
URLやパスは、設定値として.envファイルに抽出する
{ "dependencies": { "dotenv": "^16.0.3", } }
import env from 'dotenv'; env.config()
TARGET_URL=http://localhost:3000
OUTPUT_FILE=./text-data.csv
EMAIL_FROM=
EMAIL_TO=
APP_PASS=
エラーが発生時の処理(例外処理)を記述
import { chromium } from "@playwright/test" ;(async () => { const browser = await chromium.launch({}) const page = await browser.newPage() await page.goto("http://localhost:3000") await page.waitForTimeout(2000) try { const inputLocator = page.locator('//*[@id="__next"]/div[1]/label/input') // 例外が発生しそうな時のタイムアウトを設定できる await inputLocator.fill("美", { timeout: 1000 }) } catch (e) { console.log("インプットの入力処理で例外が発生しました。", e) } const pager3Locator = page.locator(".page-link.page-number >> nth=-1") await pager3Locator.click() const cardLocator = page.locator(".cards.list-group-item") const cardCount = await cardLocator.count() console.log(cardCount) await browser.close() })()
セルの書き込み
const sheet = doc.sheetsByIndex[0] await sheet.loadCells("A1:C5") const a1 = sheet.getCell(0, 0) const a5 = sheet.getCell(4, 0) const b1 = sheet.getCell(0, 1) const b2 = sheet.getCellByA1("B2") // 書式の出力 console.log("a1", a5.textFormat) a1.value = 11 b1.value = 15 // 書式の変更 a1.textFormat = { fontSize: 18 } a5.value = "=sum(A1:A4)" await sheet.saveUpdatedCells()
行の追加
// シートの作成、ヘッダーの追加 await doc.addSheet({ title: "persons", headerValues: ["name", "age", "gender"], }) const personSheet = doc.sheetsByTitle["persons"] // 行を追加 const row = await personSheet.addRow({ name: "Tom", age: 18, gender: "M", }) row.save() // 複数行を追加 const rows = await personSheet.addRows([ { name: "Tom", age: 18, gender: "M", }, { name: "Hanako", age: 20, gender: "F", }, { name: "John", age: 25, gender: "M", }, ]) for (const row of rows) { await row.save() } // or rows.forEach(async (row) => { await row.save() })
行の更新
const personSheet = doc.sheetsByTitle["persons"] const rows = await personSheet.getRows() // console.log(rows[2].age) // 行の更新 // rows[0].age = 30 // rows[0].save() // 行の削除 rows[0].delete()
スクレイピングをSheetにまとめる
import { chromium } from "@playwright/test"; async function getEmployeesByScraping() { const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto("http://localhost:3000"); const cardLocators = page.locator(".cards.list-group-item"); const cardCount = await cardLocators.count(); const fetchedCards = []; for(let i = 0; i < cardCount; i++) { const cardLocator = cardLocators.locator(`nth=${i} >> a`); const cardText = await cardLocator.textContent(); await cardLocator.click(); const companyLocator = page.locator('.card-title.company'); const companyText = await companyLocator.textContent(); fetchedCards.push({ company: companyText, name: cardText }); const backLocator = page.locator('text=戻る'); await backLocator.click(); } await browser.close(); return fetchedCards; }; export { getEmployeesByScraping };
import { GoogleSpreadsheet } from "google-spreadsheet" import env from "dotenv" env.config() import { createRequire } from "module" const require = createRequire(import.meta.url) const secrets = require("../../../google_secrets.json") import { getEmployeesByScraping } from "./scraping.mjs" ;(async () => { const doc = new GoogleSpreadsheet(process.env.GOOGLE_SHEET_ID) await doc.useServiceAccountAuth({ client_email: secrets.client_email, private_key: secrets.private_key, }) await doc.loadInfo() const employees = await getEmployeesByScraping() const sheet = doc.sheetsByTitle["scraping"] const rows = await sheet.addRows(employees) rows.forEach((row) => { row.save() }) })()
cron(スケジューラー)の使い方
https://github.com/node-cron/node-cron
スケジューリング(定期実行)の手法
import cron from "node-cron"; cron.schedule('* * * * * *', () => console.log('毎秒に実行')); cron.schedule('*/3 * * * * *', () => console.log('3秒ごとに実行')); cron.schedule('* * * * *', () => console.log('毎分に実行')); cron.schedule('0 0 9,18 * * *', () => console.log('毎日9時,18時に実行')); cron.schedule('30 30 12 * * *', () => console.log('毎日12時30分30秒に実行'));
定期的な処理のことをバッチと呼ぶ。
import { chromium } from "@playwright/test"; async function getEmployeesByScraping() { const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto("http://localhost:3000"); const cardLocators = page.locator(".cards.list-group-item"); const cardCount = await cardLocators.count(); const fetchedCards = []; for(let i = 0; i < cardCount; i++) { const cardLocator = cardLocators.locator(`nth=${i} >> a`); const cardText = await cardLocator.textContent(); await cardLocator.click(); const companyLocator = page.locator('.card-title.company'); const companyText = await companyLocator.textContent(); fetchedCards.push({ company: companyText, name: cardText }); const backLocator = page.locator('text=戻る'); await backLocator.click(); } await browser.close(); return fetchedCards; }; export { getEmployeesByScraping };
import { GoogleSpreadsheet } from "google-spreadsheet" import env from "dotenv" env.config() import { createRequire } from "module" const require = createRequire(import.meta.url) const secrets = require("../../../google_secrets.json") import { getEmployeesByScraping } from "./scraping.mjs" export const addEmployeesToGS = async () => { const doc = new GoogleSpreadsheet(process.env.GOOGLE_SHEET_ID) await doc.useServiceAccountAuth({ client_email: secrets.client_email, private_key: secrets.private_key, }) await doc.loadInfo() const employees = await getEmployeesByScraping() const sheet = doc.sheetsByTitle["scraping"] const rows = await sheet.addRows(employees) rows.forEach((row) => { row.save() }) }
import { addEmployeesToGS } from "./google-sheet.mjs" import cron from "node-cron" // 15:52毎に実行 cron.schedule("52 15 * * *", () => { addEmployeesToGS() })
定期実行の結果をEmailで送信してみよう
2段階認証でアプリパスワードを設定する
import nodemailer from "nodemailer" import dotenv from "dotenv" dotenv.config() ;(async () => { const message = { from: process.env.EMAIL_FROM, to: process.env.EMAIL_TO, subject: "メールの件名です", text: `これはスクリプトによって送信されました。\n改行後`, } const smtpConfig = { host: "smtp.gmail.com", port: 465, secure: true, // SSL auth: { user: process.env.EMAIL_FROM, //googleアカウントのアプリパスワードを設定 // see https://support.google.com/accounts/answer/185833?hl=ja pass: process.env.APP_PASS, }, } const transporter = nodemailer.createTransport(smtpConfig) transporter.sendMail(message, function (err, response) { console.log(err || response) }) })()
TARGET_URL=http://localhost:3000
OUTPUT_FILE=./text-data.csv
GOOGLE_SHEET_ID=
EMAIL_FROM=
EMAIL_TO=
APP_PASS=
スクレイピング→シート書込み→メール送信
// 自作モジュールよりパッケージは先に読み込んだ方が綺麗に書ける import cron from "node-cron" import { addEmployeesToGS } from "./google-sheet.mjs" import { sendEmail } from "./email.mjs" // cron.schedule("52 15 * * *", () => { // main() // }) main() async function main() { const dt = new Date() const dtStr = dt.toDateString() const sheetUrl = `https://docs.google.com/spreadsheets/d/${process.env.GOOGLE_SHEET_ID}` try { await addEmployeesToGS() sendEmail("処理が成功しました。", `処理時刻:${dtStr}\n${sheetUrl}`) } catch (error) { sendEmail("エラーが発生しました。", `エラー発生時刻:${dtStr}\n${error}`) } }
import nodemailer from "nodemailer" import dotenv from "dotenv" dotenv.config() const sendEmail = async (subject, text) => { const message = { from: process.env.EMAIL_FROM, to: process.env.EMAIL_TO, subject: subject, text: text, } const smtpConfig = { host: "smtp.gmail.com", port: 465, secure: true, // SSL auth: { user: process.env.EMAIL_FROM, // googleアカウントのアプリパスワードを設定 // see https://support.google.com/accounts/answer/185833?hl=ja pass: process.env.APP_PASS, }, } const transporter = nodemailer.createTransport(smtpConfig) transporter.sendMail(message, function (err, response) { console.log(err || response) }) } export { sendEmail }
import { GoogleSpreadsheet } from "google-spreadsheet" import env from "dotenv" env.config() import { createRequire } from "module" const require = createRequire(import.meta.url) const secrets = require("../../../google_secrets.json") import { getEmployeesByScraping } from "./scraping.mjs" export const addEmployeesToGS = async () => { const doc = new GoogleSpreadsheet(process.env.GOOGLE_SHEET_ID) await doc.useServiceAccountAuth({ client_email: secrets.client_email, private_key: secrets.private_key, }) await doc.loadInfo() const employees = await getEmployeesByScraping() const sheet = doc.sheetsByTitle["scraping"] const rows = await sheet.addRows(employees) rows.forEach((row) => { row.save() }) }
import { chromium } from "@playwright/test" async function getEmployeesByScraping() { const browser = await chromium.launch() const page = await browser.newPage() await page.goto("http://localhost:3000") const cardLocators = page.locator(".cards.list-group-item") const cardCount = await cardLocators.count() const fetchedCards = [] for (let i = 0; i < cardCount; i++) { const cardLocator = cardLocators.locator(`nth=${i}`) const cardText = await cardLocator.textContent() await cardLocator.click() const companyLocator = page.locator(".card-title.company") const companyText = await companyLocator.textContent() fetchedCards.push({ company: companyText, name: cardText, }) const backLocator = page.locator("text=戻る") await backLocator.click() } await browser.close() return fetchedCards } export { getEmployeesByScraping }