概述
最近在学习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. 只获取完全公开的数据,有可能涉及隐私的数据绝对不碰
因篇幅问题不能全部显示,请点此查看更多更全内容