数据采集实战(一)-- 链家网成交数据 (by puppeteer)
概述
最近在学习python的各种数据分析库,为了尝试各种库中各种分析算法的效果,陆陆续续爬取了一些真实的数据来。
顺便也练习练习爬虫,踩了不少坑,后续将采集的经验逐步分享出来,希望能给后来者一些参考,也希望能够得到先驱者的指点!
采集工具
其实基本没用过什么现成的采集工具,都是自己通过编写代码来采集,虽然耗费一些时间,但是感觉灵活度高,可控性强,遇到问题时解决的方法也多。
一般根据网站的情况,如果提供api最好,直接写代码通过访问api来采集数据。
如果没有api,就通过解析页面(html)来获取数据。
本次采集的数据是链家网上的成交数据,因为是学习用,所以不会去大规模的采集,只采集了南京各个区的成交数据。
采集使用puppeteer库,puppeteer 是一个 node 库,它提供了高级的 api 并通过 devtools 协议来控制 chrome(或chromium)。
通俗来说就是一个 headless chrome 浏览器:
通过 puppeteer,可以模拟网页的手工操作方式,也就是说,理论上,能通过浏览器正常访问看到的内容就能采集到。
采集过程
其实数据采集的代码并不复杂,时间主要花在页面的分析上了。
链家网的成交数据不用登录也可以访问,这样就省了很多的事情。
只要找出南京市各个区的成交数据页面的url,然后访问就行。
页面分析
下面以栖霞区的成交页面为例,分析我们可能需要的数据。
页面url:
根据页面,可以看出重复的主要是红框内的数据,其中销售人员的姓名涉及隐私,我们不去采集。
采集的数据分类为:(有的户型可能没有下面列的那么全,缺少房屋优势字段,甚至成交价格字段等等)
- name: 小区名称和房屋概要,比如:新城香悦澜山 3室2厅 87.56平米
- houseinfo: 房屋朝向和装修情况,比如:南 北 | 精装
- dealdate: 成交日期,比如:2021.06.14
- totalprice: 成交价格(单位: 万元),比如:338万
- positioninfo: 楼层等信息,比如:中楼层(共5层) 2002年建塔楼
- unitprice: 成交单价,比如:38603元/平
- advantage: 房屋优势,比如:房屋满五年
- listprice: 挂牌价格,比如:挂牌341万
- dealcycledays: 成交周期,比如:成交周期44天
核心代码
链家网上采集房产成交数据很简单,我在采集过程中遇到的唯一的限制就是根据检索条件,只返回100页的数据,每页30条。
也就是说,不管什么检索条件,链家网只返回前3000条数据。
可能这也是链家网控制服务器访问压力的一个方式,毕竟如果是正常用户访问的话,一般也不会看3000条那么多,返回100页数据绰绰有余。
为了获取想要的数据,只能自己设计下检索条件,保证每个检索条件下的数据不超过3000条,最后自己合并左右的采集结果,去除重复数据。
这里,只演示如何采集数据,具体检索条件的设计,有兴趣根据自己需要的数据尝试下即可,没有统一的方法。
通过puppeteer采集数据,主要步骤很简单:
- 启动浏览器,打开页面
- 解析当前页面,获取需要的数据(也就是上面列出的9个字段的数据)
- 进入下一页
- 如果是最后一页,则退出程序
- 如果不是最后一页,进入步骤2
初始化并启动页面
import puppeteer from "puppeteer"; (async () => { // 启动页面,得到页面对象 const page = await startpage(); })(); // 初始化浏览器 const initbrowser = async () => { const browser = await puppeteer.launch({ args: ["--no-sandbox", "--start-maximized"], 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", name: "gulou", size: 2 }, // 测试用 { url: "https://nj.lianjia.com/chengjiao/gulou", name: "gulou", size: 30 }, { url: "https://nj.lianjia.com/chengjiao/jianye", name: "jianye", size: 20 }, { url: "https://nj.lianjia.com/chengjiao/qinhuai", name: "qinhuai", size: 29, }, { url: "https://nj.lianjia.com/chengjiao/xuanwu", name: "xuanwu", size: 14 }, { url: "https://nj.lianjia.com/chengjiao/yuhuatai", name: "yuhuatai", size: 14, }, { url: "https://nj.lianjia.com/chengjiao/qixia", name: "qixia", size: 14 }, { url: "https://nj.lianjia.com/chengjiao/jiangning", name: "jiangning", size: 40, }, { url: "https://nj.lianjia.com/chengjiao/pukou", name: "pukou", size: 25 }, { url: "https://nj.lianjia.com/chengjiao/liuhe", name: "liuhe", size: 4 }, { url: "https://nj.lianjia.com/chengjiao/lishui", name: "lishui", size: 4 }, ]; // 南京各区成交数据 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 ->", err); advantage = ""; } const [listprice, dealcycledays] = await listcontent[i].$$eval( ".info>.dealcycleeinfo>.dealcycletxt>span", (nodes) => nodes.map((n) => n.innertext) ); console.log("name: ", name); console.log("houseinfo: ", houseinfo); console.log("dealdate: ", dealdate); console.log("totalprice: ", totalprice); console.log("positioninfo: ", positioninfo); console.log("unitprice: ", unitprice); console.log("advantage: ", advantage); console.log("listprice: ", listprice); console.log("dealcycledays: ", dealcycledays); lines.push( `${name},${houseinfo},${dealdate},${totalprice},${positioninfo},${unitprice},${advantage},${listprice},${dealcycledays}` ); } catch (err) { console.log("数据解析失败:", err); } } return lines; };
我是把要采集的页面列在 const mapareapagesize
这个变量中,其中 url
是页面地址,size
是访问多少页(根据需要,并不是每个检索条件都要访问100页)。
采集数据的核心在 parselianjiadata
函数中,通过 chrome 浏览器的debug模式,找到每个数据所在的页面位置。
puppeteer提供强大的html 选择器功能,通过html元素的 id
和 class
可以很快定位数据的位置(如果用过jquery,很容易就能上手)。
这样,可以避免写复杂的正则表达式,提取数据更方便。
采集之后,我最后将数据输出成 csv
格式。
注意事项
爬取数据只是为了研究学习使用,本文中的代码遵守:
- 如果网站有 robots.txt,遵循其中的约定
- 爬取速度模拟正常访问的速率,不增加服务器的负担
- 只获取完全公开的数据,有可能涉及隐私的数据绝对不碰