前言

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

网上只提供数据,好像很少有人提供方法。最近有时间就来做一次爬虫的初尝,有想法但无奈没学 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. spring(一) IOC讲解

    spring基本就两个核心内容,IOC和AOP.把这两个学会了基本上就会用了. --WH 一.什么是IOC? IOC:控制反转,通俗点讲,将对象的创建权交给spring,我们需要new对象,则由spr ...

  2. React+Node初尝试

    这是第一次写React和Node,选用的是前端Material-ui框架,后端使用的是Express框架,数据库采用的是Mongodb. 项目代码在:GitHub/lilu_movie 这是一个通过从 ...

  3. 多线程CountDownLatch和Join

    如果现在有五个线程A.B.C.D.E,请问如何用E线程用于统计A.B.C.D四个线程的结果? 题意需要用E线程统计A.B.C.D四个线程,也就是说E线程必须要等到前面四个线程运行结束之后才能执行.那么 ...

  4. sas2ircu工具信息收集及磁盘定位

    最近几台Dell服务器的磁盘损坏,报修厂商之后dell工程师需要手机机器磁盘插槽位置信息,使用的就是sas2ircu工具. 此工具还可以配置RAID信息,但是我这次只需要他的查看信息的功能,下面就开始 ...

  5. Java基础—String类小结

    一.String类是什么 public final class String implements java.io.Serializable, Comparable<String>, Ch ...

  6. FB,Flash,as3 Bug集

    一.Flash builder 报错 当导入3.0的项目时运行出现如下错误: 进程已终止,没有建立到调试器的连接.error while loading initial content 启动命令详细信 ...

  7. RabbitMQ入门教程

    1.下载安装RabbitMQ windows下 先 下载Erlang 64位 其它去这里下载 http://www.erlang.org/downloads 然后 下载RabbitMQ  官网 htt ...

  8. D3.js-数值自动变动的条形图表

    开始停止   // <p> <style><!-- button{ background-color:#aaaaaa; font-family:微软雅黑; font-si ...

  9. 10 分钟学会Linux常用 bash命令

    目录 基本操作 1.1. 文件操作 1.2. 文本操作 1.3. 目录操作 1.4. SSH, 系统信息 & 网络操作 基本 Shell 编程 2.1. 变量 2.2. 字符串替换 2.3. ...

  10. android开发用无线网络进行Android开发中的调试

    1.手机具有root权限 2.安装adbWireless1.5.4.apk (下面有下载地址) 3.敲入命令:adb connect 192.168.1.127  后面是手机的IP地址 打开eclip ...