對於區劃代碼數據,不少人都不會陌生,大多公司數據庫都會維護一份區劃代碼,包含省市區等數據。區劃信息跟用戶信息息息相關,每每因爲歷史緣由不少數據都是比較老的數據,且不會輕易更改。網上也有不少人提供的數據,或許大多數數據已經老舊,儘管並不會影響太多。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表示縣(旗、自治縣、自治旗)。
對於省級直轄縣級行政單位:同地區。
// 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 = {};
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); });
地址是相對路徑,就須要根據請求的 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":"景山街道" ... ... }
根據區劃代碼變動的頁面獲取數據來更新已經獲取到的 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
mapLimit(coll, limit, iterate, callback)
當循環省份超過 10 個時,回調執行寫入區劃信息會失敗,也沒執行 error,是否是個人用法有問題,望能獲得解答