从统计局采集最新的省市区县数据,纯js

作者: 高坚果兄弟 | 来源:发表于2018-01-27 14:51 被阅读45次

    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,数据全部搞完:


    数据问题

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

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

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

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

    5. 最终数据已上传了一份到CSDN,含所有代码和本文档:http://download.csdn.net/download/xiangyuecn/10226964

    相关文章

      网友评论

        本文标题:从统计局采集最新的省市区县数据,纯js

        本文链接:https://www.haomeiwen.com/subject/arhnaxtx.html