本文更新:
19-03-22 新采集了2018的城市数据
18-11-28 采集了2017的城市数据

数据下载 GitHub:https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov/releases
<https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov/releases>
相关更新情况,请查阅我发布的其他文章,本文以下内容不再更新。

18-01-28早上6:30的火车,从三亚回老家,票难买啊。好激动~
声明:文中涉及到的数据和第三方接口、url仅供学习使用,请勿它用~


这几天都在磨着搭建本地测试环境,看到省市区数据表里面是空的,想着以前的老数据还是13年采集的,含省市区县4级数据共4.8万条,时间久了,使用过程中发现有些新的城市名称数据库中没有,县级数据从来就没有用到过,想着还是重新采集一份。

新采集的省市区数据有3589条,这次并没有把县级数据采过来,需要的时候再添加也挺好。

数据来源

国家统计局统计标准《2016年统计用区划代码和城乡划分代码(截止2016年07月31日)》,这个是2017-05-16发布的,当前是最新的。


数据采集


对于数据采集,根据工作需要,对于一些小的数据采集功能有些接触。因为对html和js熟些,很早以前就用IE浏览器对本地html文件支持任意跨域ajax请求数据、和支持读写Excel文件,就直接写一个html文件作为采集工具给别人使用,批量查询人员资料、考试结果什么的功能。所以采集省市区数据主要用的js。

1. 抓取原始数据

打开网页http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/index.html
省份的数据就有了,进入市级页面,然后进入区级页面,还可以进入县级页面。整个流程地址结构非常简单,数据格式也很好提取。


进入网页后打开浏览器控制台,执行下面代码,这段代码仅仅包含采集省市区的,把县级的阉割掉了,13年的老代码有县级的。很早以前写的代码,风格有点丑,不过能能正常使用就是好的,这个采集是“单线程的”,因为这些数据少,速度并不慢:
/* 获取城市名称http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/index.html */
(function(){ if(!window.URL){ throw new Error("浏览器版本太低"); }; function
ajax(url,True,False){ var ajax=new XMLHttpRequest(); ajax.timeout=1000;
ajax.open("GET",url); ajax.onreadystatechange=function(){
if(ajax.readyState==4){ if(ajax.status==200){ True(ajax.responseText); }else{
False(); } } } ajax.send(); } function msg(){ console.log.apply(console,
arguments); } function cityClass(name,url,code){ this.name=name; this.url=url;
this.code=code; this.child=[]; this.tryCount=0; } cityClass.prototype={
getValue:function(){ var obj={name:this.name,code:this.code,child:[]}; for(var
i=0;i<this.child.length;i++){ obj.child.push(this.child[i].getValue()); }
return obj; } } function load_all(True){ var
path="http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016";
ajax(path+"/index.html",function(text){ var
reg=/href='(.+?)'>(.+?)<br/ig,match; var idx; if((idx=text.indexOf("<tr
class='provincetr'>"))+1){ reg.lastIndex=idx; while(match=reg.exec(text)){ var
url=match[1]; if(url.indexOf("//")==-1 && url.indexOf("/")!=0){
url=path+"/"+url; } var name=match[2]; DATA.push(new cityClass(name,url,0)); }
True(); }else{ msg("未发现省份数据"); } },function(){ msg("读取省份列表出错","程序终止"); }); }
function load_shen(True, False){ var city=DATA[JD.shen]; city.tryCount++;
if(city.tryCount>3){ msg("读取省份["+city.name+"]超过3次"); False(); return; };
function get(){ msg("读取省份["+city.name+"]", getJD()); save();
city.child[JD.si].tryCount=0; load_si(function(){ JD.shen++;
if(JD.shen>=DATA.length){ JD.shen=0; True(); return; };
DATA[JD.shen].tryCount=0; load_shen(True,False); },function(){ False(); }); }
if(city.child.length){ get(); }else{ ajax(city.url,function(text){ var reg=/<tr
class='citytr'>.+?href='(.+?)'>(.+?)<.+?'>(.+?)</ig; var match;
while(match=reg.exec(text)){ var url=match[1]; if(url.indexOf("//")==-1 &&
url.indexOf("/")!=0){
url=city.url.substring(0,city.url.lastIndexOf("/"))+"/"+url; } var
code=match[2]; var name=match[3]; city.child.push(new
cityClass(name,url,code)); } JD.si=0; get(); },function(){
load_shen(True,False); }); }; } function load_si(True,False){ var
shen=DATA[JD.shen]; var city=shen.child[JD.si]; city.tryCount++;
if(city.tryCount>3){ msg("读取城市["+city.name+"]超过3次"); False(); return; };
function get(){ msg("___读取城市["+city.name+"]", getJD());
city.child[JD.xian].tryCount=0; JD.si++; if(JD.si>=shen.child.length){ JD.si=0;
True(); return; }; shen.child[JD.si].tryCount=0; load_si(True,False); }
if(city.child.length){ get(); }else{ ajax(city.url,function(text){ var
reg=/class='(?:countytr|towntr)'.+?<\/tr>/ig; var match;
while(match=reg.exec(text)){ var reg2=/class='(?:countytr|towntr)'.+?(?:<td><a
href='(.+?)'>(.+?)<.+?'>(.+?)<|<td>(.+?)<.+?<td>(.+?)<)/ig; var match2;
if(match2=reg2.exec(match[0])){ var url=match2[1]||""; if(url.indexOf("//")==-1
&& url.indexOf("/")!=0){
url=city.url.substring(0,city.url.lastIndexOf("/"))+"/"+url; } var
code=match2[2]||match2[4]; var name=match2[3]||match2[5]; city.child.push(new
cityClass(name,url,code)); }else{ msg("未知城市模式:"); msg(city.url); msg(match[0]);
throw new Error("end"); } } JD.xian=0; get(); },function(){
load_si(True,False); }); }; } function getJD(){ var
str="省:"+(JD.shen+1)+"/"+DATA.length; var shen=DATA[JD.shen]; if(shen){ str+="
市:"+(JD.si+1)+"/"+shen.child.length; var si=shen.child[JD.si]; if(si){ str+="
县:"+(JD.xian+1)+"/"+si.child.length; }else{ str+=" 县:"+JD.xian; } }else{ str+="
市:"+JD.si+" 县:"+JD.xian; } return str; } function save(){ } var DATA=[]; var
JD; window.RunLoad=function(shen,si,xian){ RunLoad.T1=Date.now(); JD={
shen:shen||0 ,si:si||0 ,xian:xian||0 } function get(){
DATA[JD.shen].tryCount=0; load_shen(function(){
console.log("完成:"+(Date.now()-RunLoad.T1)/1000+"秒"); save(); var data=[];
for(var i=0;i<DATA.length;i++){ data.push(DATA[i].getValue()); } var
url=URL.createObjectURL( new Blob([ new Uint8Array([0xEF,0xBB,0xBF]) ,"var
CITY_LIST=" ,JSON.stringify(data,null,"\t") ] ,{"type":"text/plain"}) ); var
downA=document.createElement("A"); downA.innerHTML="下载查询好城市的文件";
downA.href=url; downA.download="data.txt"; document.body.appendChild(downA);
downA.click(); msg("--完成--"); },function(){ save(); msg("当前进度:", getJD()); });
} var data=localStorage["load_data"]; if(data){ DATA=JSON.parse(data); get();
}else{ load_all(get); } } })();//@ sourceURL=console.js //立即执行代码 RunLoad()
采集截图:


2. 处理数据和拼音标注

数据处理就简单些了,比如编号格式化、名称格式化等。

拼音标注:这个需要找一个接口对文字进行拼音翻译,只有一个要求:重庆能正常的翻译成chong qing即可,翻译成zhong
qing的就low了。满足这个条件,百度上搜索到的翻译小网站80%就被干掉了。

浏览器中打开找到的翻译接口http://www.qqxiuzi.cn/zh/pinyin/
,截止到目前是能正常调用的,因为要用ajax请求数据,在页面里面就没有跨域的问题,查看网页源码,把token值记录下来,这个网站翻译请求需要带这个token,注意~刷新页面要重新获取:



拼音这个因为数据量比较多,采用了“4个线程”采集,先把第一步采集到的文件打开,把数据复制到打开的翻译网站浏览器控制台里面执行(相当于把数据导入),然后执行下面代码:
/* 拼音翻译 http://www.qqxiuzi.cn/zh/pinyin/
http://www.qqxiuzi.cn/zh/pinyin/show.php POST
t=汉字&d=1&s=null&k=1&b=null&h=null&u=null&v=1&y=null&z=null&token=页面token请求一次获取
先加载数据 控制台输入data.txt */ window.PageToken=window.PageToken||""; var
FixTrim=function(name){ return name.replace(/^\s+|\s+$/g,""); }; var
CITY_LIST2; var QueryPinYin=function(end){ if(!window.PageToken){
console.error("Need PageToken"); return; }; var ids=[]; var
fixCode=function(o){ if(o.deep==0){ o.orgCode="0"; }else{ o.orgCode=o.code;
if(o.deep==1){ o.code=o.code.substr(o,4); }else{
o.code=o.code.replace(/(000000|000)$/g,"");//有少部分区多3位 }; }; return o; }; var
fix=function(o,p){ var name=o.name; if(o.deep==0){
name=name.replace(/(市|省|(维吾尔|壮族|回族)?自治区)$/ig,""); }else if(o.deep==1){
if(name=="市辖区"){ name=p.o2.name; }else if(/行政区划$/ig.test(name)){ name="直辖市";
}else if(name.length>2){ name=name.replace(/市$/ig,""); }; }else{
if(name.length>2 && name!="市辖区" && !/(自治.|地区|矿区)$/.test(name)){//直接排除会有同名的
name=name.replace(/(市|区|县|镇|管委会|街道办事处)$/ig,""); }; }; var o2={ name:name
,ext_name:o.name ,id:+o.code||0 ,ext_id:+o.orgCode ,pid:p&&+p.code||0
,deep:o.deep }; o.o2=o2; return o2; }; for(var i=0;i<CITY_LIST.length;i++){ var
shen=CITY_LIST[i]; shen.deep=0; for(var i2=0;i2<shen.child.length;i2++){ var
si=shen.child[i2]; if(!shen.code){ shen.code=si.code.substr(0,2);
ids.push(fix(fixCode(shen))); }; si.deep=1; ids.push(fix(fixCode(si),shen));
for(var i3=0;i3<si.child.length;i3++){ var qu=si.child[i3]; qu.deep=2;
ids.push(fix(fixCode(qu),si)); }; }; }; CITY_LIST2=ids;
//console.log(JSON.stringify(ids,null,"\t")) //return; var idx=-1; var
run=function(stack){ stack=+stack||0; idx++; if(idx>=ids.length){ thread--;
if(thread==0){ end(); }; return; }; var idx_=idx; var id=ids[idx]; if(id.P){
stack++; if(stack%50==0){ setTimeout(function(){run()}); }else{ run(stack); };
return; }; var name=id.name; var tryCount=0; var tryLoad=function(){ $.ajax({
url:"/zh/pinyin/show.php"
,data:"t="+encodeURIComponent(name)+"&d=1&s=null&k=1&b=null&h=null&u=null&v=1&y=null&z=null&token="+PageToken
,type:"POST" ,dataType:"text" ,timeout:1000 ,error:function(e){ if(tryCount>3){
console.error("--QueryPinYin error--"+e); run(); return; }; tryCount++;
tryLoad(); } ,success:function(txt){
txt=FixTrim(txt.replace(/<.+?>/g,"").replace(/\s+/g," ")); id.P=txt;
console.log("--"+idx_+"-QueryPinYin "+name+":"+txt+" --"); run(); } }); };
tryLoad(); }; var thread=4; run(); run(); run(); run(); }; var
ViewDown=function(){ console.log("完成:"+(Date.now()-RunPinYin.T1)/1000+"秒");
window.CITY_LIST_PINYIN=CITY_LIST2; var url=URL.createObjectURL( new Blob([ new
Uint8Array([0xEF,0xBB,0xBF]) ,"var CITY_LIST_PINYIN="
,JSON.stringify(CITY_LIST2,null,"\t") ] ,{"type":"text/plain"}) ); var
downA=document.createElement("A"); downA.innerHTML="下载查询好城市的文件";
downA.href=url; downA.download="data-pinyin.txt";
document.body.appendChild(downA); downA.click(); }; var RunPinYin=function(){
RunPinYin.T1=Date.now(); QueryPinYin(ViewDown); }; //立即执行代码
if(window.CITY_LIST){ if(!PageToken){ PageToken=prompt("Token"); };
RunPinYin(); }else{ console.error("data.txt未输入"); };
这时候会提示输入token,把刚才找到的token粘贴进去,然后就开始工作了:


还挺快的,2分钟多点全部翻译完成。

3. 格式化成CSV

数据全部有了,导出成比较正常使用的格式,CSV最好了。这个导出比较简单,任意网页控制台把第二部保存的文件打开,复制数据到任意网页控制台,然后输入以下代码:
/* 格式并且输出为csv 先加载数据 控制台输入data-pinyin.txt 导入数据库: 文件格式Unicode,文字为字符流
检查id重复项,修正id 转入area_city 增加港澳台、海外两个省级 检查名称重复项,修正名称 select * from area_city
where len(name)=1 select pid,name,count(*) from area_city group by pid,name
having COUNT(*)>1 */ var FixTrim=function(name){ return
name.replace(/^\s+|\s+$/g,""); }; function CSVName(name){ return
'"'+FixTrim(name).replace(/"/g,'""')+'"'; }; var
CITY_CSV=["id,pid,deep,name,pinyin_prefix,pinyin,ext_id,ext_name"]; for(var
i=0;i<CITY_LIST_PINYIN.length;i++){ var o=CITY_LIST_PINYIN[i]; var pf=""; var
pinyin=FixTrim(o.P).toLowerCase(); var ps=pinyin.split(" "); for(var
j=0;j<ps.length&&j<3;j++){ pf+=ps[j].substr(0,j==0?2:1); };
CITY_CSV.push(o.id+","+o.pid+","+o.deep+","+CSVName(o.name)
+","+CSVName(pf)+","+CSVName(o.P)
+","+CSVName(o.ext_id+"")+","+CSVName(o.ext_name)); }; var
url=URL.createObjectURL( new Blob([ new Uint8Array([0xEF,0xBB,0xBF])
,CITY_CSV.join("\n") ] ,{"type":"text/plain"}) ); var
downA=document.createElement("A"); downA.innerHTML="下载查询好城市的文件";
downA.href=url; downA.download="ok_data.csv"; document.body.appendChild(downA);
downA.click();
OK,数据全部搞完:


数据问题

*
id编号和国家统计局的编号基本一致,方便以后更新。

*
id重复项目前是没有(已优化过了),不过以前采集后直接对统计局的编号进行简单缩短后会有重复现象(算是精度丢失)。

*
拼音前缀取的是第一个字前两个字母和后两个字首字母,意图是让第一个字相同名称的尽量能排序在一起。排序1:黑龙江helj、湖北hub、湖南hun;排序2:
湖北hb、黑龙江hlj、湖南hn,排序一胜出。

*
因为区名字是直接去掉市、区后缀,存在那么几对名字变得完全一样的,需要手动吧市区后缀加上,不然会产生小问题。

*
最终数据已上传了一份到CSDN,含所有代码和本文档:http://download.csdn.net/download/xiangyuecn/10226964
,GitHub下载最新数据
<https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov/releases>