Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions cli-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7825,6 +7825,202 @@
"modulePath": "jimeng/workspaces.js",
"sourceFile": "jimeng/workspaces.js"
},
{
"site": "ke",
"name": "chengjiao",
"description": "贝壳找房成交记录",
"domain": "ke.com",
"strategy": "cookie",
"browser": true,
"args": [
{
"name": "city",
"type": "str",
"default": "bj",
"required": false,
"help": "城市代码,如 bj(北京), sh(上海), gz(广州), sz(深圳), zs(中山)"
},
{
"name": "district",
"type": "str",
"required": false,
"help": "区域拼音,如 chaoyang, haidian"
},
{
"name": "limit",
"type": "int",
"default": 20,
"required": false,
"help": "返回数量"
}
],
"columns": [
"title",
"community",
"layout",
"area",
"deal_price",
"unit_price",
"deal_date"
],
"type": "js",
"modulePath": "ke/chengjiao.js",
"sourceFile": "ke/chengjiao.js"
},
{
"site": "ke",
"name": "ershoufang",
"description": "贝壳找房二手房列表",
"domain": "ke.com",
"strategy": "cookie",
"browser": true,
"args": [
{
"name": "city",
"type": "str",
"default": "bj",
"required": false,
"help": "城市代码,如 bj(北京), sh(上海), gz(广州), sz(深圳), zs(中山)"
},
{
"name": "district",
"type": "str",
"required": false,
"help": "区域拼音,如 chaoyang, haidian, tianhe"
},
{
"name": "min-price",
"type": "int",
"required": false,
"help": "最低总价(万元)"
},
{
"name": "max-price",
"type": "int",
"required": false,
"help": "最高总价(万元)"
},
{
"name": "rooms",
"type": "int",
"required": false,
"help": "几居室 (1-5)"
},
{
"name": "limit",
"type": "int",
"default": 20,
"required": false,
"help": "返回数量"
}
],
"columns": [
"title",
"community",
"layout",
"area",
"direction",
"total_price",
"unit_price",
"url"
],
"type": "js",
"modulePath": "ke/ershoufang.js",
"sourceFile": "ke/ershoufang.js"
},
{
"site": "ke",
"name": "xiaoqu",
"description": "贝壳找房小区列表",
"domain": "ke.com",
"strategy": "cookie",
"browser": true,
"args": [
{
"name": "city",
"type": "str",
"default": "bj",
"required": false,
"help": "城市代码,如 bj(北京), sh(上海), gz(广州), sz(深圳), zs(中山)"
},
{
"name": "district",
"type": "str",
"required": false,
"help": "区域拼音,如 chaoyang, haidian"
},
{
"name": "limit",
"type": "int",
"default": 20,
"required": false,
"help": "返回数量"
}
],
"columns": [
"name",
"district",
"avg_price",
"year",
"on_sale"
],
"type": "js",
"modulePath": "ke/xiaoqu.js",
"sourceFile": "ke/xiaoqu.js"
},
{
"site": "ke",
"name": "zufang",
"description": "贝壳找房租房列表",
"domain": "ke.com",
"strategy": "cookie",
"browser": true,
"args": [
{
"name": "city",
"type": "str",
"default": "bj",
"required": false,
"help": "城市代码,如 bj(北京), sh(上海), gz(广州), sz(深圳), zs(中山)"
},
{
"name": "district",
"type": "str",
"required": false,
"help": "区域拼音,如 chaoyang, haidian"
},
{
"name": "min-price",
"type": "int",
"required": false,
"help": "最低月租(元)"
},
{
"name": "max-price",
"type": "int",
"required": false,
"help": "最高月租(元)"
},
{
"name": "limit",
"type": "int",
"default": 20,
"required": false,
"help": "返回数量"
}
],
"columns": [
"title",
"community",
"area",
"layout",
"price",
"url"
],
"type": "js",
"modulePath": "ke/zufang.js",
"sourceFile": "ke/zufang.js"
},
{
"site": "lesswrong",
"name": "comments",
Expand Down
77 changes: 77 additions & 0 deletions clis/ke/chengjiao.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { cli, Strategy } from '@jackwener/opencli/registry';
import { cityUrl, gotoKe } from './utils.js';

cli({
site: 'ke',
name: 'chengjiao',
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: 'limit', type: 'int', default: 20, help: '返回数量' },
],
columns: ['title', 'community', 'layout', 'area', 'deal_price', 'unit_price', 'deal_date'],
func: async (page, kwargs) => {
const city = kwargs.city || 'bj';
const limit = Number(kwargs.limit) || 20;
const base = cityUrl(city);

let path = '/chengjiao/';
if (kwargs.district) {
path = `/chengjiao/${kwargs.district}/`;
}

await gotoKe(page, base + path);

const items = await page.evaluate(`(async () => {
// chengjiao page uses .listContent li or similar structure
const selectors = [
'.listContent li',
'ul.listContent li',
'.sellListContent li.clear',
'li.clear',
];
let cards = [];
for (const sel of selectors) {
cards = document.querySelectorAll(sel);
if (cards.length > 0) break;
}

const results = [];
for (const card of cards) {
const titleEl = card.querySelector('.title a, a.VIEWDATA');
if (!titleEl) continue;

const houseInfoEl = card.querySelector('.houseInfo');
const communityEl = card.querySelector('.positionInfo a');
const priceEl = card.querySelector('.totalPrice span');
const unitPriceEl = card.querySelector('.unitPrice span');
const dateEl = card.querySelector('.dealDate');
const dealCycleEl = card.querySelector('.dealCycleTxt span');

const houseText = (houseInfoEl ? houseInfoEl.textContent : '').replace(/\\s+/g, ' ').trim();
const houseParts = houseText.split('|').map(s => s.trim());

const layoutMatch = (houseParts[0] || '').match(/(\\d室\\d厅)/);
const layout = layoutMatch ? layoutMatch[1] : (houseParts[0] || '');

results.push({
title: (titleEl.textContent || '').trim(),
url: titleEl.href || '',
community: (communityEl ? communityEl.textContent : '').trim(),
layout: layout,
area: (houseParts[1] || '').trim(),
deal_price: ((priceEl ? priceEl.textContent : '').trim() || '') + '万',
unit_price: (unitPriceEl ? unitPriceEl.textContent : '').trim(),
deal_date: (dateEl ? dateEl.textContent : '').replace(/\\s+/g, ' ').trim(),
});
}
return results;
})()`);

return (items || []).slice(0, limit);
},
});
100 changes: 100 additions & 0 deletions clis/ke/ershoufang.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { cli, Strategy } from '@jackwener/opencli/registry';
import { cityUrl, gotoKe } from './utils.js';

cli({
site: 'ke',
name: 'ershoufang',
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, tianhe' },
{ name: 'min-price', type: 'int', help: '最低总价(万元)' },
{ name: 'max-price', type: 'int', help: '最高总价(万元)' },
{ name: 'rooms', type: 'int', help: '几居室 (1-5)' },
{ name: 'limit', type: 'int', default: 20, help: '返回数量' },
],
columns: ['title', 'community', 'layout', 'area', 'direction', 'total_price', 'unit_price', 'url'],
func: async (page, kwargs) => {
const city = kwargs.city || 'bj';
const limit = Number(kwargs.limit) || 20;
const base = cityUrl(city);

let path = '/ershoufang/';
if (kwargs.district) {
path = `/ershoufang/${kwargs.district}/`;
}

const priceParts = [];
if (kwargs['min-price'] || kwargs['max-price']) {
const min = kwargs['min-price'] || '';
const max = kwargs['max-price'] || '';
priceParts.push(`p${min}t${max}`);
}

const roomParts = [];
if (kwargs.rooms) {
roomParts.push(`l${kwargs.rooms}`);
}

const filters = [...priceParts, ...roomParts].join('');
const url = base + path + (filters ? filters + '/' : '');

await gotoKe(page, url);

const items = await page.evaluate(`(async () => {
const cards = document.querySelectorAll('.sellListContent li.clear');
const results = [];
for (const card of cards) {
const titleEl = card.querySelector('.title a');
const communityEl = card.querySelector('.positionInfo a');
const houseInfoEl = card.querySelector('.houseInfo');
const priceEl = card.querySelector('.totalPrice span');
const unitPriceEl = card.querySelector('.unitPrice span');

if (!titleEl) continue;

// houseInfo text varies:
// "中楼层 (共24层) 4室2厅 | 133.99平米 | 东南"
// "高楼层 (共32层) | 2022年 | 4室2厅 | 110平米"
const houseText = (houseInfoEl ? houseInfoEl.textContent : '').replace(/\\s+/g, ' ').trim();
const houseParts = houseText.split('|').map(s => s.trim());

// Extract structured fields from all parts
let layout = '', area = '', direction = '', floor = '';
for (const part of houseParts) {
if (/\\d室\\d厅/.test(part)) {
layout = part.match(/(\\d室\\d厅)/)[1];
} else if (/平米|㎡/.test(part)) {
area = part;
} else if (/^[东南西北]+$/.test(part.replace(/\\s/g, ''))) {
direction = part;
} else if (/楼层/.test(part)) {
floor = part;
}
}
// layout might be embedded in the floor part: "中楼层 (共24层) 4室2厅"
if (!layout) {
const m = houseText.match(/(\\d室\\d厅)/);
if (m) layout = m[1];
}

results.push({
title: (titleEl.textContent || '').trim(),
url: titleEl.href || '',
community: (communityEl ? communityEl.textContent : '').trim(),
layout: layout,
area: area,
direction: direction,
total_price: ((priceEl ? priceEl.textContent : '').trim() || '') + '万',
unit_price: (unitPriceEl ? unitPriceEl.textContent : '').trim(),
});
}
return results;
})()`);

return (items || []).slice(0, limit);
},
});
Loading