🦆
【GAS】収集した情報のタグ付けを自動化したい
なぜ作成したのか
- 情報収集を自動化したら分類がおざなりになってタグ付けに追われる羽目になったため
やりたいこと
- 毎日収集しているRSSFeedの情報のタイトルと登録済みタグをマッチングし、合致するものがあった場合は記事とタグを紐づける関係情報を追加する
- タグは「AWS」「AWS ECS」みたいに詳細化したタグも存在するので長い文字列のタグからマッチングする必要がある
- 一致しないやつはタグ付けしないでよい(あとでタグ登録含め、人力でやる)
既存の環境
シート「db」
column | description |
---|---|
feed_id | RSSFeed取得元情報源を示すID(別シートのプライマリKey) |
title | RSSFeedで取得した記事タイトル |
timestamp | RSSFeedから取得した公開日 |
link | RSSFeedから取得した記事URL |
content | RSSFeedから取得した記事概要 |
create_date | 取得処理を実行した日 |
exclude | 表示除外フラグ |
bookmark | ブックマークフラグ |
シート「Tags」
column | description |
---|---|
id | タグを一意に識別するID |
tag | タグ文字列 |
description | タグの解説文 |
parent-tag | 親子関係にある親タグのID(同一シート内IDを参照) |
シート「tagRelations」
column | description |
---|---|
id | タグと記事の関連性を識別する一意のID |
tag | tagsのidを参照 |
db | dbのlinkを参照 |
処理としてやりたいことの記述
処理対象とするスコープ
シート「db」のうち、シート「tagRelations」に関連レコードが存在しないレコード
処理概要
処理はGASで実装する
対象レコードのtitleに対して、「Tags」のTagに登録されている長い文字列から順にタグ文字列に一致する箇所を検索し、一致した場合は該当タグと記事との関連性があるものとみなし、その関連性を「tagRelations」にレコード追加する
一致しないものは既存タグに該当するものがないとして、レコードは追加しない
処理がタイムアウトにより中断されることを考慮し、繰り返し実行に耐えられるよう処理の一貫性を担保する
GAS
/**
* ──────────────────────────────────────────────
* RSS 記事タイトルと Tags をマッチングし tagRelations に追加
* - idempotent:同じ (link, tag_id) を二重登録しない
* - 長いタグから順に検索
* - 処理件数を制限してもデータ整合性が崩れない
* ──────────────────────────────────────────────
*/
const DB_SHEET = 'db';
const TAGS_SHEET = 'Tags';
const REL_SHEET = 'tagRelations';
const BATCH_SIZE = 400; // 1 回で処理する db 行数(タイムアウト対策)
const PROGRESS_KEY = 'tagger_last_row'; // ScriptProperties に進捗を保存
function tagArticles() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const dbSh = ss.getSheetByName(DB_SHEET);
const tagsSh = ss.getSheetByName(TAGS_SHEET);
let relSh = ss.getSheetByName(REL_SHEET);
// ---------- tagRelations シートの自動生成 & ヘッダー ----------
if (!relSh) {
relSh = ss.insertSheet(REL_SHEET);
relSh.appendRow(['id','tag','db']); // ヘッダー
}
// ---------- 既存 tagRelations をセット化 ----------
const relValues = relSh.getDataRange().getValues();
const relHeader = relValues.shift();
const relKeySet = new Set(relValues.map(r => `${r[1]}|${r[2]}`)); // "tagId|link"
// ---------- Tags シートを読み込んで tag 長さで降順ソート ----------
const tagValues = tagsSh.getDataRange().getValues();
tagValues.shift(); // ヘッダー除外
const tags = tagValues
.filter(r => r[1]) // tag 文字列が空でない
.map(r => ({id: String(r[0]), tag: String(r[1])}))
.sort((a,b) => b.tag.length - a.tag.length); // 長い -> 短い
// ---------- db シートで未処理行をバッチ取得 ----------
const lastRowProp = PropertiesService.getScriptProperties();
let startRow = Number(lastRowProp.getProperty(PROGRESS_KEY) || 2); // 1 行目はヘッダー
const lastRow = dbSh.getLastRow();
if (startRow > lastRow) startRow = 2; // 巡回完了 → 先頭へ
const endRow = Math.min(startRow + BATCH_SIZE - 1, lastRow);
const dbValues = dbSh.getRange(startRow, 1, endRow - startRow + 1, dbSh.getLastColumn())
.getValues();
const linkIdx = 3; // db シートにおける link 列 (0-based)
const titleIdx = 1; // title 列
const newRows = [];
for (const row of dbValues) {
const link = String(row[linkIdx]);
if (!link) continue; // 空行防止
const title = String(row[titleIdx]);
// 既に 1 レコードでも tagRelations があればスキップ
if ([...relKeySet].some(key => key.endsWith(`|${link}`))) continue;
// タグ検索(長い順)※同じ記事に複数タグ付与可
const lowerTitle = title.toLowerCase();
for (const {id: tagId, tag} of tags) {
if (lowerTitle.includes(tag.toLowerCase())) {
const relKey = `${tagId}|${link}`;
if (!relKeySet.has(relKey)) {
relKeySet.add(relKey);
newRows.push([Utilities.getUuid(), tagId, link]);
}
}
}
}
// ---------- 追加分を一括書き込み ----------
if (newRows.length) {
relSh.getRange(relSh.getLastRow()+1, 1, newRows.length, newRows[0].length)
.setValues(newRows);
}
// ---------- 進捗保存(次回は続きから) ----------
const nextStart = (endRow >= lastRow) ? 2 : endRow + 1;
lastRowProp.setProperty(PROGRESS_KEY, String(nextStart));
console.log(`Checked rows ${startRow}-${endRow}. Added ${newRows.length} relations.`);
}
所感
- 処理は数秒で完了してるのでタイムアウトは今のところ大丈夫そう
- 今のところ毎日の更新が100件強だからこの敷居で良いけれど、情報源、タグともに増加要員なので適宜チューニングが必要になるかも
- タグもちゃんと用意してないと「Amazon Quicksight」が「Amazon Q」でタグ付けされたりするから地道にタグを充実させよう。。。
Discussion