😄

Zenn記事リストを全文スクレイピングする GASを使って

2024/08/26に公開

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(/&nbsp;/g, ' ')
             .replace(/&amp;/g, '&')
             .replace(/&lt;/g, '<')
             .replace(/&gt;/g, '>')
             .replace(/&quot;/g, '"')
             .replace(/&#039;/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を実行した結果は、下記。きちんと記事の全文が取得できました!

Accenture Japan (有志)

Discussion