Edited at

SPAサイトのサイトマップをスクレイピングで自動生成した話


概要

SPAサイト全体をスクレイピングし、サイトマップを自動生成します。因みにSPAのフレームワークは、vue.jsを使用しました。

実装には、以下のNode.jsライブラリを使っています。



  • Chromy(Chromeをヘッドレスブラウザとして操作するためのライブラリ)


  • xml2js(XMLの解析と生成を行えるライブラリ)


処理の流れ

全体の流れを簡単に説明すると、スクレイピングのスタート地点を指定し、そこから全てのページ内のaタグのhref属性を取得していき、最終的にsitemap.xmlを生成します。


Chromyでスクレイピング

const Chromy = require('chromy')

async function scraping(chromy, array, to) {
await chromy.blockUrls(['*.ttf', '*.gif', '*.png', '*.jpg', '*.jpeg', '*.webp'])
await chromy.goto(to)

// 指定の要素が現れるまで処理を中断
await chromy.wait('body.is-content-loaded')

 // ページ内のaタグのhref属性を全て取得
const anchors = await chromy.evaluate(()=> {
const result = []
for(const item of document.body.getElementsByTagName('a')) {
if(item.target != '_blank') {
result.push(item.href)
}
}
return result
})

// 配列に格納
for(let item of anchors) {
if(array.indexOf(item) == -1) > -1) {
array.push(item)
}
}

await chromy.close()
}

async function start() {
const chromy = new Chromy()
const urls = ['https://sample.com']
let count = 0

do {
await scraping(chromy, urls, urls[count])
count++
} while (count < urls.length - 1)
}

start()

scraping()関数は、Webページへのスクレイピングと、スクレイピングした結果からaタグのhref属性を全ての取得し、urls配列に格納しています。

SPAではAPIからコンテンツデータを取得し、それをレンダリングする必要があるため、データを取得するにはレンダリングを待たなければなりません。そこでchromy.wait()関数で指定した要素(今回はbody.is-content-loaded)が現れるまで処理を停止しています。

またurls配列には、既に配列に格納されていない値のみ格納します。

start()関数は、urls配列に格納された値をdo while文でループし、scraping()関数に渡すことでサイト内の全てのページをスクレイピングします。

ループしつつ、scraping()関数がurls配列に重複のない値を格納していくため、最終的に格納できる値がなくなりスクレイピングは終了されます。


xml2jsでsitemap.xmlを生成

const xml2js = require('xml2js')

const fs = require('fs')

function xmlBuild(object) {
const array = []

// 配列を入れ子に
for(const item of object) {
array.push({ url: { loc: item } })
}

const builder = new xml2js.Builder({ rootName: 'urlset' }).buildObject({ $: { xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9' } })
const xml = builder.buildObject(array)
fs.writeFile('sitemap.xml', xml, 'utf8')
}

const urls = ['https://sample.com']
xmlBuild(urls)

配列の構造通りにXMLファイルが生成されるように、urls配列の値を入れ子にした新しい配列を生成しています。


今回のソースコード

const Chromy = require("chromy")

const xml2js = require("xml2js")
const fs = require("fs")

async function scrap(chromy, pushArray, to) {
await chromy.blockUrls(["*.ttf", "*.gif", "*.png", "*.jpg", "*.jpeg", "*.webp"])
await chromy.goto(to)
await chromy.wait("body.is-content-loaded")
const anchors = await chromy.evaluate(()=> {
const result = []
for(const item of document.body.getElementsByTagName("a")) {
if(item.target != "_blank") {
result.push(item.href)
}
}
return result
})

for(let item of anchors) {
if(pushArray.indexOf(item) == -1 && item.indexOf("http://") > -1) {
pushArray.push(item)
}
}

await chromy.close()
}

function xmlBuild(object) {
const array = []
for(const item of object) {
array.push({ url: { loc: item } })
}
const builder = new xml2js.Builder({ rootName: "urlset" }).buildObject({ $: { xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9" } })
const xml = builder.buildObject(array)
fs.writeFile("sitemap.xml", xml, "utf8")
}

async function scrapingStart() {
const chromy = new Chromy()
const urls = ['https://sample.com']
let count = 0

do {
await scrap(chromy, urls, urls[count])
count++
} while (count < urls.length - 1)

await xmlBuild(urls)
}

start()

今回は以上です。

実は、スクレイピングを試す前に既存のサイトマップ生成サービス、ライブラリも試してみたのですがどれも上手くいかなかったので、生成できるものを知ってる方がいれば、是非教えてください!