zoukankan      html  css  js  c++  java
  • 数据采集实战(一)-- 链家网成交数据 (by puppeteer)

    概述

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

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

    采集工具

    其实基本没用过什么现成的采集工具,都是自己通过编写代码来采集,虽然耗费一些时间,但是感觉灵活度高,可控性强,遇到问题时解决的方法也多。

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

    本次采集的数据是链家网上的成交数据,因为是学习用,所以不会去大规模的采集,只采集了南京各个区的成交数据。

    采集使用puppeteer库,Puppeteer 是一个 Node 库,它提供了高级的 API 并通过 DevTools 协议来控制 Chrome(或Chromium)。
    通俗来说就是一个 headless chrome 浏览器: https://github.com/puppeteer/puppeteer

    通过 puppeteer,可以模拟网页的手工操作方式,也就是说,理论上,能通过浏览器正常访问看到的内容就能采集到。

    采集过程

    其实数据采集的代码并不复杂,时间主要花在页面的分析上了。

    链家网的成交数据不用登录也可以访问,这样就省了很多的事情。
    只要找出南京市各个区的成交数据页面的URL,然后访问就行。

    页面分析

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

    页面URL: https://nj.lianjia.com/chengjiao/qixia/
    image.png
    根据页面,可以看出重复的主要是红框内的数据,其中销售人员的姓名涉及隐私,我们不去采集。
    采集的数据分类为:(有的户型可能没有下面列的那么全,缺少房屋优势字段,甚至成交价格字段等等)

    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", "--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({  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("
    ")
        );
      }
    };
    
    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元素的 idclass 可以很快定位数据的位置(如果用过jQuery,很容易就能上手)。
    这样,可以避免写复杂的正则表达式,提取数据更方便。

    采集之后,我最后将数据输出成 csv 格式。

    注意事项

    爬取数据只是为了研究学习使用,本文中的代码遵守:

    1. 如果网站有 robots.txt,遵循其中的约定
    2. 爬取速度模拟正常访问的速率,不增加服务器的负担
    3. 只获取完全公开的数据,有可能涉及隐私的数据绝对不碰
  • 相关阅读:
    python 开源机器学习包
    linux hadoop 集群安装步骤
    bbc 大数据
    微信小程序弹出可填写框两种方法
    js 一个对象的属性名是一个变量怎么处理?
    解决微信小程序使用switchTab跳转后页面不刷新的问题
    SQL 和 NoSQL 的区别
    IndexedDB API
    jQuery jsonp跨域请求
    关于setInterval返回值问题
  • 原文地址:https://www.cnblogs.com/wang_yb/p/14955938.html
Copyright © 2011-2022 走看看