为了项目后续数据需要做准备,开始渐进深入去学习爬虫,最近做了一个实战样例demo,写了一个爬虫,获取全国统计用区划代码。数据来源,国家统计局:http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2019/
整体分析一下,这个网站的布局样式简直不忍直视,可以说是一览无遗,基本上啥都没有,突出了政府网站一贯的简洁高效风格。
我将按照代码顺序,差穿插着说明开发思路过程。
代码目录:
先介绍下用到的基础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
部分运行日志:
代码中已经写了详细的步骤注释,理解起来应该没有什么问题。不过还有很大的改进空间,比如改当前的深度爬取为广度爬取,可以有效降低服务器负载等,后续不断积累经验,越来越好吧。欢迎批评指正交流。