UNPKG

rsshub

Version:
173 lines (172 loc) 7.45 kB
import { t as rofetch } from "./ofetch-U0CdpE2g.mjs"; import { t as cache_default } from "./cache-C1Y-9qDV.mjs"; import { n as clean, s as parseJpy } from "./utils--J7EU6Hb.mjs"; import { load } from "cheerio"; import pMap from "p-map"; //#region lib/routes/inshokuten/rent-benchmark.ts const HOST = "https://www.inshokuten.com"; const MARKET = `${HOST}/bukken/kanto/market/rent`; const DETAIL_CONCURRENCY = 2; const DETAIL_CACHE_SECONDS = 86400; /** Area selection pages of the 首都圏 edition (`/market/rent/line/{slug}`); line ids on them are area-scoped. */ const AREAS = [ { slug: "23ward", label: "東京23区", pref: "東京都" }, { slug: "23ward_out", label: "東京都下", pref: "東京都" }, { slug: "chiba", label: "千葉", pref: "千葉県" }, { slug: "saitama", label: "埼玉", pref: "埼玉県" }, { slug: "yokohama_kawasaki", label: "神奈川", pref: "神奈川県" } ]; /** Selection pages list `<li><a href="…/result/?q[region_id]=10">千代田区</a>` (or `q[station_id]=243` for a line). */ const parseTargets = (html, param) => { const $ = load(html); return $(`a[href*="${param}"]`).toArray().map((a) => ({ name: clean($(a).text()) ?? "", url: new URL($(a).attr("href") ?? "", HOST).href })).filter((t) => t.name !== "" && t.url.includes(param)); }; /** Lines are `<a href="…/market/rent/station/2">JR山手線</a>` on the area page; the numeric id is scoped to that area. */ const parseLines = (html) => { const $ = load(html); return $("a[href*=\"/market/rent/station/\"]").toArray().map((a) => ({ id: $(a).attr("href")?.match(/\/station\/(\d+)/)?.[1] ?? "", name: clean($(a).text()) ?? "" })).filter((l) => l.id !== "" && l.name !== ""); }; /** * Detail page (`/market/rent/result/?q[region_id]=10` or `?q[line_id]=2&q[station_id]=243`): * h2.rate '千代田区の店舗賃料相場情報(直近1年間)', #rate-total th → td: 平均坪単価 / 最高坪単価 / 最低坪単価 / 一番多い階, * 賃料分布図 hidden inputs js-circle-graph-val1..6 = listing counts per rent bucket (A: 20万円未満 … F: 100万円以上), * yearly trend table (平均坪単価 by year). No 中央値 and no 更新日 are published. */ const parseDetail = (html, base) => { const $ = load(html); const cell = (label) => clean($(`#rate-total th:contains("${label}")`).first().next("td").text()); const period = $("h2.rate").first().text().match(/(([^)]+))/)?.[1] ?? null; const buckets = $("input[name^=\"js-circle-graph-val\"]").toArray().map((el) => Number($(el).attr("value"))).filter((n) => Number.isFinite(n)); const trend = $("#rate-total").eq(1).find("tr").toArray().map((tr) => clean($(tr).text())).filter((t) => t !== null && /^\d{4}年/.test(t)).join(" / "); const raw = Object.fromEntries([ ["avg", cell("平均坪単価")], ["max", cell("最高坪単価")], ["min", cell("最低坪単価")], ["top_floor", cell("一番多い階")], ["distribution", buckets.length > 0 ? buckets.join(",") : null], ["distribution_buckets", clean($(".right-graf-ex").text())], ["trend", trend === "" ? null : trend] ].filter((e) => e[1] !== null)); return { source: "inshokuten", ...base, rent_per_tsubo_jpy: null, rent_per_tsubo_median_jpy: null, rent_per_tsubo_avg_jpy: parseJpy(raw.avg ?? null), rent_per_tsubo_min_jpy: parseJpy(raw.min ?? null), rent_per_tsubo_max_jpy: parseJpy(raw.max ?? null), sample_count: buckets.length > 0 ? buckets.reduce((a, b) => a + b, 0) : null, period, raw }; }; const describe = (x) => [ x.rent_per_tsubo_avg_jpy === null ? null : `平均坪単価 ${x.rent_per_tsubo_avg_jpy.toLocaleString("ja-JP")}円`, x.rent_per_tsubo_max_jpy === null ? null : `最高 ${x.rent_per_tsubo_max_jpy.toLocaleString("ja-JP")}円`, x.rent_per_tsubo_min_jpy === null ? null : `最低 ${x.rent_per_tsubo_min_jpy.toLocaleString("ja-JP")}円`, x.sample_count === null ? null : `${x.period ?? ""}${x.sample_count}件`, x.raw.top_floor === void 0 ? null : `一番多い階 ${x.raw.top_floor}` ].filter((p) => p !== null).join(" / "); const toItem = (target, kind, pref) => cache_default.tryGet(target.url, async () => { const extra = parseDetail(await rofetch(target.url), { area_kind: kind, area_name: target.name, pref }); return { title: `${extra.area_name} 賃料相場 ${extra.period ?? ""}`.trim(), link: `${target.url}#rate-total`, guid: target.url, description: describe(extra), _extra: extra }; }, DETAIL_CACHE_SECONDS); const handler = async (ctx) => { const slug = ctx.req.param("area") ?? "23ward"; const lineId = ctx.req.param("line"); const area = AREAS.find((a) => a.slug === slug); if (!area) throw new Error(`Unknown area "${slug}", expected one of ${AREAS.map((a) => a.slug).join(", ")}`); const areaUrl = `${MARKET}/line/${area.slug}`; const areaHtml = await cache_default.tryGet(areaUrl, () => rofetch(areaUrl), DETAIL_CACHE_SECONDS); if (lineId === void 0) { const wards = parseTargets(areaHtml, "region_id"); return { title: `飲食店.COM 賃料相場 (${area.label}・市区町村別)`, link: areaUrl, language: "ja", item: await pMap(wards, (w) => toItem(w, "ward", area.pref), { concurrency: DETAIL_CONCURRENCY }) }; } const line = parseLines(areaHtml).find((l) => l.id === lineId); if (!line) throw new Error(`Line ${lineId} is not listed for ${area.label}; see ${areaUrl} for its line ids`); const lineUrl = `${MARKET}/station/${line.id}`; const stations = parseTargets(await cache_default.tryGet(lineUrl, () => rofetch(lineUrl), DETAIL_CACHE_SECONDS), "station_id"); return { title: `飲食店.COM 賃料相場 (${area.label}${line.name}・駅別)`, link: lineUrl, language: "ja", item: await pMap(stations, (s) => toItem(s, "station", area.pref), { concurrency: DETAIL_CONCURRENCY }) }; }; const route = { path: "/rent-benchmark/:area?/:line?", name: "賃料相場", url: "www.inshokuten.com", maintainers: ["pseudoyu"], handler, example: "/inshokuten/rent-benchmark/23ward", parameters: { area: { description: "首都圏 sub-area", default: "23ward", options: AREAS.map((a) => ({ value: a.slug, label: a.label })) }, line: { description: "Line id from the area page (`/bukken/kanto/market/rent/line/{area}`, e.g. `2` = JR山手線 in 東京23区); when given, one item per station on that line instead of one per 市区町村" } }, description: `Restaurant-property rent benchmarks (坪単価,消費税込み募集金額,直近 1 年間) published by 飲食店.COM. Without \`line\` the feed has one item per 市区町村 of the area; with \`line\` one item per station on that line. Each item's \`_extra\` carries \`rent_per_tsubo_avg_jpy\` / \`_min_jpy\` / \`_max_jpy\` (円 / 坪 / 月), \`sample_count\` (sum of the 賃料分布図 buckets), \`period\` and the raw site text; the site publishes no 中央値 and no 更新日,so \`rent_per_tsubo_median_jpy\` is always \`null\` and items carry no \`pubDate\`. Detail pages are cached for one day.`, categories: ["other"], features: { requireConfig: false, requirePuppeteer: false, antiCrawler: false, supportRadar: true }, radar: [{ source: ["www.inshokuten.com/bukken/kanto/market/rent/line/:area"], target: "/rent-benchmark/:area" }, { source: ["www.inshokuten.com/bukken/kanto/market/rent/station/:line"], target: "/rent-benchmark/23ward/:line" }] }; //#endregion export { handler, route };