區劃代碼 node 版爬蟲嘗試

前言

對於區劃代碼數據,不少人都不會陌生,大多公司數據庫都會維護一份區劃代碼,包含省市區等數據。區劃信息跟用戶信息息息相關,每每因爲歷史緣由不少數據都是比較老的數據,且不會輕易更改。網上也有不少人提供的數據,或許大多數數據已經老舊,儘管並不會影響太多。html

網上只提供數據,好像不多有人提供方法。最近有時間就來作一次爬蟲的初嘗,有想法但無奈沒學 python,就拼湊了個 node 版的。node

第一步 找資源

地名服務資源通常只有政府部門纔有權威性,比對某些網上提供的資源發現並不靠譜,特別是縣如下的區劃代碼。搜索到如下資源:python

結果發現,政府沒有提供統一的數據,區劃信息比較分散,縣級以上的數據有提供 2017 最新版的數據,但縣級如下的數據只有 2013 年 和 2015 年的數據,以及每一年來縣級如下的數據變動狀況。但 2013 和 2015 以不一樣的方式展現,一個頁面的數據仍是比較容易獲取,如 2013 年的數據。但 2015 年數據儘管是分散在不一樣的頁面中,仍是有必定的規律的。另外博雅地名分享網的數據相對比較新,但與官方比較仍是有些差別。此次以 2015 的數據爲例

第二步 搭環境

node 環境提供了衆多包,能夠方便的實現一些功能,下面只是這些工具包在本次爬蟲的所用到的功能,更多資料網上有不少,不過多說明(其實我是不徹底會用這些工具,只是用了部分功能去實現而已~~捂臉)

request       // 請求
cheerio       // node 版的 jQuery
iconv-lite    // 請求返回的數據轉碼
async         // 處理併發請求數

第三步 書寫代碼

觀察區劃代碼,你就會發現一些規律,根據這些規律,能夠把省市區的數據格式化成本身想要的格式

省級或直轄市: 第三,四位是 00
市級: 第五,六位是 00
省級直轄縣: 第三,四位是 90

編碼規則 引自維基百科

代碼從左至右的含義是:
第1、二位表示省級行政單位(省、自治區、直轄市、特別行政區),其中第一位表明大區。
第3、四位表示地級行政單位(地級市、地區、自治州、盟及省級單位直屬縣級單位的彙總碼)。
  對於省(自治區)下屬單位:01-20,51-70表示省轄市(地級市);21-50表示地區(自治州、盟);90表示省(自治區)直轄縣級行政區劃的彙總。
  對於直轄市下屬單位:01表示市轄區的彙總;02表示縣的彙總。
第5、六位表示縣級行政單位(縣、自治縣、市轄區、縣級市、旗、自治旗、林區、特區)。
  對於地級市下屬單位:01-20表示市轄區(特區);21-80表示縣(旗、自治縣、自治旗、林區);81-99表示地級市代管的縣級市。
  對於直轄市所轄縣級行政單位:01-20、51-80表明市轄區;21-50表明縣(自治縣)。
  對於地區(自治州、盟)下屬單位:01-20表示縣級市;21-80表示縣(旗、自治縣、自治旗)。
  對於省級直轄縣級行政單位:同地區。

  1. 須要把獲取的數據寫入 json 文件就須要 fs
// require 須要的包
const fs = require('fs');
const entryUrl = 'http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2015/index.html';
const writeJSON = fs.createWriteStream(__dirname + '/data/list-2015.json')

// 待寫入的 JSON 數據
let listJSON = {};
  1. 須要獲取首頁的數據

2015年統計用區劃代碼

function captureUrl(url, fn, provinceName) {
  request.get({
    url: url,
    encoding: null //讓 body 直接是 buffer,也有別的方法實現
  }, (error, response, body) => {
    if (!error && response.statusCode == 200) {

      // 頁面編碼爲 gb2312 須要轉碼
      const convertedHtml = iconv.decode(body, 'gb2312');

      // 請求返回數據解析 jQuery 對象
      const $ = cheerio.load(convertedHtml, {
        decodeEntities: false
      });
      $.href = url;
      $.provinceName = provinceName;
      fn($);
    }
    if (error) {
      console.log('error: ' + url)
    }
  });
}

captureUrl(entryUrl, function (res) {
  parseHtml.deal(res, entryUrl);
});
  1. 根據頁面結構獲取想要的數據,這裏是連接和城市名,瀏覽器 f12 都能看得懂的哈~

頁面結構

地址是相對路徑,就須要根據請求的 url 地址來拼接出下一個頁面真實地址

// 相對路徑地址轉換
function combineLink({
  originUrl,
  spliceUrl
}) {
  if (typeof originUrl == 'string' && /^http/.test(originUrl)) {
    const lastIndex = originUrl.lastIndexOf('/');
    return originUrl.substr(0, lastIndex + 1) + spliceUrl;
  }
}

有了 cheerio 能夠方便的獲取頁面節點,從而拿到咱們想要的數據

const parseHtml = {
  captureProvince($) {
    const linkList = $('.provincetr a');
    let provinceArr = [];

    linkList.map((index, item) => {
      // 循環 10 個省份能無缺執行異步回調,超過就出現問題
      // if (index < 10) {
      const $item = $(item);
      provinceArr.push({
        url: combineLink({
          originUrl: entryUrl,
          spliceUrl: $item.attr('href')
        }),
        name: $item.text().trim()
      });
      // }
    });

    return provinceArr;
  }
}

有了連接就能夠作下一步的處理,一樣觀察頁面結構,獲取有用的數據。這麼多連接請求,到底何時會請求完成?這時候就有請 Promise 出場了

// 省份連接
const provinceArr = parseHtml.captureProvince(res);

Promise.all(provinceArr.map(item => {
    const provinceName = item.name;
    const provinceUrl = item.url;
    return new Promise(resolve => {
        captureUrl(provinceUrl, resolve, provinceName);
    });
  })).then(res => {
    // 全部請求都結束
  }).catch(err => {
    // 異常處理
  });

覺得就這樣順利的走下去,就大功告成了。可遺憾的是新的問題出現了。在抓取博雅網的時候併發請求過多,請求失敗的異常連接會有不少,但這些連接並無問題。這是網站的防護機制,咱們僞造的合法請求觸發網站的防護機制,或許叫 CC 攻擊來實現 DDOS 。貌似這麼作對別的網站形成損失可能就要被請喝茶,請慎重,這時候就須要限制併發數不妨礙別人網站經營,asyn 有很強大的功能,這裏只用了 mapList 方法,目的限制併發數

Promise.all(provinceArr.map(item => {
    const provinceName = item.name;
    const provinceUrl = item.url;
    return new Promise(resolve => {
        captureUrl(provinceUrl, resolve, provinceName);
    });
  }))
  .then(res => {
    let tempArr = [];
    res.map(item => {
        // 解析市一級數據
        tempArr = tempArr.concat(this.captureCity(item, item.href))
    });

    return new Promise(resolve => {
      async.mapLimit(tempArr, 10, function (item, callback) {
      let temp = [];
      captureUrl(item, function ($) {
          console.log(item);
          // 解析區縣一級數據
          temp = temp.concat(parseHtml.captureCountry($));
          callback(null, temp);
      });
      }, function (err, result) {

        let tempArr = [];
        result.map(item => {
            tempArr = tempArr.concat(item);
        });
        resolve(tempArr);
      });
    });

  }).then(res => {
    return new Promise(resolve => {
        async.mapLimit(res, 10, function (item, callback) {
        let temp = [];
        captureUrl(item, function ($) {
          console.log(item);

          // 解析街道或者鄉鎮一級數據
          parseHtml.captureTown($);
          // 省份超過 10 個 callback 無效。找不到緣由,不得已寫法
          // fs.createWriteStream(__dirname + '/data/list-2015.json').write(JSON.stringify(listJSON));
          callback(null);
        });
      }, function (err, result) {
        console.log(err)
        resolve(true)
      });
    });
  }).then(res => {
    // 全部請求完成後 callback
    console.log('區劃信息寫入成功~')
    writeJSON.write(JSON.stringify(listJSON));
  }).catch(err => {
    console.log(err)
  });

處理數據過程當中,能夠對源數據加以處理,獲取數據展現

{
  "110000000":"北京市",
  "110101000":"東城區",
  "110101001":"東華門街道",
  "110101002":"景山街道"
  ...
  ...
}

第四步 update 數據

根據區劃代碼變動的頁面獲取數據來更新已經獲取到的 2015 年的數據,採起一樣的方法去拿到連接獲取頁面數據,發現這些變動狀況的連接有重定向,須要作相應的處理才能拿到真實連接

function getRedirectUrl($) {
  const scriptText = $('script').eq(0).text().trim();
  const matchArr = scriptText.match(/^window.location.href="(\S*)";/);
  if (matchArr && matchArr[1]) {
    return matchArr[1];
  } else {
    console.log('重定向頁面轉換錯誤~')
  }
}

在調試的過程當中發現變動狀況頁面是表格類型,不能很方便的拿到數據並區分開來,不能只是左側的數據須要 delete,右側的數據 add。 個人思路是根據變動緣由來分類處理,此部分代碼沒有寫完

變動狀況

調試

我使用的編輯工具是 VSCode,調試也比較方便,F5 寫好 launch.json 就可調試了。 可能我只知道這種調試吧,哈哈哈~

結局

因爲官方地名普查辦正在進行第二次地名普查,確保 2017 年 6 月 30 號完成全國地名普查工做,並向社會提供地名服務,詳見如何切實作好第二次全國地名普查驗收工做

到時候看官方公佈的數據狀況,來決定要不要完成這個工做,目前不想浪費太多的時間在這個上面,耗費了時間得來的數據有誤差,未來多是要負責任的, 得不償失。在此只是提供一種思路,有興趣的能夠本身嘗試,有好的方法能夠推薦一下哈哈~

詳細代碼見 division-code

疑惑

async.mapList

mapLimit(coll, limit, iterate, callback)

當循環省份超過 10 個時,回調執行寫入區劃信息會失敗,也沒執行 error,是否是個人用法有問題,望能獲得解答

參考資料

node API

async 控制併發

相關文章
相關標籤/搜索