国家统计局全国统计用区划代码和城乡划分代码Python爬虫样例

为了项目后续数据需要做准备,开始渐进深入去学习爬虫,最近做了一个实战样例demo,写了一个爬虫,获取全国统计用区划代码。数据来源,国家统计局:http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2019/

整体分析一下,这个网站的布局样式简直不忍直视,可以说是一览无遗,基本上啥都没有,突出了政府网站一贯的简洁高效风格。

我将按照代码顺序,差穿插着说明开发思路过程。

代码目录:

国家统计局全国统计用区划代码和城乡划分代码Python爬虫样例_第1张图片

先介绍下用到的基础python文件。

Bean包下两个类,NbsRegionDTO用于存放爬取解析后的数据,便于最后存库,字段和数据库对应。

'''
国家统计局行政区划实体类
'''
class NbsRegionDTO:

    # 统计用区划代码
    nbs_code = ''

    #'国家统计局父级统计用区划code',
    nbs_parent_code = ''

    # '国家统计局区域层级', 1 - 5
    nbs_level = 0

    # '国家统计局名称',
    nbs_name = ''

    # 城乡分类代码  五级才有数据
    nbs_town_country_code = ''


    #定义构造方法
    def __init__(self,nbs_code,nbs_parent_code,nbs_level,nbs_name,nbs_town_country_code):
        self.nbs_code = nbs_code
        self.nbs_parent_code = nbs_parent_code
        self.nbs_level = nbs_level
        self.nbs_name = nbs_name
        self.nbs_town_country_code = nbs_town_country_code
ParseErrorClass 用于封装爬取过程中出现问题的数据信息,便于最后集中处理。
'''
解析暂时出现异常的数据实体类
'''
class ParseErrorClass:

    # 当前 父节点code
    parent_code = ''

    # 当前  父节点区划等级
    parent_level = 0

    # 当前待解析url
    to_parse_url = ''

    # 定义构造方法
    def __init__(self, parent_code, parent_level, to_parse_url):
        self.parent_code = parent_code
        self.parent_level = parent_level
        self.to_parse_url = to_parse_url


util包下有两个工具类文件   

UrlGetUtil 用于抓取相关url页面,里面提供两种方法,request.get() 和 urlopen() ,两种方式都是可行的。

import sys
from urllib.request import urlopen

from pip._vendor import requests

'''
工具类
'''
class UrlGetUtil:

    '''
    url 待抓取url
    tarBianMa 目标编码,eg: 'gbk'
    '''
    def getByRequestGet(self,url,tarBianMa):
        response = requests.get(url)
        #print(response.encoding) #查看现有编码
        response.encoding = tarBianMa  # 改变编码
        #print(response.encoding)#查看改变后的编码
        html = response.text
        return html

    '''
    url 待抓取url
    tarBianMa 目标编码,eg: 'gbk'
    '''
    def getByUrlOpen(self,url,tarBianMa):
        #10s超时
        html_obj = urlopen(url,timeout = 10)
        html = html_obj.read().decode(tarBianMa)
        return html
DataBaseUtil 提供两个方法,用于获取连接对象和光标对象
import pymysql
'''
数据库操作工具类
'''
class DataBaseUtil:

    '''获取连接对象'''
    def getConnObj(self,host_param,unix_socket_param,user_param,passwd_param,db_param):
        conn = pymysql.connect(host=host_param,unix_socket=unix_socket_param,user=user_param,passwd=passwd_param,db=db_param,charset='utf8')
        return conn
    '''
    获取光标对象
    参数:连接对象conn
    '''
    def getCurObj(self,conn):
        return conn.cursor()

另外还有三个文件,NbsMain是程序运行入口,NbsCycleSpider用于深层次递归爬取下级行政区划数据,SaveData用于存储数据到数据库。

★SaveData中主要就是一个批量插入数据到mysql的方法。

1,方法参数传进来是一个集合,里面存放一个个封装好的数据NbsRegionDTO对象,为了便于后面批量插入,先把集合参数tar_obj_set处理转成列表套元组的形式。

2,准备数据库连接参数,用户名,密码,数据库,ip等老几样数据,获取连接对象和光标对象。

3,执行sql,关于批量插入的注意事项,代码中的注释有详述

4,最后,记得关闭连接和光标,防止泄露。


'''
国家统计局 数据入库存储
'''
from NbsRegionSpider.Util.DataBaseUtil import DataBaseUtil

#批量插入
def nbsDataToSaveBatch(tar_obj_set):
    #参数处理成列表套元组的形式
    tar_list = list()   #或  tar_list = []
    for tar_obj in tar_obj_set:
        tar_tuple = (tar_obj.nbs_code,tar_obj.nbs_parent_code,tar_obj.nbs_level,tar_obj.nbs_name,tar_obj.nbs_town_country_code)
        tar_list.append(tar_tuple)
    # 数据库信息
    dataBaseUtilObj = DataBaseUtil()
    host_param = 'xxxx.xx.xx.xx'
    unix_socket_param = ''
    user_param = 'root'
    passwd_param = 'xxxxx'
    db_param = 'qqq'
    conn = dataBaseUtilObj.getConnObj(host_param,unix_socket_param,user_param,passwd_param,db_param)
    cur = dataBaseUtilObj.getCurObj(conn)

    #执行sql
    cur.execute("use qqq")
    # 注意这里使用的是executemany而不是execute,下边有对executemany的详细说明
    '''
    另外,针对executemany
    execute(sql) : 接受一条语句从而执行
    executemany(templet,args):能同时执行多条语句,执行同样多的语句可比execute()快很多,强烈建议执行多条语句时使用executemany
        templet : sql模板字符串,  例如 ‘insert into table(id,name,age) values(%s,%s,%s)’
        args: 模板字符串中的参数,是一个list,在list中的每一个元素必须是元组!!!  例如: [(1,‘mike’),(2,‘jordan’),(3,‘james’),(4,‘rose’)]
    '''
    cur.executemany('insert into nbs_region(nbs_code,nbs_parent_code,nbs_level,nbs_name,nbs_town_country_code) values (%s,%s,%s,%s,%s)',tar_list)
    conn.commit()
    cur.close()
    conn.close()

★NbsMain:

基本逻辑

        在NbsMain中解析省一级数据,然后在NbsCycleSpider中递归该省的下级数据的深度爬虫,把每一层的爬取结果集合,返回到上一级,再同本级结果集合取并集来合并所有结果,同时把出现爬取异常的数据暂存到错误数据集合,最终返回结果集到NbsMain中,然后执行该省数据的存储入库,然后循环下一个省份的处理。

       最后再处理上述过程中产生的错误数据集合error_data_set,进行重试爬取并入库,直到error_data_set容量为0 为止。

import time
from urllib.request import urlopen
from bs4 import BeautifulSoup
from NbsRegionSpider.Bean.NbsRegionDTO import NbsRegionDTO
from NbsRegionSpider.NbsCycleSpider import cycleSpider, error_data_set
from NbsRegionSpider.SaveData import nbsDataToSaveBatch
from NbsRegionSpider.Util.UrlGetUtil import UrlGetUtil

'''国家统计局省级区域数据爬取 程序入口'''
base_url = 'http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2019/'
urlGetUtilObj = UrlGetUtil()
#两种方式二选一来爬取url页面
#urlopern方式
html = urlGetUtilObj.getByUrlOpen(base_url,'gbk')
# request.get 方式
#html = urlGetUtilObj.getByRequestGet(base_url,'gbk')
'''
异常:Some characters could not be decoded, and were replaced with REPLACEMENT CHARACTER.
https://www.cnblogs.com/HANYI7399/p/6080070.html
'''
#bs = BeautifulSoup(html, 'html.parser', from_encoding = "iso-8859-1")
bs = BeautifulSoup(html,'html.parser')
#找出存放省信息的tr行
trs = bs.findAll('tr',{'class':'provincetr'})
#先取出所有的省数据节点
data_item_set = set()
for tr in trs:
	tds = tr.findAll('td')
	for td in tds:
		a_flag = td.a
		if a_flag is None:
			continue
		data_item_set.add(a_flag)

# 每一轮节点的循环都是对一个省的数据处理
for a_flag in data_item_set:
	# 本省所有行政区划结果集
	tar_obj_set = set()
	#解析数据
	province_name = a_flag.get_text()# 获取省名称
	print('-'*12 +province_name+'---爬取开始'+ '-'*12)
	province_code = ''
	to_spider_url = a_flag['href']
	if len(province_name) == 0:
		continue
	if len(to_spider_url) != 0:
		province_code = ''.join(filter(str.isdigit, to_spider_url))#列表转字符串,获取省一级code
	# 省一级数据封装成实体
	tarObj = NbsRegionDTO(province_code,'',1,province_name,'')
	grade_2_url = base_url + to_spider_url #基础路径拼接当前路径 相当于 下一级的url解析路径
	tar_obj_set.add(tarObj)
	#调用循环方法,去解析子层级区域
	tar_obj_set_result = cycleSpider(province_code,1,grade_2_url)
	# 合并两个集合结果集【取并集】
	tar_obj_set = tar_obj_set | tar_obj_set_result
	# for item in tar_obj_set:
	# 	print(item.nbs_code + '------' + item.nbs_parent_code + '------' + item.nbs_name + '----------' + item.nbs_town_country_code)
	print('-'*12 +province_name+'---爬取结束'+'-'*12)
	time.sleep(10)
	#本省数据入库存储
	try:
		nbsDataToSaveBatch(tar_obj_set)
	except Exception as e:
		print('-'*12 +province_name+'---数据入库存储异常')
		print(e)

#全国数据初次处理完毕,开始处理整体过程中失败的数据,重试
print('------开始处理初次全国爬取失败的数据,共------'+str(len(error_data_set))+'条')
while(len(error_data_set) > 0):
	#暂存入库成功的数据,最后一次性从error_data_set删除掉
	temp_set = set()
	#逐一对错误数据进行解析,存储
	for item in error_data_set:
		tar_save_set = cycleSpider(item.parent_code, item.parent_level, item.to_parse_url)
		#存储入库
		try:
			nbsDataToSaveBatch(tar_save_set)
		except:
			print('---某个全国爬取过程的错误数据重试爬取后入库存储异常,该数据打印如下:'+item.parent_code + '---' + str(item.parent_level) + '---' + item.to_parse_url)
		#该错误数据成功入库,先暂存,最后将其从集合中删除
		temp_set.add(item)
	#更新error_data_set
	if(len(temp_set) > 0):
		for item in temp_set:
			error_data_set.discard(item)
	time.sleep(10)

NbsCycleSpider:

'''
    多层级循环递归调用解析区域,然后返回数据集合
'''
import socket
import time
from urllib.error import HTTPError
from urllib.request import urlopen
from bs4 import BeautifulSoup
from NbsRegionSpider.Bean.NbsRegionDTO import NbsRegionDTO

from NbsRegionSpider.Bean.ParseErrorClass import ParseErrorClass
from NbsRegionSpider.Util.UrlGetUtil import UrlGetUtil

#全局变量,存储爬取过程中出现错误的数据
error_data_set = set()

def cycleSpider(parent_code,parent_level,to_parse_url):
    # 本次爬取行政区划数据结果集
    tar_obj_set = set()
    #按层级使用不同的解析标签关键字
    tr_flag = ''
    current_level = parent_level + 1#级别级别
    if current_level == 2:
        tr_flag = 'citytr'
    elif current_level == 3:
        tr_flag = 'countytr'
    elif current_level == 4:
        tr_flag = 'towntr'
    elif current_level == 5:
        tr_flag = 'villagetr'
    else:
        pass
    #解析
    try:
        urlGetUtilObj = UrlGetUtil()
        # urlopern方式
        html = urlGetUtilObj.getByUrlOpen(to_parse_url,'gbk')
        # request.get 方式
        # html = urlGetUtilObj.getByRequestGet(to_parse_url,'gbk')
    except socket.timeout:
        print('待解析URL:' + to_parse_url + '请求超时')
        # 暂时跳过,将出现异常的待解析数据暂存起来
        error_data_set.add(ParseErrorClass(parent_code, parent_level, to_parse_url))
        return tar_obj_set
    except HTTPError as e:
        print('待解析URL:' + to_parse_url + '出现http错误:'+e)
        # 暂时跳过,将出现异常的待解析数据暂存起来
        error_data_set.add(ParseErrorClass(parent_code, parent_level, to_parse_url))
        return tar_obj_set
    except Exception:
        print('待解析URL:'+to_parse_url+'请求出现异常')
        #暂时跳过,将出现异常的待解析数据暂存起来
        error_data_set.add(ParseErrorClass(parent_code,parent_level,to_parse_url))
        return tar_obj_set
    else:
        pass
    bs = BeautifulSoup(html, 'html.parser')
    #获取本页所有数据节点
    trs = bs.findAll('tr', {'class': tr_flag})
    for tr in trs:
        #注意,5极页面有三个td,和别的等级页面中的相同td位置,存放数据不是一样的类型,所以进行判断
        td_1 = tr.find('td')#第一个td节点
        current_code = td_1.get_text()
        current_url = ''
        current_name = ''
        current_town_country_code = ''
        td_2 = td_1.next_sibling#第二个td节点
        if(current_level == 5):
            current_town_country_code = td_2.get_text()
            current_name = td_2.next_sibling.get_text()
        else:
            td_1_a = td_1.a
            if td_1_a is not None:  #比如青海省西宁市市辖区 ,才到三级,就咩有下级了,所以a标签为None对象
                current_url = td_1_a['href']
            current_name = td_2.get_text()
        #打印开始日志
        if (current_level == 2):
            print('-'*8 + current_name + '---开始')
        elif(current_level == 3):
            print('-' * 4 + current_name + '---开始')
        #封装数据
        tarObj = NbsRegionDTO(current_code, parent_code, current_level, current_name, current_town_country_code)
        tar_obj_set.add(tarObj)
        # 递归调用,获取下级数据
        tar_obj_set_result = set()
        if (current_level != 5 and current_url != ''): # 五级一定没有下级
            # 当前解析页面截取最后一个'/'之前的url ,再拼接当前页面的href   就是下一级别的解析url
            pos = to_parse_url.rfind("/")
            next_url_data = to_parse_url[:pos] + '/' + current_url
            tar_obj_set_result = cycleSpider(current_code,current_level,next_url_data)
        # 合并两个集合结果集【取并集】 并返回
        tar_obj_set = tar_obj_set | tar_obj_set_result

    if(current_level == 2 or current_level == 3):
        time.sleep(10)
    return tar_obj_set






部分运行日志:

国家统计局全国统计用区划代码和城乡划分代码Python爬虫样例_第2张图片

代码中已经写了详细的步骤注释,理解起来应该没有什么问题。不过还有很大的改进空间,比如改当前的深度爬取为广度爬取,可以有效降低服务器负载等,后续不断积累经验,越来越好吧。欢迎批评指正交流。

你可能感兴趣的:(04,python,python,爬虫)