🏘️

SUUMOの検索結果から物件名と価格だけを抽出する

2024/11/14に公開

SUUMOで中古マンションを特定の条件で検索した結果ページをブックマークして定期的にチェックしている。マンション名と価格だけを抽出してぱっと確認したいが、デフォルトの検索結果ページは情報が多いので確認しづらい。

ということで、検索結果ページでブックマークレットを発火すると、そのページに表示されている内容からマンション名と価格を抽出して、ブラウザのダイアログに表示するものを作る。

要件

  • ほしいのは 物件名 価格 を1行ずつ並べたもの
  • 同じ物件名の重複は除外する
  • 億を超える価格のものは除外する
  • あらかじめ「安い順」でソート済なので、2ページ目以降は無視でOK

要素を特定する

ブラウザのDevToolsで、欲しい要素がどう書かれているかを調べていくと、以下のようになっていることが分かった。

  • 各物件の情報が載っている表の部分が .property_unit-body(赤)
  • 表の中の各行が.dottable-line(緑)
  • 行の中の keyvalue がそれぞれ .dottable-vm(橙)

.dottable-line(緑)の1件目の中の、.dottable-vm(橙)の2件目」のように指定すれば、欲しい要素が取得できそう。

(残念ながら .property_name とか .property_price みたいな要素にはなっていなかった)

できたコード

自力であれこれ試した結果

javascript: (function() {
    const arrProperties = document.querySelectorAll('div.property_unit-body');
    let msg ='';
    for (let i = 0; i < arrProperties.length; i++) {
        const thisProperty = arrProperties[i].querySelectorAll('.dottable-line')[0].querySelectorAll('.dottable-vm')[1].textContent.trim();
        const thisPropertyPrice = arrProperties[i].querySelectorAll('.dottable-line')[1].querySelectorAll('.dottable-vm')[1].textContent.trim();
        if (thisPropertyPrice.indexOf('億') === -1){
            if (i === 0) {
                msg += `${thisProperty} ${thisPropertyPrice}`;
            } else {
                const prevProperty = arrProperties[i-1].querySelector('.dottable-line').querySelectorAll('.dottable-vm')[1].textContent.trim();
                if (thisProperty !== prevProperty) {
                    msg +=`\n${thisProperty} ${thisPropertyPrice}`;
                }
            }
        }
    }
    alert(msg);
})()

で一応欲しいものは出るようになった。

が、まだまだ問題がある。

  • 記述の重複が多すぎる。リファクタリングの余地は絶対ある。
  • マンション名の全角半角のゆらぎを吸収できない(たとえば ABマンションABマンション は別物になってしまって邪魔)

これを改善したい。


ChatGPTに相談

自分で考えても埒が明かないと感じたので、ChatGPTに「このコードを改善して」と依頼したら、数十秒で完璧な答えが帰ってきた。

いただいたJavaScriptのコードを拝見しました。このコードは、HTMLドキュメントから特定の要素を抽出し、重複を除いたプロパティ情報を整形してアラートで表示するもののようですね。

まず意図を完璧に把握された。そして続くのが改善ポイントの要約。

いくつか改善点がありますので、より効率的かつ読みやすく、最新のJavaScript構文を使用した形にリファクタリングしてみました。

改善ポイント

  1. for ループの代わりに forEach を使用 して、コードをシンプルに。
  2. querySelectorAllの使用回数を削減 してパフォーマンスを向上。
  3. Set を利用して重複チェック を簡潔に実装。
  4. テンプレートリテラル を活用して文字列の組み立てを効率化。

このあとに具体的なコード→改善ポイントの詳しい説明、と続くのだが、この順序もとてもわかりやすい。人間の先生でもこのレベルの回答ができる人は多くない気がする。

全角→半角の変換も入れてもらい、変数名も調整してできたのが以下。とても読みやすいし、内容も洗練された。パフォーマンスを気にする量ではないが、改善されているはず。

const arrProperties = document.querySelectorAll('div.property_unit-body');
const setPropName = new Set();
let msg = '';

// 全角英数字を半角に変換する関数
function toHalfWidth(str) {
    return str.replace(/[---]/g, char => String.fromCharCode(char.charCodeAt(0) - 0xFEE0));
}

// 指定した行の2番目の列のテキストを取得する関数
// (各行が「物件名 hogeマンション」のようになっているので、それぞれの2番目の要素を取得する)
function getValue(element) {
    return element.querySelectorAll('.dottable-vm')[1]?.textContent.trim() || '';
}

arrProperties.forEach((property, index) => {
    const keyValues = property.querySelectorAll('.dottable-line');
    // 1行目と2行目の情報を取得
    let propName = toHalfWidth(getValue(keyValues[0]));
    const propPrice = getValue(keyValues[1]);

    // 値段に「億」が含まれている場合はスキップ
    if (!propPrice.includes('億')) {
        // プロパティ名の重複を除外してセットに追加
        if (!setPropName.has(propName)) {
            setPropName.add(propName);
            msg += `${index > 0 ? '\n' : ''}${propName} ${propPrice}`;
        }
    }
});

// メッセージをアラートで表示
if (msg) alert(msg);

学び

  • ChatGPTに相談すると良い。人に聞くより、検索するより速く正確な答えが得られる可能性が十分ある。
  • まだまだ知らない表現があるので身に付けたい。少なくとも存在を思い出せるようにしたい(思い出せれば調べられるので)
    • 配列の要素ひとつひとつへの繰り返しには forEach を使う
    • 重複の排除には Set を使う
      • set.add(hoge) で、配列と同様に要素を追加する
        • 追加するだけで勝手に重複を除外してくれる
      • set.has(fuga) で、「setの中にfugaが既にあるかどうか」を調べられる
    • 「配列のN番目」を要求するときは、クエリ結果が存在しない場合でもエラーを防ぐために、optional chaining (?.) を使う
    • 「文字列の中に hoge を含むかどうか」だけを調べるときは、includes が便利
      • indexOf-1 かどうか」よりも「includestrue かどうか」 の方が読みやすい
    • if文でfalseの場合を指定するときは if(!hoge) と書く
    • 出力する文を作る時の「0番目以外は最初に改行を入れる」は、三項演算子を使えば1行で書ける

さらに改善

タグ名での要素指定

元のWebページのソースをよく見ると、

<div class="property_unit-body">
  <div classs="dottable-line">
    <dl>
      <dt class="dottable-vm">物件名</dt>
      <dd class="dottable-vm">◯◯マンション</dd>
    </dl>
  </div classs="dottable-line"></div>

のようになっていた。これであれば、クラス名で指定するより、タグ名 dl dd で指定して取得するほうが見やすいコードになる。

広さも入れたい

これもよく考えたら当然だったのだが、自分のニーズとしては「物件の広さ(専有面積)」も省略した一覧にほしい情報だった。同様にして「5件目のdldd」として取得する。

坪数や (壁芯) の表記は不要で、全角カッコで囲まれた部分を削除する処理を入れる。
また、上付き文字の部分が m<sup>2</sup> とHTMLで処理されているのを、プレーンテキストの時点で上付き文字になるように置換する処理も追加。

最新のコード

javascript:(function(){
const arrProps = document.querySelectorAll('div.property_unit-body');
const setPropNames = new Set();
let msg = '';

function toHalfWidth(str) {
    return str.replace(/[---]/g, char => String.fromCharCode(char.charCodeAt(0) - 0xFEE0));
}

function getDd(element) {
    return element.querySelector('dd')?.textContent.trim() || '';
}

function formatPropSize(str) {
    return str.replace(/[^]*/g, '').replace('m2','m²').trim();
}

arrProps.forEach((property, index) => {
    const dlAll = property.querySelectorAll('dl');
    const propName = toHalfWidth(getDd(dlAll[0]));
    const propPrice = getDd(dlAll[1]);
    const propSize = formatPropSize(getDd(dlAll[4]));

    if (!propPrice.includes('億')) {
        if (!setPropNames.has(propName)) {
            setPropNames.add(propName);
            msg += `${index > 0 ? '\n' : ''}${propName} ${propPrice}${propSize}`;
        }
    }
});

if (msg) alert(msg);
})();

Discussion