😄
Zenn記事リストを全文スクレイピングする GASを使って
GoogleスプレッドシートにリストしたZenn記事を全文スクレイピングしてみます。
インプットとなるスプレッドシートは下記です。
シート「List」
スクレイピングですが、スプレッドシート内で完結した方がシンプルに作れそうだったので、App Script(いわゆるGAS)で構築します。
GASのUrlFetchAppという機能を使ってスクレイピングしていきます。
下記はメイン処理です。インプットのシート「List」を1行ずつ読み込んでいきます。
全文スクレイピング結果の出力先はシート「Detail」です。
function scrapeBlogPosts() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceSheet = ss.getSheetByName("List");
var targetSheet = ss.getSheetByName("Detail");
var urls = sourceSheet.getRange("A2:A" + sourceSheet.getLastRow()).getValues();
var results = [];
results.push(["URL", "枝番", "Title", "記事全文"]);
urls.forEach(function(row) {
var url = row[0];
if (url) {
try {
var result = scrapeZennArticle(url);
var splitContent = splitLongContent(result.content);
splitContent.forEach(function(content, index) {
results.push([result.url, index + 1, result.title, content]);
});
} catch (e) {
Logger.log("Error scraping " + url + ": " + e.toString());
results.push([url, 1, "Error: " + e.toString(), ""]);
}
}
});
targetSheet.getRange(1, 1, results.length, 4).setValues(results);
Logger.log("Scraping and updating completed.");
}
下記は具体的にスクレイピングするFunction。
function scrapeZennArticle(url) {
var response = UrlFetchApp.fetch(url);
var content = response.getContentText();
var title = extractTitle(content);
var body = extractBody(content);
if (!title) {
throw new Error("Title not found");
}
if (!body) {
throw new Error("Content not found");
}
var plainText = removeHtmlTags(body);
return {
url: url,
title: title,
content: plainText
};
}
下記は、スクレイピングしたタイトルと本文を抽出するFunctionです。
function extractTitle(content) {
var titlePatterns = [
/<h1 class="ArticleTitle_title__[^"]*"[^>]*>([\s\S]*?)<\/h1>/i,
/<title>(.*?)<\/title>/i,
/<meta property="og:title" content="(.*?)"/i
];
for (var i = 0; i < titlePatterns.length; i++) {
var match = content.match(titlePatterns[i]);
if (match && match[1]) {
return match[1].trim();
}
}
return null;
}
function extractBody(content) {
var bodyPatterns = [
/<div class="BodyContent_anchorToHeadings__[^"]*"[^>]*>([\s\S]*?)<\/div>/i,
/<article[^>]*>([\s\S]*?)<\/article>/i,
/<div class="ArticleContent_content__[^"]*"[^>]*>([\s\S]*?)<\/div>/i
];
for (var i = 0; i < bodyPatterns.length; i++) {
var match = content.match(bodyPatterns[i]);
if (match && match[1]) {
return match[1];
}
}
return null;
}
下記はスクレイピングしたデータからHTMLタグを削除するFunctionと、スプレッドシートのセルの上限値を超えないように本文を分割するFunctionです。。
function removeHtmlTags(html) {
// コードブロックを保護
html = html.replace(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, function(match) {
return '[CODE]' + match + '[/CODE]';
});
// その他のHTMLタグを削除
var text = html.replace(/<[^>]+>/g, '');
// 保護したコードブロックを復元
text = text.replace(/\[CODE\]<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>\[\/CODE\]/gi, function(match, code) {
return '\n\nコードブロック:\n' + code + '\n\n';
});
// 特殊文字をデコード
text = text.replace(/ /g, ' ')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'");
// 余分な空白行を削除
text = text.replace(/\n\s*\n/g, '\n\n');
return text.trim();
}
function splitLongContent(content) {
const maxLength = 10000;
const chunks = [];
while (content.length > 0) {
if (content.length <= maxLength) {
chunks.push(content);
break;
}
let chunk = content.substr(0, maxLength);
let lastPeriodIndex = chunk.lastIndexOf('。');
if (lastPeriodIndex === -1) {
lastPeriodIndex = maxLength;
} else {
lastPeriodIndex += 1; // 句点を含める
}
chunks.push(chunk.substr(0, lastPeriodIndex));
content = content.substr(lastPeriodIndex);
}
return chunks;
}
GASを実行した結果は、下記。きちんと記事の全文が取得できました!
Discussion