SUUMOの検索結果から物件名と価格だけを抽出する
SUUMOで中古マンションを特定の条件で検索した結果ページをブックマークして定期的にチェックしている。マンション名と価格だけを抽出してぱっと確認したいが、デフォルトの検索結果ページは情報が多いので確認しづらい。
ということで、検索結果ページでブックマークレットを発火すると、そのページに表示されている内容からマンション名と価格を抽出して、ブラウザのダイアログに表示するものを作る。
要件
- ほしいのは
物件名 価格
を1行ずつ並べたもの - 同じ物件名の重複は除外する
- 億を超える価格のものは除外する
- あらかじめ「安い順」でソート済なので、2ページ目以降は無視でOK
要素を特定する
ブラウザのDevToolsで、欲しい要素がどう書かれているかを調べていくと、以下のようになっていることが分かった。
- 各物件の情報が載っている表の部分が
.property_unit-body
(赤) - 表の中の各行が
.dottable-line
(緑) - 行の中の
key
とvalue
がそれぞれ.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構文を使用した形にリファクタリングしてみました。
改善ポイント
for
ループの代わりにforEach
を使用 して、コードをシンプルに。querySelectorAll
の使用回数を削減 してパフォーマンスを向上。Set
を利用して重複チェック を簡潔に実装。- テンプレートリテラル を活用して文字列の組み立てを効率化。
このあとに具体的なコード→改善ポイントの詳しい説明、と続くのだが、この順序もとてもわかりやすい。人間の先生でもこのレベルの回答ができる人は多くない気がする。
全角→半角の変換も入れてもらい、変数名も調整してできたのが以下。とても読みやすいし、内容も洗練された。パフォーマンスを気にする量ではないが、改善されているはず。
const arrProperties = document.querySelectorAll('div.property_unit-body');
const setPropName = new Set();
let msg = '';
// 全角英数字を半角に変換する関数
function toHalfWidth(str) {
return str.replace(/[A-Za-z0-9]/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
かどうか」よりも「includes
がtrue
かどうか」 の方が読みやすい
- 「
- 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件目のdl
のdd
」として取得する。
坪数や (壁芯)
の表記は不要で、全角カッコで囲まれた部分を削除する処理を入れる。
また、上付き文字の部分が 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(/[A-Za-z0-9]/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