前言

对于区划代码数据,很多人都不会陌生,大多公司数据库都会维护一份区划代码,包含省市区等数据。区划信息跟用户信息息息相关,往往由于历史原因很多数据都是比较老的数据,且不会轻易更改。网上也有很多人提供的数据,或许大多数数据已经老旧,尽管并不会影响太多。

网上只提供数据,好像很少有人提供方法。最近有时间就来做一次爬虫的初尝,有想法但无奈没学 python,就拼凑了个 node 版的。

第一步 找资源

地名服务资源一般只有政府部门才有权威性,比对某些网上提供的资源发现并不靠谱,特别是县以下的区划代码。搜索到以下资源:

结果发现,政府没有提供统一的数据,区划信息比较分散,县级以上的数据有提供 2017 最新版的数据,但县级以下的数据只有 2013 年 和 2015 年的数据,以及每年来县级以下的数据变更情况。但 2013 和 2015 以不同的方式展示,一个页面的数据还是比较容易获取,如 2013 年的数据。但 2015 年数据尽管是分散在不同的页面中,还是有一定的规律的。另外博雅地名分享网的数据相对比较新,但与官方比较还是有些差异。这次以 2015 的数据为例

第二步 搭环境

node 环境提供了众多包,可以方便的实现一些功能,下面只是这些工具包在本次爬虫的所用到的功能,更多资料网上有很多,不过多说明(其实我是不完全会用这些工具,只是用了部分功能去实现而已~~捂脸)

request       // 请求
cheerio // node 版的 jQuery
iconv-lite // 请求返回的数据转码
async // 处理并发请求数

第三步 书写代码

观察区划代码,你就会发现一些规律,根据这些规律,可以把省市区的数据格式化成自己想要的格式

省级或直辖市: 第三,四位是 00
市级: 第五,六位是 00
省级直辖县: 第三,四位是 90

编码规则 引自维基百科

代码从左至右的含义是:

第一、二位表示省级行政单位(省、自治区、直辖市、特别行政区),其中第一位代表大区。

第三、四位表示地级行政单位(地级市、地区、自治州、盟及省级单位直属县级单位的汇总码)。

  对于省(自治区)下属单位:01-20,51-70表示省辖市(地级市);21-50表示地区(自治州、盟);90表示省(自治区)直辖县级行政区划的汇总。

  对于直辖市下属单位:01表示市辖区的汇总;02表示县的汇总。

第五、六位表示县级行政单位(县、自治县、市辖区、县级市、旗、自治旗、林区、特区)。

  对于地级市下属单位: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. 需要获取首页的数据

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 控制并发

区划代码 node 版爬虫尝试的更多相关文章

  1. Atitit 爬虫 node版 attilaxA

    Atitit 爬虫 node版 attilax 1.1. 貌似不跟python压实,,java的webmagic压实,,什么爬虫框架也没有,只好自己写了. 查了百度三爷资料也没有.都是自己写.. 1. ...

  2. Node.js aitaotu图片批量下载Node.js爬虫1.00版

    即使是https网页,解析的方式也不是一致的,需要多试试. 代码: //====================================================== // aitaot ...

  3. Node.js abaike图片批量下载Node.js爬虫1.01版

    //====================================================== // abaike图片批量下载Node.js爬虫1.01 // 1.01 修正了输出目 ...

  4. Node.js abaike图片批量下载Node.js爬虫1.00版

    这个与前作的差别在于地址的不规律性,需要找到下一页的地址再爬过去找. //====================================================== // abaik ...

  5. 80行Python代码搞定全国区划代码

    微信搜索:码农StayUp 主页地址:https://gozhuyinglong.github.io 源码分享:https://github.com/gozhuyinglong/blog-demos ...

  6. 【原】小玩node+express爬虫-2

    上周写了一个node+experss的爬虫小入门.今天继续来学习一下,写一个爬虫2.0版本. 这次我们不再爬博客园了,咋玩点新的,爬爬电影天堂.因为每个周末都会在电影天堂下载一部电影来看看. talk ...

  7. 一个超级简单的node.js爬虫(内附表情包)

    之所以会想到要写爬虫,并不是出于什么高大上的理由,仅仅是为了下载个表情包而已-- 容我先推荐一下西乔出品的神秘的程序员表情包. 这套表情包着实是抵御产品.对付测试.嘲讽队友.恐吓前任的良品, 不过不知 ...

  8. node.js爬虫杭州房产销售及数据可视化

    现在年轻人到25岁+,总的要考虑买房结婚的问题,2016年的一波房价大涨,小伙伴们纷纷表示再也买不起上海的房产了,博主也得考虑考虑未来的发展了,思考了很久,决定去杭州工作.买房.定居.生活,之前去过很 ...

  9. node.js爬虫

    这是一个简单的node.js爬虫项目,麻雀虽小五脏俱全. 本项目主要包含一下技术: 发送http抓取页面(http).分析页面(cheerio).中文乱码处理(bufferhelper).异步并发流程 ...

随机推荐

  1. iOS性能之其他

    本篇文章是个引用,因为这些技术我都只是研究过,但是并没有在项目中使用,也没有深入研究,所以只能当做一个笔记了 网络请求 现在大多数的网络请求都是使用的json格式(相信没有APP再使用XML格式了吧) ...

  2. Java进阶之多线程

    多线程 多线程(multiple thread)是计算机实现多任务并行处理的一种方式. 在单线程情况下,计算机中存在一个控制权,并按照顺序依次执行指令.单线程好像是一个只有一个队长指挥的小队,整个小队 ...

  3. java中的==、equals()、hashCode()源码分析

    转载自:http://www.cnblogs.com/xudong-bupt/p/3960177.html 在Java编程或者面试中经常会遇到 == .equals()的比较.自己看了看源码,结合实际 ...

  4. 【Ubuntu】您没有查看“sf_VirtualDisk”的内容所需的权限。

    原文链接:http://www.crifan.com/can_not_access_share_folder_in_ubuntu_virtualbox/ [问题] 之前已经搞定可以自动共享文件夹了: ...

  5. Redis基础学习(四)—Redis的持久化

    一.概述      Redis的强大性能很大程度上都是因为数据时存在内存中的,然而当Redis重启时,所有存储在内存中的数据将会丢失,所以我们要将内存中的数据持久化. Redis支持两种数据持久化的方 ...

  6. selenium实例:unittest框架+PO开发模式

    这是<selenium2+python学习总结>的升级版. 1.         项目结构 2.         项目代码 1)         globalparameter.py # ...

  7. AngularJS1.X学习笔记14-动画(解读文档)

    最近在看算法分析,那个大O啊,小o啊,分治法啊(目前就看到这里),真是搞死了.这回呢休息一下,学学AngularJS动画,上一篇文章根据自由男人的书简单谈到了动画的话题,发现反响很大(好吧,我说慌了, ...

  8. 微信小程序,前端大梦想(三)

    微信小程序的事件及生命周期   继续下节课,今天我们还是从四个方面来了解小程序:     ●常用事件和事件冒泡   ●配置   ●app生命周期及app对象的使用   ●页面的生命周期   一.事件的 ...

  9. 使用点击二分图计算query-document的相关性

    之前的博客中已经介绍了Ranking Relevance的一些基本情况(Click Behavior,和Text Match):http://www.cnblogs.com/bentuwuying/p ...

  10. KVO 模式详解

    KVO:观察者模式.当指定的被观察对象属性被修改时,允许对象接收到通知的机制. p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 18.0px Menlo; ...