數據采集實戰(一)-- 鏈家網成交數據


概述

最近在學習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({ 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元素的 idclass 可以很快定位數據的位置(如果用過jQuery,很容易就能上手)。
這樣,可以避免寫復雜的正則表達式,提取數據更方便。

采集之后,我最后將數據輸出成 csv 格式。

注意事項

爬取數據只是為了研究學習使用,本文中的代碼遵守:

  1. 如果網站有 robots.txt,遵循其中的約定
  2. 爬取速度模擬正常訪問的速率,不增加服務器的負擔
  3. 只獲取完全公開的數據,有可能涉及隱私的數據絕對不碰


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM