您的当前位置:首页正文

数据采集实战(一)--链家网成交数据

2023-08-15 来源:小奈知识网
数据采集实战(⼀)--链家⽹成交数据

概述

最近在学习python的各种数据分析库,为了尝试各种库中各种分析算法的效果,陆陆续续爬取了⼀些真实的数据来。

顺便也练习练习爬⾍,踩了不少坑,后续将采集的经验逐步分享出来,希望能给后来者⼀些参考,也希望能够得到先驱者的指点!

采集⼯具

其实基本没⽤过什么现成的采集⼯具,都是⾃⼰通过编写代码来采集,虽然耗费⼀些时间,但是感觉灵活度⾼,可控性强,遇到问题时解决的⽅法也多。

⼀般根据⽹站的情况,如果提供API最好,直接写代码通过访问API来采集数据。如果没有API,就通过解析页⾯(html)来获取数据。

本次采集的数据是链家⽹上的成交数据,因为是学习⽤,所以不会去⼤规模的采集,只采集了南京各个区的成交数据。通过 puppeteer,可以模拟⽹页的⼿⼯操作⽅式,也就是说,理论上,能通过浏览器正常访问看到的内容就能采集到。

采集过程

其实数据采集的代码并不复杂,时间主要花在页⾯的分析上了。链家⽹的成交数据不⽤登录也可以访问,这样就省了很多的事情。只要找出南京市各个区的成交数据页⾯的URL,然后访问就⾏。

页⾯分析

下⾯以栖霞区的成交页⾯为例,分析我们可能需要的数据。

1. name: ⼩区名称和房屋概要,⽐如:新城⾹悦澜⼭ 3室2厅 87.56平⽶2. houseInfo: 房屋朝向和装修情况,⽐如:南 北 | 精装3. dealDate: 成交⽇期,⽐如:2021.06.14

4. totalPrice: 成交价格(单位: 万元),⽐如:338万

5. positionInfo: 楼层等信息,⽐如:中楼层(共5层) 2002年建塔楼6. unitPrice: 成交单价,⽐如:38603元/平7. advantage: 房屋优势,⽐如:房屋满五年8. listPrice: 挂牌价格,⽐如:挂牌341万

9. dealCycleDays: 成交周期,⽐如:成交周期44天

核⼼代码

链家⽹上采集房产成交数据很简单,我在采集过程中遇到的唯⼀的限制就是根据检索条件,只返回100页的数据,每页30条。也就是说,不管什么检索条件,链家⽹只返回前3000条数据。

可能这也是链家⽹控制服务器访问压⼒的⼀个⽅式,毕竟如果是正常⽤户访问的话,⼀般也不会看3000条那么多,返回100页数据绰绰有余。

为了获取想要的数据,只能⾃⼰设计下检索条件,保证每个检索条件下的数据不超过3000条,最后⾃⼰合并左右的采集结果,去除重复数据。

这⾥,只演⽰如何采集数据,具体检索条件的设计,有兴趣根据⾃⼰需要的数据尝试下即可,没有统⼀的⽅法。通过puppeteer采集数据,主要步骤很简单:

1. 启动浏览器,打开页⾯

2. 解析当前页⾯,获取需要的数据(也就是上⾯列出的9个字段的数据)3. 进⼊下⼀页

4. 如果是最后⼀页,则退出程序5. 如果不是最后⼀页,进⼊步骤2

初始化并启动页⾯

import puppeteer from \"puppeteer\";(async () => {

// 启动页⾯,得到页⾯对象

const page = await startPage();})();

// 初始化浏览器

const initBrowser = async () => {

const browser = await puppeteer.launch({ args: [\"--no-sandbox\ headless: false,

userDataDir: \"./user_data\

ignoreDefaultArgs: [\"--enable-automation\"], executablePath:

\"C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\ });

return browser;};

// 启动页⾯

const startPage = async (browser) => { const page = await browser.newPage();

await page.setViewport({ width: 1920, height: 1080 }); return page;};

采集数据

import puppeteer from \"puppeteer\";(async () => {

// 启动页⾯,得到页⾯对象

const page = await startPage();

// 采集数据

await nanJin(page);})();

const mapAreaPageSize = [

// { url: \"https://nj.lianjia.com/chengjiao/gulou\测试⽤ { url: \"https://nj.lianjia.com/chengjiao/gulou\ { url: \"https://nj.lianjia.com/chengjiao/jianye\ {

url: \"https://nj.lianjia.com/chengjiao/qinhuai\ name: \"qinhuai\ size: 29, },

{ url: \"https://nj.lianjia.com/chengjiao/xuanwu\ {

url: \"https://nj.lianjia.com/chengjiao/yuhuatai\ name: \"yuhuatai\ size: 14, },

{ url: \"https://nj.lianjia.com/chengjiao/qixia\ {

url: \"https://nj.lianjia.com/chengjiao/jiangning\ name: \"jiangning\ size: 40, },

{ url: \"https://nj.lianjia.com/chengjiao/pukou\ { url: \"https://nj.lianjia.com/chengjiao/liuhe\ { url: \"https://nj.lianjia.com/chengjiao/lishui\];

// 南京各区成交数据

const nanJin = async (page) => {

for (let i = 0; i < mapAreaPageSize.length; i++) {

const areaLines = await nanJinArea(page, mapAreaPageSize[i]); // 分区写⼊csv

await saveContent( `./output/lianjia`,

`${mapAreaPageSize[i].name}.csv`, areaLines.join(\"\\n\") ); }};

const nanJinArea = async (page, m) => { let areaLines = [];

for (let i = 1; i <= m.size; i++) { await page.goto(`${m.url}/pg${i}`);

// 等待页⾯加载完成,这是显⽰总套数的div

await page.$$(\"div>.total.fs\"); await mouseDown(page, 800, 10);

// 解析页⾯内容

const lines = await parseLianjiaData(page); areaLines = areaLines.concat(lines);

// 保存页⾯内容

await savePage(page, `./output/lianjia/${m.name}`, `page-${i}.html`); }

return areaLines;};

// 解析页⾯内容

// 1. name: ⼩区名称和房屋概要// 2. houseInfo: 房屋朝向和装修情况// 3. dealDate: 成交⽇期

// 4. totalPrice: 成交价格(单位: 万元)// 5. positionInfo: 楼层等信息// 6. unitPrice: 成交单价// 7. advantage: 房屋优势// 8. listPrice: 挂牌价格

// 9. dealCycleDays: 成交周期

const parseLianjiaData = async (page) => {

const listContent = await page.$$(\".listContent>li\"); let lines = [];

for (let i = 0; i < listContent.length; i++) { try {

const name = await listContent[i].$eval( \".info>.title>a\

(node) => node.innerText );

const houseInfo = await listContent[i].$eval( \".info>.address>.houseInfo\ (node) => node.innerText );

const dealDate = await listContent[i].$eval( \".info>.address>.dealDate\ (node) => node.innerText );

const totalPrice = await listContent[i].$eval( \".info>.address>.totalPrice>.number\ (node) => node.innerText );

const positionInfo = await listContent[i].$eval( \".info>.flood>.positionInfo\ (node) => node.innerText );

const unitPrice = await listContent[i].$eval( \".info>.flood>.unitPrice>.number\ (node) => node.innerText + \"元/平\" );

let advantage = \"\"; try {

advantage = await listContent[i].$eval(

\".info>.dealHouseInfo>.dealHouseTxt>span\ (node) => node.innerText );

} catch (err) {

console.log(\"err is ->\ advantage = \"\"; }

const [listPrice, dealCycleDays] = await listContent[i].$$eval( \".info>.dealCycleeInfo>.dealCycleTxt>span\ (nodes) => nodes.map((n) => n.innerText) );

console.log(\"name: \

console.log(\"houseInfo: \ console.log(\"dealDate: \ console.log(\"totalPrice: \ console.log(\"positionInfo: \ console.log(\"unitPrice: \ console.log(\"advantage: \ console.log(\"listPrice: \

console.log(\"dealCycleDays: \ lines.push(

`${name},${houseInfo},${dealDate},${totalPrice},${positionInfo},${unitPrice},${advantage},${listPrice},${dealCycleDays}` );

} catch (err) {

console.log(\"数据解析失败:\

} }

return lines;};

我是把要采集的页⾯列在 const mapAreaPageSize 这个变量中,其中 url 是页⾯地址,size 是访问多少页(根据需要,并不是每个检索条件都要访问100页)。

采集数据的核⼼在 parseLianjiaData 函数中,通过 chrome 浏览器的debug模式,找到每个数据所在的页⾯位置。

puppeteer提供强⼤的html 选择器功能,通过html元素的 id 和 class 可以很快定位数据的位置(如果⽤过jQuery,很容易就能上⼿)。这样,可以避免写复杂的正则表达式,提取数据更⽅便。采集之后,我最后将数据输出成 csv 格式。

注意事项

爬取数据只是为了研究学习使⽤,本⽂中的代码遵守:1. 如果⽹站有 robots.txt,遵循其中的约定

2. 爬取速度模拟正常访问的速率,不增加服务器的负担

3. 只获取完全公开的数据,有可能涉及隐私的数据绝对不碰

因篇幅问题不能全部显示,请点此查看更多更全内容