全部代码以及分析见GitHub:https://github.com/dta0502/NBSPRC-spider
页面分析
这里我分析一下页面,为后面的页面解析做准备。我就拿2016年的页面做下分析:2016年统计用区划代码和城乡划分代码(截止2016年07月31日)。
省级页面分析
省级信息提取
我们进入到2016年统计用区划代码和城乡划分代码(截止2016年07月31日)这个页面,然后用chrome的“检查”工具看下我们要找的信息在哪。
这里我们需要爬取省级名称、省内市级信息的子链接这两个参数。
我们从图中可以发现,左边页面每一行对应的XPath路径为:
//tr[@class="provincetr"]
然后一行中每个省的信息在下一级的td标签内:
td/a/text()
td/a/@href
下级链接获取
省级页面的URL:
http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/index.html
下级页面的URL(我这里以浙江省
为例):
http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/33.html
页面中提取到的信息(我这里以浙江省
为例):
33.html
所以我们可以通过如下方式获取真实的URL保存到一个列表中:
url = "http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/index.html"
# provinceLink = "33.html"
provinceURL = url[:-10] + provinceLink
市级页面分析
市级信息提取
我们进入到浙江省中。具体的分析跟上面的省级页面分析类似,不再赘述。下面是市级页面分析图:
下级链接获取
市级页面的URL:
http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/33.html
下级页面的URL(我这里以杭州市
为例):
http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/33/3301.html
页面中提取到的信息(我这里以杭州市
为例):
33/3301.html
区级页面分析
区级信息提取
我们进入到杭州市中。具体的分析跟上面的省级页面分析类似,不再赘述。下面是区级页面分析图:
下级链接获取
区级页面的URL:
http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/33/3301.html
下级页面的URL(我这里以上城区
为例):
http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/33/01/330102.html
页面中提取到的信息(我这里以上城区
为例):
01/330102.html
街道页面分析
街道信息提取
我们进入到上城区中。具体的分析跟上面的省级页面分析类似,不再赘述。下面是街道页面分析图:
下级链接获取
街道页面的URL:
http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/33/01/330102.html
街道页面的URL(我这里以湖滨街道
为例):
http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/33/01/02/330102003.html
页面中提取到的信息(我这里以湖滨街道
为例):
02/330102003.html
居委会页面分析
居委会信息提取
我们进入到湖滨街道中。具体的分析跟上面的省级页面分析类似,不再赘述。下面是居委会页面分析图:
这里已经到了最底层,没有下级链接了。
国家统计局的统计用区划代码和城乡划分代码爬取—第一版
下面我们开始一步步爬取过程。
导入库
import requests
from lxml import etree
import csv
主页面爬取过程
url = "http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/index.html"
headers={'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'}
这里需要注意一下,国家统计局的统计用区划代码和城乡划分代码网页的编码为gb2312
。一般我都采用下面的代码来获取页面:
data = requests.get(url,headers = headers).text
但是在碰到这个编码方式时会出现乱码,所以做出如下改变:
response = requests.get(url,headers = headers)
response.apparent_encoding
'GB2312'
response.encoding = response.apparent_encoding
data = response.text
页面解析过程
省级名称、URL获取
selector = etree.HTML(data)
provinceList = selector.xpath('//*[@class="provincetr"]')
provinceList
[<Element tr at 0x54a4c88>,
<Element tr at 0x54b4148>,
<Element tr at 0x54b4188>,
<Element tr at 0x54b41c8>]
for i in provinceList:
provinceName = i.xpath('td/a/text()')
provinceLink = i.xpath('td/a/@href')
下面是根据获取到的每个省的链接进行补全,得到真实的URL。
provinceURL = provinceLink
for i in range(len(provinceLink)):
for j in range(len(provinceLink[i])):
provinceURL[i][j][0] = url[:-10] + provinceLink[i][j][0]
provinceURL[0:2]
['http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/11.html',
'http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/12.html']
市级代码、URL获取
cityCode = [] #第二维的数组
cityLink = []
cityName = []
for provURL in provinceURL:
response = requests.get(provURL,headers = headers)
response.encoding = response.apparent_encoding
data = response.text
selector = etree.HTML(data)
cityList = selector.xpath('//tr[@class="citytr"]')
#下面是抓取每一个城市的代码、URL
Code = [] #第一维的数组:里面存储了一个省下辖的市的信息
Link = []
Name = []
for i in cityList:
Code.append(i.xpath('td[1]/a/text()'))
Link.append(i.xpath('td[1]/a/@href'))
Name.append(i.xpath('td[2]/a/text()'))
cityCode.append(Code)
cityLink.append(Link)
cityName.append(Name)
cityCode[3][0:2]
[['140100000000'], ['140200000000']]
cityLink[3][0:2]
[['14/1401.html'], ['14/1402.html']]
cityName[3][0:2]
[['太原市'], ['大同市']]
下面是根据获取到的每个市的链接进行补全,得到真实的URL。
cityLink[0][0][0]
'11/1101.html'
cityURL = cityLink
for i in range(len(cityLink)):
for j in range(len(cityLink[i])):
cityURL[i][j][0] = url[:-10] + cityLink[i][j][0]
cityURL[3][0:2]
[['http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/14/1401.html'],
['http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/14/1402.html']]
区级代码、URL获取
由于之前获得的市级链接是多维列表的形式,为了后面能够很方便的获取页面,下面将这个链接转换成一维列表形式。
cityURL_list = []
for i in range(len(cityURL)):
for j in range(len(cityURL[i])):
cityURL_list.append(cityURL[i][j][0])
cityURL_list[0:2]
['http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/11/1101.html',
'http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/12/1201.html']
这里计算下市级区域的个数(为了得到区级信息,需要请求344个url)。
len(cityURL_list)
344
多线程
由于这里的网页请求很多,我将采用多线程来加快速度。
from queue import Queue
from threading import Thread
qurl = Queue() #队列
thread_num = 10 #进程数
#下面三个全局变量是每个区的代码、URL、名称信息
countyCode = []
countyURL = []
countyName = []
def produce_url(url_list):
for url in url_list:
qurl.put(url) # 生成URL存入队列,等待其他线程提取
def getCounty():
while not qurl.empty(): # 保证url遍历结束后能退出线程
url = qurl.get() # 从队列中获取URL
response = requests.get(url,headers = headers)
response.encoding = response.apparent_encoding
data = response.text
selector = etree.HTML(data)
countyList = selector.xpath('//tr[@class="countytr"]')
#下面是爬取每个区的代码、URL
for i in countyList:
countyCode.append(i.xpath('td[1]/a/text()'))
countyURL.append(i.xpath('td[1]/a/@href'))
countyName.append(i.xpath('td[2]/a/text()'))
def run(url_list):
produce_url(url_list)
ths = []
for _ in range(thread_num):
th = Thread(target = getCounty)
th.start()
ths.append(th)
for th in ths:
th.join()
run(cityURL_list)
写入csv文件
with open('county_v1.csv','w',newline = '',encoding = 'utf-8') as f:
writer = csv.writer(f)
writer.writerow(["countyCode","countyName","countyURL"])
for i in range(len(countyCode)):
writer.writerow([countyCode[i],countyName[i],countyURL[i]])
后面的街道信息、居委会信息爬取的代码类似,我就不继续写了。
设计过程中遇到的问题
1)中文乱码问题
分析requests的源代码发现,text返回的是处理过的Unicode型的数据,而使用content返回的是bytes型的原始数据。也就是说,r.content相对于r.text来说节省了计算资源,content是把内容bytes返回. 而text是decode成Unicode。
如果直接采用下面的代码获取数据,会出现中文乱码的问题。
data = requests.get(url,headers = headers).text
原因在于:《HTTP权威指南》里第16章国际化里提到,如果HTTP响应中Content-Type字段没有指定charset,则默认页面是’ISO-8859-1’编码。这处理英文页面当然没有问题,但是中文页面,就会有乱码了!
解决办法:如果在确定使用text,并已经得知该站的字符集编码(使用apparent_encoding可以获得真实编码)时,可以使用 r.encoding = ‘xxx’ 模式, 当你指定编码后,requests在text时会根据你设定的字符集编码进行转换。
>>> response = requests.get(url,headers = headers)
>>> response.apparent_encoding
'GB2312'
>>> response.encoding = response.apparent_encoding
>>> data = response.text
这样就不会有中文乱码的问题了。
2)多线程碰到的问题1—csv文件中出现很多空值
下面是出现这个问题的区级信息获取的部分代码:
qurl = Queue() #队列
thread_num = 10 #进程数
#下面三个全局变量是每个区的代码、URL、名称信息
countyCode = []
countyURL = []
countyName = []
def produce_url(url_list):
for url in url_list:
qurl.put(url) # 生成URL存入队列,等待其他线程提取
def getCounty():
while not qurl.empty(): # 保证url遍历结束后能退出线程
url = qurl.get() # 从队列中获取URL
response = requests.get(url,headers = headers)
response.encoding = response.apparent_encoding
data = response.text
selector = etree.HTML(data)
countyList = selector.xpath('//tr[@class="countytr"]')
#下面是爬取每个区的代码、URL
for i in countyList:
countyCode.append(i.xpath('td[1]/a/text()'))
countyURL.append(i.xpath('td[1]/a/@href'))
countyName.append(i.xpath('td[2]/a/text()'))
def run(url_list):
produce_url(url_list)
ths = []
for _ in range(thread_num):
th = Thread(target = getCounty)
th.start()
ths.append(th)
for th in ths:
th.join()
run(cityURL_list)
这里有几个问题:
首先,下面这个解析代码获取的是一个列表:
i.xpath('td[1]/a/text()
而外面套着的append操作则使得列表嵌套列表(生成一个二维列表):
countyCode.append(i.xpath('td[1]/a/text()'))
同时又有三个全部变量来存储数据:
#下面三个全局变量是每个区的代码、URL、名称信息
countyCode = []
countyURL = []
countyName = []
在单线程下,这个操作是没有问题的,但是由于这里是多线程实现,多个线程同时运行,例如:线程1写入countyCode这个全局变量,然后在写入countyURL这个变量之前,线程2写入了它对应的countyURL,导致了线程1的countyCode对应了线程2的countyURL,这样整个列表对应顺序出现了错乱。
解决办法:使用字典封装每个区的三个信息,这部分实现如下代码所示。
county = [] #记录区级信息的字典(全局)
#下面是爬取每个区的代码、URL
for i in countyList:
countyCode = i.xpath('td[1]/a/text()')
countyLink = i.xpath('td[1]/a/@href')
countyName = i.xpath('td[2]/a/text()')
#上面得到的是列表形式的,下面将其每一个用字典存储
for j in range(len(countyLink)):
countyURL = url[:-9] + countyLink[j]
county.append({'code':countyCode[j],'link':countyURL,'name':countyName[j]})
这样在多线程操作时,每个线程写入的都是完整的一个区的信息,不会出现对应错误。
3)多线程碰到的问题2—信息顺序混乱
这是多线程无法避免的问题,不同线程写入时间存在先后关系。我这里先完成多线程信息采集,然后对这个“字典”列表进行排序,最后再写入csv文件。代码如下所示:
county = getCounty(df_city['link'])
df_county = pd.DataFrame(county)
df_county_sorted = df_county.sort_values(by = ['code']) #按1列进行升序排序
df_county_sorted.to_csv('county.csv', sep=',', header=True, index=False)
4)数据量过大,内存不足
我把整个爬虫放到1G内存的VPS上进行爬取,运行一段时间后就会被killed。我查看日志发现,这是内存不足导致的,OOM killer杀死了这个占用很大内存的非内核进程以防止内存耗尽影响系统运行。具体分析见:Linux下Python程序Killed,分析其原因。
最后我采取分段爬取居委会一级的数据,每次爬取5000个街道的URL,具体代码:
village = getVillage(df_town['link'][0:5000])
df_village = pd.DataFrame(village)
# 信息写入csv文件
df_village.to_csv('village-0.csv', sep=',', header=True, index=False)
最后将所有爬取得到的csv文件进行合并得到全部居委会信息。
国家统计局的统计用区划代码和城乡划分代码爬取—最终版
本部分实现参考了Python爬虫练习五:爬取 2017年统计用区划代码和城乡划分代码(附代码与全部数据)
完整的代码见:Urban-and-rural-statistics-spider.py。
第一版存在的问题
第一版的实现还存在一些问题:
- 多线程碰到的问题
- 因为没有模块化,代码结构比较混乱
具体的问题分析和解决见上面。
总体思路说明
首先我定义了一个网页爬取函数,然后依次定义省级代码获取函数、市级代码获取函数、区级代码获取函数、街道代码获取函数、居委会代码获取函数,这些函数都会调用网页爬取函数。其中区级代码获取函数、街道代码获取函数、居委会代码获取函数这三个函数都是多线程实现爬取的。最后我将爬取得到的数据输出为csv格式文件。
库函数导入
import requests
from lxml import etree
import csv
import time
import pandas as pd
from queue import Queue
from threading import Thread
from fake_useragent import UserAgent
网页爬取函数
# 下面加入了num_retries这个参数,经过测试网络正常一般最多retry一次就能获得结果
def getUrl(url,num_retries = 5):
ua = UserAgent()
headers = {'User-Agent':ua.random}
try:
response = requests.get(url,headers = headers)
response.encoding = response.apparent_encoding
data = response.text
return data
except Exception as e:
if num_retries > 0:
time.sleep(10)
print(url)
print("requests fail, retry!")
return getUrl(url,num_retries-1) #递归调用
else:
print("retry fail!")
print("error: %s" % e + " " + url)
return #返回空值,程序运行报错
获取省级代码函数
def getProvince(url):
province = []
data = getUrl(url)
selector = etree.HTML(data)
provinceList = selector.xpath('//tr[@class="provincetr"]')
for i in provinceList:
provinceName = i.xpath('td/a/text()') #这里如果采用//a/text()路径会出现问题!!
provinceLink = i.xpath('td/a/@href')
for j in range(len(provinceLink)):
provinceURL = url[:-10] + provinceLink[j] #根据获取到的每个省的链接进行补全,得到真实的URL。
province.append({'name':provinceName[j],'link':provinceURL})
return province
pro = getProvince("http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/index.html")
df_province = pd.DataFrame(pro)
信息写入csv文件
df_province.to_csv('province.csv', sep=',', header=True, index=False)
获取市级代码函数
def getCity(url_list):
city_all = []
for url in url_list:
data = getUrl(url)
selector = etree.HTML(data)
cityList = selector.xpath('//tr[@class="citytr"]')
#下面是抓取每一个城市的代码、URL
city = []
for i in cityList:
cityCode = i.xpath('td[1]/a/text()')
cityLink = i.xpath('td[1]/a/@href')
cityName = i.xpath('td[2]/a/text()')
for j in range(len(cityLink)):
cityURL = url[:-7] + cityLink[j]
city.append({'name':cityName[j],'code':cityCode[j],'link':cityURL})
city_all.extend(city) #所有省的城市信息合并在一起
return city_all
city = getCity(df_province['link'])
df_city = pd.DataFrame(city)
信息写入csv文件
df_city.to_csv('city.csv', sep=',', header=True, index=False)
获取区级代码函数—多线程实现
def getCounty(url_list):
queue_county = Queue() #队列
thread_num = 10 #进程数
county = [] #记录区级信息的字典(全局)
def produce_url(url_list):
for url in url_list:
queue_county.put(url) # 生成URL存入队列,等待其他线程提取
def getData():
while not queue_county.empty(): # 保证url遍历结束后能退出线程
url = queue_county.get() # 从队列中获取URL
data = getUrl(url)
selector = etree.HTML(data)
countyList = selector.xpath('//tr[@class="countytr"]')
#下面是爬取每个区的代码、URL
for i in countyList:
countyCode = i.xpath('td[1]/a/text()')
countyLink = i.xpath('td[1]/a/@href')
countyName = i.xpath('td[2]/a/text()')
#上面得到的是列表形式的,下面将其每一个用字典存储
for j in range(len(countyLink)):
countyURL = url[:-9] + countyLink[j]
county.append({'code':countyCode[j],'link':countyURL,'name':countyName[j]})
def run(url_list):
produce_url(url_list)
ths = []
for _ in range(thread_num):
th = Thread(target = getData)
th.start()
ths.append(th)
for th in ths:
th.join()
run(url_list)
return county
county = getCounty(df_city['link'])
df_county = pd.DataFrame(county)
排序
由于多线程的关系,数据的顺序已经被打乱,所以这里按照区代码进行“升序”排序。
df_county_sorted = df_county.sort_values(by = ['code']) #按1列进行升序排序
信息写入csv文件
df_county_sorted.to_csv('county.csv', sep=',', header=True, index=False)
获取街道代码函数—多线程实现
def getTown(url_list):
queue_town = Queue() #队列
thread_num = 50 #进程数
town = [] #记录街道信息的字典(全局)
def produce_url(url_list):
for url in url_list:
queue_town.put(url) # 生成URL存入队列,等待其他线程提取
def getData():
while not queue_town.empty(): # 保证url遍历结束后能退出线程
url = queue_town.get() # 从队列中获取URL
data = getUrl(url)
selector = etree.HTML(data)
townList = selector.xpath('//tr[@class="towntr"]')
#下面是爬取每个区的代码、URL
for i in townList:
townCode = i.xpath('td[1]/a/text()')
townLink = i.xpath('td[1]/a/@href')
townName = i.xpath('td[2]/a/text()')
#上面得到的是列表形式的,下面将其每一个用字典存储
for j in range(len(townLink)):
townURL = url[:-11] + townLink[j]
town.append({'code':townCode[j],'link':townURL,'name':townName[j]})
def run(url_list):
produce_url(url_list)
ths = []
for _ in range(thread_num):
th = Thread(target = getData)
th.start()
ths.append(th)
for th in ths:
th.join()
run(url_list)
return town
town = getTown(df_county['link'])
df_town = pd.DataFrame(town)
排序
由于多线程的关系,数据的顺序已经被打乱,所以这里按照街道代码进行“升序”排序。
df_town_sorted = df_town.sort_values(by = ['code']) #按1列进行升序排序
信息写入csv文件
df_town_sorted.to_csv('town.csv', sep=',', header=True, index=False)
获取居委会代码函数—多线程实现
def getVillage(url_list):
queue_village = Queue() #队列
thread_num = 200 #进程数
town = [] #记录街道信息的字典(全局)
def produce_url(url_list):
for url in url_list:
queue_village.put(url) # 生成URL存入队列,等待其他线程提取
def getData():
while not queue_village.empty(): # 保证url遍历结束后能退出线程
url = queue_village.get() # 从队列中获取URL
data = getUrl(url)
selector = etree.HTML(data)
villageList = selector.xpath('//tr[@class="villagetr"]')
#下面是爬取每个区的代码、URL
for i in villageList:
villageCode = i.xpath('td[1]/text()')
UrbanRuralCode = i.xpath('td[2]/text()')
villageName = i.xpath('td[3]/text()')
#上面得到的是列表形式的,下面将其每一个用字典存储
for j in range(len(villageCode)):
town.append({'code':villageCode[j],'UrbanRuralCode':UrbanRuralCode[j],'name':villageName[j]})
def run(url_list):
produce_url(url_list)
ths = []
for _ in range(thread_num):
th = Thread(target = getData)
th.start()
ths.append(th)
for th in ths:
th.join()
run(url_list)
return town
village = getVillage(df_town['link'])
df_village = pd.DataFrame(village)
# 排序:由于多线程的关系,数据的顺序已经被打乱,所以这里按照街道代码进行“升序”排序。
df_village_sorted = df_village.sort_values(by = ['code']) #按1列进行升序排序
# 信息写入csv文件
df_village_sorted.to_csv('village.csv', sep=',', header=True, index=False)
全部代码以及分析见GitHub:https://github.com/dta0502/NBSPRC-spider