rsshub
Version:
Make RSS Great Again!
202 lines (201 loc) • 8.67 kB
JavaScript
import { t as rofetch } from "./ofetch-U0CdpE2g.mjs";
import { t as logger } from "./logger-DlhSwgkg.mjs";
import { t as cache_default } from "./cache-C1Y-9qDV.mjs";
import { c as parseMonths, d as parseWard, h as tsuboUnit, i as parseArea, m as summarize, n as clean, o as parseHeavyFood, r as normalizeFloor, s as parseJpy, u as parseWalkMin } from "./utils--J7EU6Hb.mjs";
import { load } from "cheerio";
import pMap from "p-map";
//#region lib/routes/temponw/estates.ts
const HOST = "https://www.temponw.com";
const DETAIL_CONCURRENCY = 2;
const PAGES = 2;
/** Tokyo 23 wards as the site's `cities[]` codes (101 千代田区 … 123 江戸川区). */
const TOKYO_WARDS = Array.from({ length: 23 }, (_, i) => String(101 + i));
const isBlank = (text) => text === null || /^[-−-]$/.test(text);
/**
* List page (10 cards, each rendered twice — only the PC copy `.result-contents.sm-hidden` is read):
* title .result-content-title span, id/link hidden inputs source_system_code + item_no,
* rows .result-detail-row > div(label) + div(value): 賃料 '月額 22.5万円 税別', 面積 '57.91 m2(17.52 坪)',
* 使用階/部屋番号 '2/202' | 'B1/B001', 最寄駅 'JR総武線 新小岩', 所在地 '東京都葛飾区新小岩1丁目41番地12号',
* 保証金/礼金 (two <p>: '3.00ヶ月', '2.00ヶ月' | '110万円'), 管理費等; PR .point-content; image .result-content-img img
*/
const parseList = (html) => {
const $ = load(html);
return $(".result-contents.sm-hidden").toArray().map((el) => {
const $el = $(el);
const source = $el.find("input[name=\"source_system_code\"]").first().attr("value");
const itemNo = $el.find("input[name=\"item_no\"]").first().attr("value");
const title = clean($el.find(".result-content-title span").text());
if (!source || !itemNo || !title) return null;
const row = (label) => $el.find(".result-detail-row").toArray().map((r) => $(r).children()).find((children) => clean(children.first().text()) === label)?.last();
const value = (label) => {
const v = clean(row(label)?.text());
return isBlank(v) ? null : v;
};
const deposits = row("保証金/礼金")?.find("p").toArray().map((p) => clean($(p).text())) ?? [];
const raw = {
rent: value("賃料"),
area: value("面積"),
floor_room: value("使用階/部屋番号"),
station: value("最寄駅"),
address: value("所在地"),
guarantee: isBlank(deposits[0] ?? null) ? null : deposits[0],
key_money: isBlank(deposits[1] ?? null) ? null : deposits[1],
management_fee: value("管理費等"),
comment: clean($el.find(".point-content").text())
};
const [line, station] = (raw.station?.normalize("NFKC") ?? "").split(" ", 2);
const rentJpy = parseJpy(raw.rent);
const { tsubo, area_m2 } = parseArea(raw.area);
const image = $el.find(".result-content-img img").first().attr("src");
return {
title,
link: `${HOST}/store_detail?source_system_code=${source}&item_no=${itemNo}`,
image: image ? new URL(image, HOST).href : void 0,
extra: {
source: "temponw",
listing_id: `${source}-${itemNo}`,
rent_jpy: rentJpy,
tsubo,
area_m2,
tsubo_unit_jpy: tsuboUnit(rentJpy, tsubo),
floor: normalizeFloor(clean(raw.floor_room?.split("/", 1)[0])),
station: clean(station?.replace(/、他.*$/, ""))?.replace(/駅$/, "") ?? null,
line: clean(line),
walk_min: null,
deposit_months: parseMonths(raw.guarantee),
deposit_jpy: /[万円]/.test(raw.guarantee ?? "") ? parseJpy(raw.guarantee) : null,
key_money_months: parseMonths(raw.key_money),
fixtures_transfer_jpy: null,
condition: null,
prev_business: null,
heavy_food_ok: null,
business_limit: null,
listed_at: null,
ward: parseWard(raw.address?.normalize("NFKC") ?? null),
address_hint: raw.address,
tags: [],
raw
}
};
}).filter((c) => c !== null);
};
/** Detail page: `.content-detail .container-div.inline-container` → span:first p (label), span:last p (value). */
const parseDetail = (html) => {
const $ = load(html);
const field = (label) => {
const box = $(".content-detail .container-div.inline-container").toArray().find((b) => clean($(b).find("span").first().find("p").text()) === label);
const v = box ? clean($(box).find("span").last().find("p").text()) : null;
return isBlank(v) ? null : v;
};
return {
access: clean($(".detail-access .detail-access-info").first().text()),
fixtures: field("造作譲渡料"),
status: field("現況"),
contract: field("契約形態"),
term: field("契約期間"),
industry_blocks: $(".applicable-industry-items").toArray().map((block) => $(block).find(".grey-div p").toArray().map((p) => clean($(p).text())).filter((t) => t !== null)),
notes: $(".detail-note li").toArray().map((li) => clean($(li).text())).filter((t) => t !== null)
};
};
const mergeDetail = (base, d) => {
const [applicable = [], features = [], location = []] = d.industry_blocks;
const restriction = clean(d.notes.find((n) => n.includes("不可業態"))?.replace(/^不可業態[::]\s*/, ""));
const limitParts = [applicable.length > 0 ? `可: ${applicable.join("、")}` : null, restriction === null ? null : `不可: ${restriction}`].filter((p) => p !== null);
return {
...base,
walk_min: parseWalkMin(d.access),
fixtures_transfer_jpy: parseJpy(d.fixtures),
condition: features.includes("スケルトン") ? "skeleton" : base.condition,
heavy_food_ok: parseHeavyFood(restriction) ?? (applicable.length > 0 ? applicable.includes("重飲食") : null),
business_limit: limitParts.length > 0 ? limitParts.join(" / ") : null,
tags: [...features, ...location],
raw: {
...base.raw,
access: d.access,
fixtures: d.fixtures,
status: d.status,
contract: d.contract,
term: d.term,
notes: d.notes.join(" / ") || null
}
};
};
/** A failed detail page keeps the list fields instead of breaking the feed. */
const enrich = async (card) => {
try {
return mergeDetail(card.extra, parseDetail(await rofetch(card.link)));
} catch (error) {
logger.warn(`temponw: detail fetch failed for ${card.link}: ${String(error)}`);
return {
...card.extra,
raw: {
...card.extra.raw,
detail_error: String(error)
}
};
}
};
const listUrlFor = (area, page) => {
if (area === void 0) return `${HOST}/result?sort=new&page=${page}`;
const query = new URLSearchParams({
prefectures_code: "13",
name_j: "東京都"
});
for (const code of TOKYO_WARDS) query.append("cities[]", code);
query.set("sort", "new");
query.set("page", String(page));
return `${HOST}/area_search/result?${query.toString()}`;
};
const handler = async (ctx) => {
const area = ctx.req.param("area");
if (area !== void 0 && area !== "tokyo") throw new Error(`Unknown area "${area}", expected "tokyo" or none`);
const pages = await Promise.all(Array.from({ length: PAGES }, (_, i) => rofetch(listUrlFor(area, i + 1))));
const seen = /* @__PURE__ */ new Set();
const cards = pages.flatMap((html) => parseList(html)).filter((card) => seen.has(card.link) ? false : seen.add(card.link));
const items = await pMap(cards, (card) => cache_default.tryGet(card.link, async () => {
const extra = await enrich(card);
return {
title: card.title,
link: card.link,
guid: card.link,
description: [extra.raw.comment, summarize(extra)].filter(Boolean).join(" / "),
image: card.image,
_extra: extra
};
}), { concurrency: DETAIL_CONCURRENCY });
return {
title: `店舗ネットワーク 新着物件${area === "tokyo" ? " (東京23区)" : ""}`,
link: listUrlFor(area, 1),
language: "ja",
item: items
};
};
const route = {
path: "/estates/:area?",
name: "新着物件",
url: "www.temponw.com",
maintainers: ["pseudoyu"],
handler,
example: "/temponw/estates/tokyo",
parameters: { area: {
description: "`tokyo` for the 23 wards of Tokyo; omit for nationwide",
options: [{
value: "tokyo",
label: "東京23区"
}]
} },
description: `Listings on 店舗ネットワーク sorted by 新着順 (first two pages, 20 listings). Each item's \`_extra\` carries the structured listing fields (賃料,坪,階,最寄駅,保証金,礼金,造作譲渡料,不可業態,…) parsed from the list and detail pages; unknown values are \`null\`. The site does not publish listing dates, so items have no \`pubDate\`; the same unit may be listed by several agencies under different ids.`,
categories: ["other"],
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportRadar: true
},
radar: [{
source: ["www.temponw.com/result", "www.temponw.com/"],
target: "/estates"
}]
};
//#endregion
export { handler, route };