スプレッドシート+ GAS で SUUMOの物件情報を自動収集!気になる物件をスクレイピングで賢く管理しよう!

2025/05/30に公開されました。
2025/06/03に更新されました。

Google Apps Script (GAS) を使って、SUUMO の物件詳細ページから特定の情報を抽出し、Google スプレッドシートに自動で書き込む方法


author: Kaoru

【GAS活用】賃貸物件探しを自動化! SUUMO の情報を Google スプレッドシートにスクレイピング!

賃貸物件探し、大変ですよね。 SUUMOで気になる物件を見つけても、そのたびに詳細ページを開いて家賃や駅からの距離などを確認するのは手間がかかります。複数の物件を比較したいときなんか、もう頭がゴチャゴチャに…!

「お、これ良さそう!」と思った物件のURLをリスト化して、必要な情報をスプレッドシートに一覧で自動収集できたら、どれだけ効率的になるでしょう?

この記事ではGoogle Apps Script (GAS) を使ってSUUMOの物件詳細ページから特定の情報を抽出し、Googleスプレッドシートに自動で書き込む方法を紹介します。プログラミング初心者の方でも取り組みやすいように、具体的なコードと手順を解説します。

この記事でできるようになること ✨

  • SUUMO物件のURLリストから、家賃、管理費、敷金、礼金、最寄駅、徒歩分数、面積、階建、築年数、向きなどの情報を自動で取得
  • 取得した情報をGoogleスプレッドシートで一覧管理
  • 家賃+管理費や敷金+礼金の合計を自動計算
  • 最上階の物件に自動で色を付けて注意喚起(筆者の個人的なこだわりです)

準備するもの 🛠️

  • Googleアカウント
  • インターネット環境
  • (お好みで)基本的なJavaScriptの知識(必須ではありませんが、コードを理解する助けになります)

さあ、物件探しをスマート化しましょう!


なぜ Google スプレッドシート + Apps Scriptなのか? 🤔

ウェブサイトからの情報収集(スクレイピング)というと、Pythonなどのプログラミング言語を使う方法が一般的ですが、Google Apps Scriptを使うことには以下のメリットがあります。

  • 環境構築が不要: Googleアカウントさえあれば、すぐに開発・実行できます。特別なソフトウェアのインストールは不要です。
  • 無料で使える: 個人利用の範囲であれば、Googleの無料枠で十分利用可能です。
  • スプレッドシートとの連携が容易: 今回のようにスプレッドシートと直接連携してデータの読み書きをするのが非常に簡単です。

ちょっとしたデータ収集には、非常に手軽で強力な組み合わせと言えます。


1. Google スプレッドシートの準備 📝

まずは、物件情報を書き込むためのスプレッドシートを用意します。

すぐ試してみたい方は、以下のリンクからスプレッドシートのテンプレートをご自身のGoogleドライブにコピーし、記事中のスクリプトをApps Scriptに貼り付けて実行してみてください!

ここをクリックしてスプレッドシートのテンプレートをコピーする

詳しい手順は、この先をじっくり読んでください。


  1. 新しいGoogleスプレッドシートを作成します。

  2. シートの1行目に、以下の見出しを入力します。この見出しの並び順が、スクリプトがデータを書き込む列の順序と対応します。

    ID SUUMO URL 家賃+管理費 敷金+礼金 路線 最寄駅 徒歩 面積 階建 築年数 向き 入居時期 都市ガス 分譲賃貸 独立洗面台 家賃(万) 管理費(万) 敷金 礼金 備考・コメント NG (コピペ推奨です!)

  3. 見栄えを良くするために、1行目をテーブル形式に変換(Ctrl + Alt + T ※環境による)したり、好みに合わせて装飾したりすると良いでしょう。これは必須ではありませんが、データの管理がしやすくなります。 (筆者はテーブル変換しましたが、必須ではありません)

スプレッドシートのテーブル変換後のイメージ 4. シートの名前を “SuumoData” に変更します。スクリプトはこのシート名を参照して動作します。

シート名をSuumoDataに変更したイメージ

これでスプレッドシート側の準備は完了です!


2. Google Apps Script (GAS) のコード作成 ⚙️

次に、SUUMOから情報を取得するためのApps Scriptを作成します。

  1. スプレッドシートを開いた状態で、メニューバーの「拡張機能」から「Apps Script」を選択します。

Apps Script選択メニューのイメージ

  1. 新しいタブでスクリプトエディタが開きます。「無題のプロジェクト」となっているはずです。

Apps Script無題のプロジェクトが開いたイメージ

  1. デフォルトのコード.gsというファイル名のままでも動作しますが、 main.gsのように役割を表す名前に変更すると管理しやすくなります。また、コードが長くなる場合は、関連する機能ごとにファイルを分割することで、可読性や保守性を高めることができます。必須ではありませんが、今後の拡張や見直しを考慮すると推奨される方法です。

  2. 以下のコードを、それぞれのファイル名で新規作成し、コピー&ペーストしてください。コードは機能ごとにいくつかのファイルに分けていますが、すべてを1つのファイルに書いても動作します。保守や可読性を考えると、分けておくのがおすすめです。

    • main.gs: スクリプト全体の流れを制御し、スプレッドシートの読み書きをするメイン処理。
    • validateUrl.gs: 入力されたSUUMOのURLを検証し、正規の形式に整形する処理。
    • fetchSuumoData.gs (またはWorkspaceSuumoData.gs): SUUMOの物件詳細ページのHTMLを取得し、必要な情報を抽出する核となる処理。
    • utils.gs: 数式の設定や特定の条件(最上階など)によるセルの書式設定の補助的な処理。

main.gs

ここに実装されている importSuumoData() がメイン関数。このスクリプトのエントリーポイントとなる関数が含まれています。スプレッドシートの各行を処理し、他のファイルの関数を呼び出してデータ取得と書き込みを行います。主にシートへの書き込みを担当しているが、分割の粒度は適当。列の定義はこのファイルだけに閉じているはずなので、入れ替えたければここを変えること。

/**
 * 広域定数の定義
 */
const SHEET_NAME = "SuumoData"; // シート名
const URL_COLUMN = 2; // URLを格納するB列
const OUTPUT_START_COLUMN = 3; // Suumoのデータを出力する開始列. C列から

/**
 * メイン関数: スプレッドシートのB列(URL)からSUUMOの物件情報を取得し、C列以降に書き込む
 */
function importSuumoData() {
  const dryRun = false; //デバッグ用フラグ

  const sheet =
    SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  if (!sheet) {
    console.error(`シート「${SHEET_NAME}」が見つかりません`);
    return;
  }

  const data = sheet.getDataRange().getValues(); // シート全データ取得

  for (let row = 1; row < data.length; row++) {
    // ヘッダーを除いて1行ごとに処理
    const urlCell = sheet.getRange(row + 1, URL_COLUMN); // URLが書かれたセル
    const outputStartCell = sheet.getRange(row + 1, OUTPUT_START_COLUMN); // データ書き込み開始セル

    // URLを取得して検証
    const url = validateAndRetrieveUrl(urlCell, outputStartCell);
    if (!url) continue;

    // 物件データ取得およびシートへの書き込み
    if (dryRun || !data[row][OUTPUT_START_COLUMN - 1]) {
      // C列が未取得の場合
      const propertyData = fetchAndValidateSuumoData(url, outputStartCell);
      console.log(propertyData); // debug表示
      if (!propertyData) continue;

      writePropertyDataToSheet(
        sheet,
        row + 1,
        OUTPUT_START_COLUMN,
        propertyData,
        dryRun,
      );

      // SUUMOへの負荷軽減のため、短時間での連続アクセスを避けるために少し待つ(任意だが推奨)
      // Utilities.sleep(500); // 例: 500ミリ秒待つ
    }
  }
  console.log("処理が完了しました。");
}

/**
 * 取得した物件情報をシートに書き込みます。
 * @param {Sheet} sheet - 対象のシートオブジェクト
 * @param {number} rowNum - 書き込み対象の行番号
 * @param {number} startColNum - 書き込み開始列番号
 * @param {Object} propertyData - 物件情報オブジェクト
 * @param {boolean} dryRun - ドライラン実行フラグ
 */
function writePropertyDataToSheet(
  sheet,
  rowNum,
  startColNum,
  propertyData,
  dryRun,
) {
  // 路線、駅名、徒歩分数をそれぞれの列に結合して改行で区切り
  const lines = propertyData.access.map((item) => item.line).join("\n");
  const stations = propertyData.access.map((item) => item.station).join("\n");
  const walks = propertyData.access.map((item) => item.walk).join("\n");

  // 1行分のデータを連想配列に格納(ここでどの列にどのデータが入るかが定義される)
  // スプレッドシートのヘッダー: ID SUUMO URL 家賃+管理費 敷金+礼金 路線 最寄駅 徒歩 面積 階建 築年数 向き 入居時期 都市ガス 分譲賃貸 独立洗面台 家賃(万) 管理費(万) 敷金 礼金 備考・コメント NG
  const updatedData = {
    rentAndManagementFee: "", // C列: 家賃+管理費の列. 後で数式を設定するため空白
    depositAndKeyMoney: "", // D列: 敷金+礼金の列. 後で数式を設定するため空白
    lines: lines, // E列: 路線
    stations: stations, // F列: 最寄駅
    walks: walks, // G列: 徒歩分
    area: propertyData.area, // H列: 専有面積
    buildingFloors: propertyData.buildingFloors, // I列: 階建
    builtYear: propertyData.builtYear, // J列: 築年数
    direction: propertyData.direction, // K列: 向き
    moveInDate: propertyData.moveInDate, // L列: 入居時期
    gas: propertyData.gas, // M列: 都市ガス ('Yes' or 'No')
    condo: propertyData.condo, // N列: 分譲賃貸 ('Yes' or 'No')
    washbasin: propertyData.washbasin, // O列: 独立洗面台など ('洗面台独立' など)
    rent: propertyData.rent, // P列: 家賃(万) (文字列)
    managementFee: propertyData.managementFee
      ? parseFloat(String(propertyData.managementFee).replace(/,/g, "")) / 10000
      : "", // Q列: 管理費(円→万変換、数値または空)
    deposit: propertyData.depositAndKeyMoney[0], // R列: 敷金(万) (文字列またはnull)
    keyMoney: propertyData.depositAndKeyMoney[1], // S列: 礼金(万) (文字列またはnull)
  };

  if (!dryRun) {
    // シートにデータを書き込み
    sheet
      .getRange(rowNum, startColNum, 1, Object.values(updatedData).length)
      .setValues([Object.values(updatedData)]);

    // C列: 家賃+管理費の合計を設定
    applySumFormula(
      sheet,
      rowNum,
      updatedData,
      "rentAndManagementFee",
      "rent",
      "managementFee",
      startColNum,
    );

    // D列: 敷金+礼金の合計を設定
    applySumFormula(
      sheet,
      rowNum,
      updatedData,
      "depositAndKeyMoney",
      "deposit",
      "keyMoney",
      startColNum,
    );

    // I列: 最上階の警告表示のための書式設定
    warnIfTopFloor(
      sheet,
      rowNum,
      Object.keys(updatedData).indexOf("buildingFloors") + startColNum,
    );
  }
}

validateUrl.gs

URLを整形する系の関数群です。

/**
 * URLを検証して取得する。無効な場合はエラーをスプレッドシートの指定したセルに記録。
 * @param {Range} urlCell - URLが書かれたセルのRangeオブジェクト
 * @param {Range} resultCell - エラーを書き込むセルのRangeオブジェクト
 * @returns {string|null} 有効なURL、またはnull
 */
function validateAndRetrieveUrl(urlCell, resultCell) {
  let url = getUrlFromCell(urlCell);
  if (!url) {
    resultCell.setValue("URLが無効です");
    return null;
  }

  if (isPattern2Url(url)) {
    const bcCode = extractBcCode(url);
    const pattern1Url = `https://suumo.jp/chintai/bc_${bcCode}/`;

    if (isUrlAccessible(pattern1Url)) {
      replaceUrlInCell(urlCell, pattern1Url);
      url = pattern1Url;
    } else {
      resultCell.setValue("URLが無効かアクセス不可");
      return null;
    }
  }

  if (!isUrlAccessible(url)) {
    resultCell.setValue("URLが無効かアクセス不可");
    return null;
  }

  return url;
}

/**
 * セルからURLを取得する
 */
function getUrlFromCell(cell) {
  const formula = cell.getFormula();
  // セルがハイパーリンクの場合はリンク先のURLを取得
  if (formula.startsWith("=HYPERLINK")) {
    return formula.match(/"(https?:\/\/.*?)"/)?.[1] || "";
  }

  // セルがハイパーリンクの場合かつリッチテキスト形式の場合
  const richText = cell.getRichTextValue();
  if (richText) {
    return richText.getLinkUrl();
  }

  // セルが通常の文字列としてURLを持つ場合
  return cell.getValue();
}

/**
 * URLがパターン2かを判定する
 */
function isPattern2Url(url) {
  return url.includes("/jnc_") && url.includes("?bc=");
}

/**
 * パターン2のURLからbcコードを抽出する
 */
function extractBcCode(url) {
  const match = url.match(/bc=(\d+)/);
  return match ? match[1] : null;
}

/**
 * URLがアクセス可能かを確認する
 */
function isUrlAccessible(url) {
  try {
    const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
    return (
      response.getResponseCode() >= 200 && response.getResponseCode() < 300
    );
  } catch (e) {
    return false;
  }
}

/**
 * セルのURLを置き換える. 元の形式を尊重する
 */
function replaceUrlInCell(cell, newUrl) {
  const formula = cell.getFormula();
  if (formula.startsWith("=HYPERLINK")) {
    const updatedFormula = formula.replace(/"(https?:\/\/.*?)"/, `"${newUrl}"`);
    cell.setFormula(updatedFormula);
  } else if (cell.getRichTextValue()) {
    const text = cell.getRichTextValue().getText();
    const richText = SpreadsheetApp.newRichTextValue()
      .setText(text)
      .setLinkUrl(newUrl)
      .build();
    cell.setRichTextValue(richText);
  } else {
    cell.setValue(newUrl);
  }
}

fetchSuumoData.gs

Suumoのウェブサイトからデータを拾ってくる関数群です。

// テスト用関数.
function testFetch() {
  // fetchSuumoData('https://suumo.jp/chintai/bc_100417068336/')
  // fetchSuumoData('https://suumo.jp/chintai/bc_100417411230/')
  fetchSuumoData("https://suumo.jp/chintai/bc_100411130329/"); // 独立洗面台
}

/**
 * URLから物件情報を取得し、エラーがあればスプレッドシートに記録する
 * @param {string} url - 取得対象のURL
 * @param {Range} resultCell - エラーを書き込むセルのRangeオブジェクト
 * @returns {Object|null} 取得した物件情報オブジェクト、またはnull
 */
function fetchAndValidateSuumoData(url, resultCell) {
  const propertyData = fetchSuumoData(url);
  if (!propertyData) {
    resultCell.setValue("取得エラー");
    return null;
  }
  return propertyData;
}

/**
 * 指定されたSUUMOのURLから物件情報を取得し、オブジェクトで返す関数
 * @param {string} url - SUUMOの物件URL
 * @return {Object} 物件情報を格納したオブジェクト
 */
function fetchSuumoData(url) {
  try {
    const html = UrlFetchApp.fetch(url).getContentText();

    // 各要素を抽出
    const rent = extractRent(html);
    const managementFee = extractManagementFee(html);
    const depositAndKeyMoney = extractDepositAndKeyMoney(html);
    const access = extractAccess(html);
    const area = extractArea(html);
    const buildingFloors = extractBuildingFloors(html);
    const builtYear = extractBuiltYear(html);
    const direction = extractDirection(html);
    const moveInDate = extractMoveInDate(html);

    // gas, condo の情報を正規表現で抽出
    const gasMatch = html.match(/都市ガス/);
    const condoMatch = html.match(/分譲賃貸/);
    const washbasin = html.match(/洗面台独立|シャワー付洗面台/);

    // 結果をオブジェクトとしてまとめる
    return {
      rent: rent, // 家賃(万円)
      managementFee: managementFee, // 管理費・共益費(円)
      depositAndKeyMoney: depositAndKeyMoney, //敷金と礼金(万円):arrayであることに注意. 受け取り側で分離する
      access:
        access.length > 0
          ? access
          : [{ line: "情報なし", station: "情報なし", walk: "情報なし" }], // アクセス情報がない場合のデフォルト値
      area: area, // 専有面積(m²)
      buildingFloors: buildingFloors, // 階建(例:3階/6階建)
      builtYear: builtYear, // 築年数(年)
      direction: direction, // 向き(例:東南西北)
      moveInDate: moveInDate, //入居時期
      gas: gasMatch ? "Yes" : "No", // 都市ガス
      condo: condoMatch ? "Yes" : "No", // 分譲賃貸
      washbasin: washbasin ? washbasin[0] : "", //独立洗面台
    };
  } catch (e) {
    Logger.log(`Error fetching data from ${url}: ${e.message}`);
    return null;
  }
}

/**
 * 家賃(賃料・初期費用)を抽出
 * @param {string} html - HTMLコンテンツ
 * @return {string} 家賃(万円)
 */
function extractRent(html) {
  const rentMatch = html.match(
    /<span class="property_view_detail-header-title">賃料・初期費用[\s\S]*?([0-9,.]+)((?:万円))/,
  );
  return rentMatch ? rentMatch[1] : "";
}

/**
 * 管理費・共益費を抽出
 * @param {string} html - HTMLコンテンツ
 * @return {string} 管理費・共益費(円)
 */
function extractManagementFee(html) {
  const managementFeeMatch = html.match(
    /<div class="property_data-title">管理費・共益費[\s\S]*?([0-9,]+)()/,
  );
  return managementFeeMatch ? managementFeeMatch[1] : "";
}

/**
 * 敷金と礼金を抽出する関数
 * @param {string} html - HTMLコンテンツ
 * @return {Array} 敷金, 礼金(万円)
 */
function extractDepositAndKeyMoney(html) {
  // 敷金と礼金を格納する変数
  let securityDeposit = null;
  let keyMoney = null;

  // 適切にマッチさせるため、2段階でマッチさせる. 1段階目は大雑把
  const preMatch = html.match(
    /<div class="property_data-title">(敷金\/礼金[\s\S]*?)<div class="property_data-title">保証金/,
  );

  // 敷金/礼金の部分を抽出するための正規表現
  const regex =
    /敷金\/礼金<\/div>\s*<div class="property_data-body">[\s\S]*?([0-9.]+万円|-)<\/span>[\s\S]*?([0-9.]+万円|-)/;

  // 2段階目
  const text = preMatch ? preMatch[1] : null;
  const matches = text.match(regex);
  if (matches && matches.length >= 3) {
    // 敷金と礼金を取得。"-" の場合は null に設定
    securityDeposit =
      matches[1].trim() === "-" ? null : matches[1].replace("万円", "").trim();
    keyMoney =
      matches[2].trim() === "-" ? null : matches[2].replace("万円", "").trim();
  }

  // 敷金と礼金の値を配列として返す
  return [securityDeposit, keyMoney];
}

/**
 * 最寄駅情報を抽出
 * @param {string} html - HTMLコンテンツ
 * @return {Array} 最寄駅情報(路線名, 駅名, 徒歩分数)
 */
function extractAccess(html) {
  // 2段階でマッチさせる
  const accessPreMatch = html.match(/<!-- アクセス[\s\S]*?(<!-- 所在地)/);
  const accessMatch = accessPreMatch
    ? accessPreMatch[0].match(/property_view_detail-text">([\s\S]*?)<\/div>/g)
    : null;
  if (!accessMatch) return [];

  const accessData = accessMatch.map((item) => {
    const [lineStation, walk] = item.match(/([^\d]+)(\d+)分/).slice(1, 3);
    const [line, station] = lineStation.split("/");
    return {
      line: line.replace(/property_view_detail-text">/, "").trim(),
      station: station.replace(/駅 歩/, "").trim(),
      walk: walk.trim(),
    };
  });

  return accessData;
}

/**
 * 専有面積を抽出
 * @param {string} html - HTMLコンテンツ
 * @return {string} 専有面積(m²)
 */
function extractArea(html) {
  const areaMatch = html.match(
    /<div class="property_data-title">専有面積[\s\S]*?([0-9,.]+)(m<sup>2)/,
  );
  return areaMatch ? areaMatch[1] : "";
}

/**
 * 階建情報を抽出
 * @param {string} html - HTMLコンテンツ
 * @return {string} 階建(例:3階/6階建, 2階/地下1地上2階建)
 */
function extractBuildingFloors(html) {
  // 「X階/地下Y地上Z階建」または「X階/Y階建」にマッチする正規表現
  const floorsMatch = html.match(/(\d+)\/(地下\d+地上\d+階建|\d+階建)/);
  return floorsMatch ? floorsMatch[0] : "";
}

/**
 * 築年数を抽出
 * @param {string} html - HTMLコンテンツ
 * @return {string} 築年数(年)
 */
function extractBuiltYear(html) {
  const builtYearMatch = html.match(
    /<div class="property_data-title">築年数[\s\S]*?(\d+)()/,
  );
  return builtYearMatch ? builtYearMatch[1] : "";
}

/**
 * 向き情報を抽出
 * @param {string} html - HTMLコンテンツ
 * @return {string} 向き(例:東南西北)
 */
function extractDirection(html) {
  const directionMatch = html.match(
    /<div class="property_data-title">向き[\s\S]*?([東南西北-]+)(<\/div>)/,
  ); // 閉じタグでマッチさせる
  return directionMatch ? directionMatch[1] : "";
}

/**
 * 入居時期をHTMLから抽出する関数
 * @param {string} html - HTMLコンテンツ
 * @return {string|null} 入居時期(例: "相談")。該当データが見つからない場合は null を返す。
 */
function extractMoveInDate(html) {
  // 正規表現で "入居" の見出しとその値を抽出
  const regex = /<th class="data_01" scope="cols">入居<\/th>\s*<td>(.*?)<\/td>/;
  const match = html.match(regex);

  // マッチした場合は値を返し、そうでない場合は空文字列を返す
  if (match) {
    // 特殊文字をデコード
    return decodeHtmlEntities(match[1].trim());
  }
  return "";
}

/**
 * HTMLエンティティを対応する文字にデコードする関数
 * @param {string} encodedStr - エンコードされたHTMLエンティティを含む文字列
 * @return {string} - デコードされた文字列
 */
function decodeHtmlEntities(encodedStr) {
  // HTMLエンティティとその対応する文字のマッピング
  const entityMap = {
    "&#39;": "'", // アポストロフィ
    "&#039;": "'", // アポストロフィ(別の表記)
    "&quot;": '"', // 二重引用符
    "&lt;": "<", // 小なり
    "&gt;": ">", // 大なり
    "&amp;": "&", // アンパサンド
    "&#x2F;": "/", // スラッシュ
    "&nbsp;": " ", // ノンブレークスペース
  };

  // エンティティを対応する文字に置換
  return encodedStr.replace(/&[#a-zA-Z0-9]+;/g, (match) => {
    return entityMap[match] || match; // マッチするエンティティがあれば置換
  });
}
`- 提供されている decodeHtmlEntities 関数は、特定のHTMLエンティティのみを処理するシンプルな実装です。SUUMOのページでこれら以外のエンティティ(例: &nbsp; 以外の空白文字や、より多くの特殊記号)が使用されていた場合、正しくデコードされない可能性があります。[^1]

この関数の限界について、読者に明確に伝える(例:「この関数は主要なHTMLエンティティに対応していますが、全てのエンティティを網羅しているわけではありません」といった注釈を加える)か、あるいはGASで利用可能なより堅牢な方法(例:XmlService.parse() を利用してHTMLをパースしテキストコンテンツを取得する方法。ただし、この方法は少し複雑になる可能性があります)

### `utils.gs`

utils.gs

```javascript
/**
 * 指定したセルに列+列の合計を示す式を適用します。
 * @param {Sheet} sheet - 対象のシートオブジェクト
 * @param {number} rowIndex - 行番号(1ベース)
 * @param {Object} updatedData - キーとデータの連想配列
 * @param {string} targetKey - 合計結果を設定する列のキー名
 * @param {string} operandKey1 - 加算対象1の列のキー名
 * @param {string} operandKey2 - 加算対象2の列のキー名
 * @param {number} offset - 列インデックスのオフセット値
 */
function applySumFormula(
  sheet,
  rowIndex,
  updatedData,
  targetKey,
  operandKey1,
  operandKey2,
  offset,
) {
  const keyList = Object.keys(updatedData);
  const targetColumnIndex = keyList.indexOf(targetKey) + offset;
  const operandColumnIndex1 = keyList.indexOf(operandKey1) + offset;
  const operandColumnIndex2 = keyList.indexOf(operandKey2) + offset;

  // 指定したセルに =P1+Q1 のような数式を設定する
  sheet
    .getRange(rowIndex, targetColumnIndex)
    .setFormula(
      "=" +
        numToColumn(operandColumnIndex1) +
        rowIndex +
        "+" +
        numToColumn(operandColumnIndex2) +
        rowIndex,
    );
}

/**
 * 数値をExcelの列形式(A, B, ..., Z, AA, AB, ...)に変換します。
 * @param {number} index - 列番号(1ベース)
 * @returns {string} 列を表すアルファベット
 */
function numToColumn(index) {
  let column = "";
  while (index > 0) {
    const remainder = (index - 1) % 26;
    column = String.fromCharCode(65 + remainder) + column;
    index = Math.floor((index - 1) / 26);
  }
  return column;
}

/**
 * 最上階の場合に警告を出す関数。
 * 最上階であるかどうかをチェックし、警告を表示します。
 *
 * @param {Sheet} sheet - 警告を表示する対象のシート。
 * @param {number} rowNum - 対象となる行番号。
 * @param {number} floorsColumnNum - 階数が記載されている列番号。
 */
function warnIfTopFloor(sheet, rowNum, floorsColumnNum) {
  sheet.getRange(rowNum, floorsColumnNum).setBackground(null);
  const value = sheet.getRange(rowNum, floorsColumnNum).getValue();
  if (typeof value !== "string") {
    // 値が文字列でない場合は処理をスキップ
    console.log(
      "階建情報が文字列ではありません: " + value + " (行: " + rowNum + ")",
    );
    return;
  }
  const match = value.match(/(\d+)\/(?:地下\d+地上)?(\d+)階建/);
  if (match) {
    const currentFloor = parseInt(match[1], 10); // x階
    const totalFloors = parseInt(match[2], 10); // y階建
    // 最上階判定
    if (currentFloor === totalFloors) {
      sheet.getRange(rowNum, floorsColumnNum).setBackground("#f4c7c3");
    }
  } else {
    console.log("値の形式が不正です:" + value + " (行: " + rowNum + ")");
  }
}

さあ、スクリプトを実行してみましょう!🚀

準備が整ったら、いよいよスクリプトを実行してSUUMOの物件情報を自動で取得してみましょう。 まずは、スプレッドシートに調査したい物件のURLをいくつか用意します。こんな感じですね! 対象物件のイメージ

次に、B列(SUUMO URLを記載した列)のセルを選択し、「リンクを挿入」(ショートカットキー: Ctrl + K または Cmd + K) を使って、実際の物件ページのURLを貼り付けます。 リンク挿入のイメージ

※もちろんURLをセルに直接テキストとして入力してもスクリプトは動作しますが、ハイパーリンクにしておくと後からどんな物件だったか一目で分かるので便利です。ちょっとした工夫でぐっと使いやすくなります!

情報の取得対象とする行のC列が空白になっていることを確認したら、いよいよApps Scriptの出番です! スプレッドシートのメニューから「拡張機能」>「Apps Script」を選び、スクリプトエディタを開きます。 main.gs ファイルが表示されていることを確認し、実行する関数として importSuumoData を選択。そして「実行」ボタンをクリックしましょう! Apps Script実行のイメージ

ドキドキの瞬間ですね!

初めての実行? スクリプトの承認ステップ ✨

スクリプトを初めて実行するときだけGoogleから「このスクリプト、本当に動かして大丈夫?」という確認(承認作業)を求められます。これはスクリプトが皆さんのデータにアクセスするための大切なステップです。

  1. 「承認が必要です」というダイアログが表示されたら、落ち着いて「権限を確認」をクリックします。 権限確認1「承認が必要です」

  2. ご自身のアカウントを選択すると、「このアプリはGoogleで確認されていません」といった画面が出ることがあります。その場合は、左下にある「詳細」(または「Advanced」)をクリックし、次に「${プロジェクト名}(安全ではないページ)に移動」というリンクをクリックしてください。(${プロジェクト名} の部分は、あなたがApps Scriptプロジェクトに付けた名前に置き換わります。) 権限確認2「安全ではないページへ移動」

  3. 次に、このスクリプトが何に対して許可を求めているか(例えば「Googleスプレッドシートのすべてのスプレッドシートの参照、編集、作成、削除」など)の一覧が表示されます。内容を確認し、右下の「許可」(または「Allow」)をクリックします。 権限確認3「許可」

これで承認作業は完了です!2回目以降の実行では、このステップは表示されません。

やったね!成功すればこんな感じに🎉

スクリプトが無事に実行されれば、スプレッドシートに物件情報がズラッと並んでいるはずです! 実行成功時のスプレッドシートイメージ

ちょっとしたポイントと注意点 💡

このスクリプトをより快適に使うために、いくつか知っておくと良い点があります。

  • 実行の判断基準: 現在のスクリプトでは、各行のC列が空白かどうかを見て、情報を取得するかどうかを判断しています。これはシンプルな仕組みですが、もし再取得したい場合はC列のデータを一度クリアしてください。
  • 非表示のデータ列: スプレッドシートのP列、Q列、R列、S列には、それぞれ「家賃」「管理費」「敷金」「礼金」の元データが入っています。これらはC列とD列の計算に使われるため重要ですが、普段は表示されていなくても良い場合もあります。そんな時は、これらの列を選択して右クリックし「列をグループ化」を選んで折りたたんでおくと、シートがスッキリします。
  • スクレイピングの安定性について: fetchSuumoData.gs では、SUUMOのウェブページのHTML構造を解析して情報を取得しています(正規表現というテクニックを使っています)。現時点では動作していますが、もしSUUMO側のウェブページの構成が大きく変わると、情報がうまく取得できなくなる可能性がある点はご了承ください。その場合は、スクリプトの該当箇所を修正する必要があります。
  • 使用上の注意: ウェブサイトから情報を取得する際は、サーバーに過度な負荷をかけないよう配慮することが重要です。短時間に大量のリクエストを送らないように注意して下さい。また、ウェブサイトによっては利用規約やrobots.txtでスクレイピングや取得したデータに関する規約が設けられていることがあります。そのような規約を遵守するようにしましょう。

※本記事は、ジーアイクラウド株式会社の見解を述べたものであり、必要な調査・検討は行っているものの必ずしもその正確性や真実性を保証するものではありません。

※リンクを利用する際には、必ず出典がGIC dryaki-blogであることを明記してください。
リンクの利用によりトラブルが発生した場合、リンクを設置した方ご自身の責任で対応してください。
ジーアイクラウド株式会社はユーザーによるリンクの利用につき、如何なる責任を負うものではありません。