Skip to content
Go back
en

How to Hide Suspicious Real Estate Agencies on SUUMO [Tampermonkey]

Published:

I’m currently deep in the process of looking for a place to build the strongest possible remote-work setup, but companies that look like they might be posting bait listings are getting in the way, so I made a Tampermonkey script to exclude them from the search results. Sharing it here.

It works by copy-paste. If you are looking for a place on SUUMO and want to completely remove listings from specific companies from your search results, give it a try.

What is Tampermonkey?

Tampermonkey is a Chrome extension that runs JavaScript on websites in a convenient way. It is more flexible than bookmarklets, so it is handy.

The article below may be helpful. [For Beginners] Try JavaScript with Tampermonkey

The Script I Made

On SUUMO search result pages, it hides listings posted by specified companies (KC codes).

Example Usage

As an example, suppose you want to hide the following company (KC code). Note: This is only an example. Using an actually problematic company as the example would be scary, so I am using a completely normal ordinary real estate agency with no issues.

TR Navi Home Akasaka branch: https://suumo.jp/chintai/kaisha/kc_030_182730001/ Use this code: kc_030_182730001

Before

The listing in the middle of the image is posted by TR Navi Home Akasaka branch.

before

After

When you enable the Tampermonkey script I made for this article, it no longer appears in the search results.

after

Dedicated UI

I made it display values such as blocked (the number of entries in the blocklist), checked (the number of listings already checked), and removed (the number of listings hidden) in the bottom-right corner of the screen. Use it to confirm that the script is working properly.

UI displayed in the bottom-right corner

Script

It works by copy-paste. Note: If SUUMO changes its HTML structure, this may stop working. Please keep that in mind.

// ==UserScript==
// @name         SUUMO: Hide listings by provider company KC
// @namespace    https://blog.devkey.jp/
// @version      1.0.0
// @description  Hide only listings in SUUMO search results that match company codes (KC) specified in the blocklist
// @match        https://suumo.jp/*
// @run-at       document-end
// ==/UserScript==

(() => {
  "use strict";

  // Company codes for the real estate agencies you want to block (the /chintai/kaisha/kc_xxx_yyyy.../ part of the company page URL)
  // Note: The two entries below are samples. Be sure to replace them with the codes of the companies you want to block.
  const BLOCKED_KC = new Set([
    "kc_030_182730001", // Example: TR Navi Home Akasaka branch
    "kc_000_000000000",
  ]);

  const MAX_CONCURRENT_FETCH = 32;
  const FETCH_DELAY_MS = 120;

  const KC_TOKEN_RE = /\/chintai\/kaisha\/(kc_\d{3}_\d+)\//;

  const sleep = ms => new Promise(r => setTimeout(r, ms));

  // Badge UI
  const UI_ID = "__suumo_kc_badge__";
  const ui = (() => {
    const existing = document.getElementById(UI_ID);
    if (existing) existing.remove();

    const wrap = document.createElement("div");
    wrap.id = UI_ID;
    wrap.style.cssText =
      "position:fixed;z-index:2147483647;right:12px;bottom:12px;" +
      "background:rgba(0,0,0,.86);color:#fff;border:1px solid rgba(255,255,255,.12);" +
      "padding:10px 12px;border-radius:12px;font:12px/1.45 system-ui;" +
      "box-shadow:0 8px 28px rgba(0,0,0,.35);max-width:360px;min-width:240px;";

    const style = document.createElement("style");
    style.textContent = `
      #${UI_ID} .row{display:flex;align-items:center;justify-content:space-between;gap:8px}
      #${UI_ID} .title{font-weight:700;font-size:12px;display:flex;align-items:center;gap:8px}
      #${UI_ID} .dot{width:9px;height:9px;border-radius:99px;background:#2ecc71;box-shadow:0 0 0 2px rgba(46,204,113,.15)}
      #${UI_ID}[data-state="idle"] .dot{background:#95a5a6;box-shadow:0 0 0 2px rgba(149,165,166,.15)}
      #${UI_ID} .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}
      #${UI_ID} .muted{opacity:.82}
      #${UI_ID} .body{margin-top:8px}
    `;
    document.head.appendChild(style);

    wrap.innerHTML = `
      <div class="row">
        <div class="title">
          <span class="dot"></span>
          <span>SUUMO KC Filter</span>
          <span class="muted mono" id="kcState">RUNNING</span>
        </div>
      </div>
      <div class="body">
        <div class="muted">blocked: <span class="mono" id="kcBlocked">0</span></div>
        <div class="muted">queued: <span class="mono" id="kcQueued">0</span> / checked: <span class="mono" id="kcChecked">0</span> / removed: <span class="mono" id="kcRemoved">0</span></div>
        <div class="muted">active fetch: <span class="mono" id="kcActive">0</span></div>
      </div>
    `;
    document.body.appendChild(wrap);

    const els = {
      root: wrap,
      state: wrap.querySelector("#kcState"),
      blocked: wrap.querySelector("#kcBlocked"),
      queued: wrap.querySelector("#kcQueued"),
      checked: wrap.querySelector("#kcChecked"),
      removed: wrap.querySelector("#kcRemoved"),
      active: wrap.querySelector("#kcActive"),
    };

    return {
      setState(state) {
        els.root.dataset.state = state; // running | idle
        els.state.textContent = state.toUpperCase();
      },
      update({ blocked, queued, checked, removed, activeFetch }) {
        els.blocked.textContent = String(blocked);
        els.queued.textContent = String(queued);
        els.checked.textContent = String(checked);
        els.removed.textContent = String(removed);
        els.active.textContent = String(activeFetch);
      },
    };
  })();

  // Concurrency limit (simple queue)
  // When checking whether a real estate agency should be blocked,
  // this script fetches the destination of the "View details" link.
  // Fetching 100+ listings all at once can get heavy, so the queue runs them gradually.
  let activeFetch = 0;
  const queue = [];
  function limit(task) {
    return new Promise(resolve => {
      queue.push({ task, resolve });
      pump();
    });
  }
  function pump() {
    while (activeFetch < MAX_CONCURRENT_FETCH && queue.length) {
      const { task, resolve } = queue.shift();
      activeFetch++;
      Promise.resolve()
        .then(task)
        .then(resolve)
        .catch(() => resolve({ hide: false }))
        .finally(() => {
          activeFetch--;
          pump();
        });
    }
  }

  // Extract only the KC for the "provider (first company)" from the listing detail page
  function extractProviderKcToken(detailDoc, detailHtml, detailUrl) {
    {
      const m = String(detailUrl).match(KC_TOKEN_RE);
      if (m) return m[1];
    }

    const html = String(detailHtml || "");

    {
      const keys = [
        "\u3053\u306e\u7269\u4ef6\u3092\u53d6\u308a\u6271\u3046\u5e97\u8217",
        "\u53d6\u308a\u6271\u3044\u5e97\u8217",
      ];
      for (const key of keys) {
        const idx = html.indexOf(key);
        if (idx !== -1) {
          const part = html.slice(idx, idx + 160_000);
          const m = part.match(KC_TOKEN_RE);
          if (m) return m[1];
        }
      }
    }

    {
      const cassettes = Array.from(
        detailDoc.querySelectorAll(
          ".itemcassette, .itemcassette.l-space_medium"
        )
      );
      for (const cas of cassettes) {
        const a = cas.querySelector('a[href*="/chintai/kaisha/"]');
        const href = a?.getAttribute("href") || "";
        const m = href.match(KC_TOKEN_RE);
        if (m) return m[1];
      }
    }

    {
      const head = html.slice(0, 200_000);
      const m = head.match(KC_TOKEN_RE);
      return m ? m[1] : null;
    }
  }

  async function shouldHideByDetailUrl(detailUrl) {
    if (!detailUrl) return { hide: false };

    let abs;
    try {
      abs = new URL(detailUrl, location.origin).toString();
    } catch {
      return { hide: false };
    }

    return await limit(async () => {
      try {
        const res = await fetch(abs, { credentials: "include" });
        if (!res.ok) return { hide: false };

        const html = await res.text();
        const doc = new DOMParser().parseFromString(html, "text/html");

        const providerKc = extractProviderKcToken(doc, html, abs);
        const hide = providerKc ? BLOCKED_KC.has(providerKc) : false;

        await sleep(FETCH_DELAY_MS);
        return { hide };
      } catch {
        return { hide: false };
      }
    });
  }

  function getRoomRows() {
    const root = document.querySelector("#js-bukkenList") || document.body;

    const clipInputs = Array.from(root.querySelectorAll("input.js-clipkey"));
    if (clipInputs.length) {
      const rows = clipInputs
        .map(inp => inp.closest("tr") || inp.closest("li"))
        .filter(Boolean);
      return Array.from(new Set(rows));
    }

    return Array.from(root.querySelectorAll("ul.l-cassetteitem > li"));
  }

  // Pick links in order (less likely to grab the wrong link)
  function findDetailLinkInRow(row) {
    return (
      row.querySelector('a[href*="/chintai/jnc_"][href]') ||
      row.querySelector('a[href*="/chintai/bc_"][href]') ||
      row.querySelector('a[href*="bc="][href]') ||
      row.querySelector("a.js-cassette_link_href[href]")
    );
  }

  function cleanupEmptyCard(row) {
    const card = row.closest("ul.l-cassetteitem > li") || row.closest("li");
    if (!card) return;
    const still = card.querySelector("input.js-clipkey");
    if (!still) card.remove();
  }

  let removed = 0;
  let checked = 0;
  let queued = 0;

  async function processRow(row) {
    if (row.dataset.__kcProcessed) return;
    row.dataset.__kcProcessed = "1";

    const a = findDetailLinkInRow(row);
    if (!a?.href) return;

    queued++;
    ui.update({
      blocked: BLOCKED_KC.size,
      queued,
      checked,
      removed,
      activeFetch,
    });

    const { hide } = await shouldHideByDetailUrl(a.href);
    checked++;

    if (hide) {
      row.remove();
      removed++;
      cleanupEmptyCard(row);
    }

    ui.update({
      blocked: BLOCKED_KC.size,
      queued,
      checked,
      removed,
      activeFetch,
    });
  }

  let scanScheduled = false;
  function scheduleScan() {
    if (scanScheduled) return;
    scanScheduled = true;
    setTimeout(() => {
      scanScheduled = false;
      scan();
    }, 200);
  }

  function scan() {
    const rows = getRoomRows();
    if (!rows.length) {
      ui.setState("idle");
      ui.update({
        blocked: BLOCKED_KC.size,
        queued,
        checked,
        removed,
        activeFetch,
      });
      return;
    }

    ui.setState("running");
    ui.update({
      blocked: BLOCKED_KC.size,
      queued,
      checked,
      removed,
      activeFetch,
    });

    for (const row of rows) void processRow(row);
  }

  scan();

  const observeRoot =
    document.querySelector("#js-bukkenList") || document.documentElement;
  new MutationObserver(() => scheduleScan()).observe(observeRoot, {
    childList: true,
    subtree: true,
  });
})();

Closing Thoughts

There are quite a few real estate agencies that casually list properties that look like bait listings. I got pretty worn out by that, so if you are dealing with the same problem, please give this a try!


  • I Want to Use Browser Twitter’s Video Clip Feature on X.com Too! [Easy Video Trimmer for X.com]
  • How to Build a GAS Tool That Sends Tweets from Specific Twitter Accounts to a Discord Channel
  • How to Build a GAS Tool That Automatically Likes Tweets on Your Twitter Timeline
  • Handpicked Chrome Extensions That Make Reading English Documentation Much Easier
  • Recreating Twitter's old video clip feature as a Chrome extension

Previous Post
I Made an Anki Deck from the Words Included in Eigo de GO!, So I’m Sharing It
Next Post
Follow This and You Can Pass: My AWS Certified SysOps Administrator - Associate (SOA-C02) Exam Experience