
SEO施策などでブログやサイトのテコ入れを始めるとき、「どの記事をリライトすべきか?」「カテゴリや構成に偏りはないか?」といった整理が必要になりますよね。
まさに今、私も自サイトの記事を見直している最中で、記事ごとのタイトル/URL/カテゴリ/タグ/H1/H2/本文を一覧で見たくなりました。
プラグインでもある程度できるのですが、カスタマイズ性や再利用性を考えるといまいち。
そこで今回は、Google Apps Script(GAS)を使って、HTMLを直接パースする方法で一括抽出する仕組みを作ってみました。
1. 既存の方法で困ったこと
- WordPressの標準エクスポート機能では「本文」や「見出し構成」が取りづらい
- 管理画面では一覧性がなく、検索性も低い
- REST APIは使えるが、認証やフィールド取得の面倒さがある
- プラグインは極力入れたくない(セキュリティ/速度/管理面から)
2. GASで実装した方法
実装の流れはこんな感じです(Cocoon用の設定になってます):
- WordPressの
sitemap.xml
(投稿記事・固定ページ)を取得 - 各URLをループでクロール
- HTMLから以下の要素を正規表現で抽出:
<title>
(ページタイトル)<meta name="description">
(ディスクリプション)<h1>
,<h2>
(見出し)<meta property="article:section">
(カテゴリ).entry-content
ブロックから本文(HTMLタグ除去済)
- スプレッドシートに整形出力(1記事1行、H2とカテゴリは「/」区切り)
3. ChatGPTとの共創プロセス
このスクリプト(記事の一番下に載せています)は、最初から全部自作したわけではなく、ChatGPTと一緒に作ったものです。
最初に私から以下のような取得条件を提示しました:
- APIは使わず、HTMLでパースする
- タイトル・メタ情報・H1/H2/カテゴリ/本文を精度高く取りたい
- 複数の
sitemap.xml
に対応したい - HTMLのノイズ要素(
<script>
,<style>
)を除去したい
これらを伝えた上で、ChatGPTにベースのスクリプトを生成させ、そこから改造と調整を加えていくスタイルで進めました。
とくに、以下の部分はAIが生成・調整してくれたものを採用しています:
sitemap.xml
からのURL抽出ループ<h2>
タグ内部のHTMLを除去してテキスト化.entry-content
の範囲指定と本文のHTML除去処理- Googleスプレッドシートへの出力整形
4. 得られた成果と応用例
このスクリプトによって、以下のことが可能になりました:
- 薄い記事やH2構成が弱い記事を一覧から発見できる
- タグやカテゴリの偏りを見える化 → 再構成の判断材料に
- 本文の「書き出し」だけを一覧化 → リライトの優先度判断に
- 「カテゴリ別の文字数分布」なども、シート上で分析できる
さらにClarityやGSCのデータと突合すれば、「改善すべきページ×読まれてない記事」なども可視化できそうです。
5. まとめ:データを“取れる”と、ブログ運営が変わる
普段はGA4やBigQueryでクライアントのデータを見る立場ですが、
自分のメディアを“構造的に見る”手段を持つことが、思った以上に有効でした。
ChatGPTとGASを組み合わせれば、非エンジニアでも
- 「どの記事がどう見えているか」
- 「どう整理・改善していくか」
を、自分で可視化して判断するツールが作れます。
ぜひ気になる方は、真似して使ってみてください。
6. 参考:今回作成したGASスクリプト
下記が今回作成したスクリプトです。
出力先のシート名は「ブログ情報リスト」になっています。
このGASをそのままスプレッドシートで実行すると、当ブログの記事情報一覧が出てきます。
function crawlMultipleSitemaps() {
const sitemapUrls = [
'https://saito-online.com/wp-sitemap-posts-post-1.xml',
'https://saito-online.com/wp-sitemap-posts-page-1.xml'
];
const sheetName = "ブログ情報リスト";
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName(sheetName);
if (!sheet) {
sheet = ss.insertSheet(sheetName);
} else {
sheet.clearContents();
}
// カラムに「カテゴリー」を追加
sheet.appendRow(["URL", "タイトル", "Meta Description", "H1", "H2一覧(複数ある場合は「/」区切り)", "カテゴリ(複数ある場合は「/」区切り)", "本文"]);
sitemapUrls.forEach(sitemapUrl => {
const xml = UrlFetchApp.fetch(sitemapUrl).getContentText();
const urls = [...xml.matchAll(/<loc>(.*?)<\/loc>/g)].map(match => match[1]);
urls.forEach((url, i) => {
try {
const html = UrlFetchApp.fetch(url).getContentText();
const title = (html.match(/<title.*?>(.*?)<\/title>/i) || [])[1] || '';
const metaDesc = (html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"]+)["']/i) || [])[1] || '';
const h1 = (html.match(/<h1[^>]*>(.*?)<\/h1>/i) || [])[1] || '';
// H2抽出とタグ除去
const h2Matches = [...html.matchAll(/<h2[^>]*>(.*?)<\/h2>/gi)].map(match => {
const innerText = match[1].replace(/<[^>]+>/g, '').trim(); // spanなどを除去
return innerText;
});
const h2List = h2Matches.join(" / ");
// カテゴリー抽出
const categoryMatches = [...html.matchAll(/<meta[^>]*property=["']article:section["'][^>]*content=["']([^"]+)["']/gi)];
const categoryList = categoryMatches.map(match => match[1].trim()).join(" / ");
// 本文抽出(指定の範囲のみ)
const bodyMatch = html.match(/<div class="entry-content cf" itemprop="mainEntityOfPage">([\s\S]*?)<footer class="article-footer entry-footer">/i);
let bodyHtml = bodyMatch ? bodyMatch[1] : '';
const bodyText = bodyHtml
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<[^>]+>/g, '')
.replace(/\s+/g, ' ')
.trim();
const snippet = bodyText.substring(0, 4000);
// データ行追加
sheet.appendRow([url, title, metaDesc, h1, h2List, categoryList, snippet]);
Utilities.sleep(3000); // アクセス制限対策
} catch (e) {
Logger.log("Error on: " + url + " → " + e.message);
sheet.appendRow([url, "ERROR", "", "", "", "", ""]);
}
});
});
SpreadsheetApp.flush();
}