9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
96 lines
3.1 KiB
JavaScript
96 lines
3.1 KiB
JavaScript
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
import { gotoKe } from './utils.js';
|
|
|
|
cli({
|
|
site: 'ke',
|
|
name: 'zufang',
|
|
access: 'read',
|
|
description: '贝壳找房租房列表',
|
|
domain: 'ke.com',
|
|
strategy: Strategy.COOKIE,
|
|
browser: true,
|
|
args: [
|
|
{ name: 'city', default: 'bj', help: '城市代码,如 bj(北京), sh(上海), gz(广州), sz(深圳), zs(中山)' },
|
|
{ name: 'district', help: '区域拼音,如 chaoyang, haidian' },
|
|
{ name: 'min-price', type: 'int', help: '最低月租(元)' },
|
|
{ name: 'max-price', type: 'int', help: '最高月租(元)' },
|
|
{ name: 'limit', type: 'int', default: 20, help: '返回数量' },
|
|
],
|
|
columns: ['title', 'community', 'area', 'layout', 'price', 'url'],
|
|
func: async (page, kwargs) => {
|
|
const city = kwargs.city || 'bj';
|
|
const limit = Number(kwargs.limit) || 20;
|
|
|
|
let path = '/zufang/';
|
|
if (kwargs.district) {
|
|
path = `/zufang/${kwargs.district}/`;
|
|
}
|
|
|
|
const priceParts = [];
|
|
if (kwargs['min-price'] || kwargs['max-price']) {
|
|
const min = kwargs['min-price'] || '';
|
|
const max = kwargs['max-price'] || '';
|
|
priceParts.push(`rp${min}t${max}`);
|
|
}
|
|
const filters = priceParts.join('');
|
|
|
|
const baseUrl = `https://${city}.zu.ke.com`;
|
|
const url = baseUrl + path + (filters ? filters + '/' : '');
|
|
|
|
await gotoKe(page, url);
|
|
|
|
const items = await page.evaluate(`(async () => {
|
|
const allLinks = document.querySelectorAll('a.twoline');
|
|
const results = [];
|
|
for (const titleEl of allLinks) {
|
|
let card = titleEl.closest('div');
|
|
if (!card) continue;
|
|
while (card && card.parentElement && !card.parentElement.classList.contains('content__list')) {
|
|
card = card.parentElement;
|
|
}
|
|
if (!card) continue;
|
|
|
|
const title = (titleEl.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
const href = titleEl.getAttribute('href') || '';
|
|
const fullUrl = href.startsWith('http') ? href : '${baseUrl}' + href;
|
|
|
|
const allPs = card.querySelectorAll('p');
|
|
let community = '', area = '', layout = '';
|
|
for (const p of allPs) {
|
|
if ((p.className || '').indexOf('des') === -1) continue;
|
|
const links = p.querySelectorAll('a[title]');
|
|
if (links.length > 0) {
|
|
community = (links[links.length - 1].getAttribute('title') || '').trim();
|
|
}
|
|
const parts = p.textContent.replace(/\\s+/g, ' ').trim().split('/');
|
|
for (const part of parts) {
|
|
const t = part.trim();
|
|
if (/\\u33A1|\\u5E73\\u7C73/.test(t)) area = t;
|
|
else if (/\\u5BA4.*\\u5385/.test(t)) layout = t;
|
|
}
|
|
break;
|
|
}
|
|
|
|
const emEls = card.querySelectorAll('em');
|
|
let priceText = '';
|
|
for (const em of emEls) {
|
|
const t = em.textContent.trim();
|
|
if (/^\\d+$/.test(t)) { priceText = t; break; }
|
|
}
|
|
|
|
results.push({
|
|
title,
|
|
url: fullUrl,
|
|
community,
|
|
area,
|
|
layout,
|
|
price: priceText ? priceText + '\\u5143/\\u6708' : '',
|
|
});
|
|
}
|
|
return results;
|
|
})()`);
|
|
|
|
return (items || []).slice(0, limit);
|
|
},
|
|
});
|