【Python Onramp】8.Python爬虫(1)基于requests和BeautifulSoup的全国区划数据爬虫

系列文章目录

【Python Onramp】 0. 卷首语

上一篇:【Python Onramp】7. web端可视化:北京地铁数据统计分析实例以及简易Echarts绘图
下一篇:【Python Onramp】9.Python爬虫(2):selenium爬取京东商品信息和知乎热榜信息

项目描述

这个项目中,你将学习到爬虫的最基础的第三方库requests,并利用一些网页解析工具如BeautifulSoup或者PyQuery进行页面解析。

详情可见我的github仓库:https://github.com/Honour-Van/CS50/tree/master/WebSpider

进入国家统计局2020年统计用区划代码和城乡划分。以此为起点,自动爬取各省市自治区相应统计单位一直到最基层为止,以此为基础,对中国基层行政单位的名称进行统计分析。这样的分析此前已经存在,比如网易数读的文章《我们分析了67万个村名,找到了中国地名的秘密》。

数据获取

爬取后的输出格式如下图所示(没有到最基层,以此类推)。

每一级统计单位相对于上一级用\t缩进,最高一级为省市自治区。每个统计单位前为统计用区划代码。

最高一级的统计用区划代码为该区域前两位,后面依次补零。

最基层有“城乡分类代码”,将该代码放到名称之后,形如:“110112005001天赐良园社区居委会111”,其中的111为“城乡分类代码”。

将分层的文本数据储存到StatData.txt

数据统计与分析

task 1

“城乡分类代码”含义如下:“111表示主城区,112表示城乡结合区,121表示镇中心区,122表示镇乡结合区,123表示特殊区域,210表示乡中心区,220表示村庄”。分别统计各分类最基层统计单位数量格式如下:

在这里插入图片描述

省市名称顺序自定,间距自定或采用Tab。

task 2

分别针对“内蒙古自治区”和“河南省”含有“村委会”的最基层统计单位,统计去除“村委会”后,最常用字前100个,观察其异同,输出按字的频率又高到低顺序输出;

task 3

根据文后附属的姓氏排行,统计带有不同姓氏的地名数量。注意:仅统计第一个字,仅统计最低两个层次;输出按文后给出的姓氏顺序,格式为每行前一个字符串为姓氏,中间以Tab隔开,后面为该形式的地名数

010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100

将上次三个小题的数据输出到ComputingData.txt

要点总结

要点1:爬虫与requests

https://docs.python-requests.org/zh_CN/latest/

requests是一个通用的爬虫库,使用简单,如果网页没有特别强的反爬虫机制,就可以使用requests进行爬取。

我们所使用的爬虫的实质,就是将网页的源代码爬取出来,通过网页解析的方式,提取出其中按照一定的规则组织的有价值信息,从而实现信息获取的目的。

requests使用方法非常简单,比如为了爬取baidu的网页,我们可以使用如下代码,
在这里插入图片描述

作为初学,我们重点需要掌握爬虫的基本方法,所以先使用requests,而后的几节我们还要做selenium和scrapy的爬虫。

要点2:BeautifulSoup页面解析

先前说到,我们的实用爬虫的一般方法是将网页的源码提取出来,然后通过对其关键部分组织规则的反向解析,获取其中有价值的信息。

BeautifulSoup的核心思想仍然是基于CSS selector,具体使用方法在之前已经说到,详细可见https://www.w3school.com.cn/cssref/css_selectors.asp

BeautifulSoup的select方法使用如下:https://www.cnblogs.com/yizhenfeng168/p/6979339.html

按照CSS选择器的语法可以筛选出符合规则的一个列表,然后遍历进行处理即可。

要点3:异常处理

https://www.runoob.com/python/python-exceptions.html
https://www.liaoxuefeng.com/wiki/1016959663602400/1017598873256736

由于各个网站都有不同程度的反爬虫机制,所以偶尔的爬虫出错在所难免。但通过有效的错误处理机制,可以保证在牺牲一定时间和更多次尝试的基础上,最终完成所有的情形。

常用的处理结构为try except finally。分别对应主要程序块、错误处理块、和不中断过程库。

具体实现

文件结构

在代码仓库https://github.com/Honour-Van/CS50/tree/master/WebSpider我们实现了一个面向对象的程序,组件parco和主程序get_data

parco.py中给出一个函数,每一个链接的调用返回一个名称:链接的字典。然后在get_data中实现递归调用。

由于要输出,所以我们考虑把层数加进去。这样就可以有机地调整输出。

由于诸多原因这个目标并未实现。后来我们不再使用面向对象的结构,而直接换用面向过程的代码,如stat_spider.py

事后反思,这一点可能是因为不熟悉面向对象的调试而导致的。

我们接下来的叙述也基于stat_spider.py

文本内容和链接获取

<a href="11.html">北京市<br/></a>

的网页,不能直接从string属性当中获得标签对应的文本。

解决方法:删除不配对标签 https://blog.csdn.net/u012587107/article/details/80543977

或者使用get_text函数

得到省级的字典如下:

{'北京市': '11.html', '天津市': '12.html', '河北省': '13.html', '山西省': '14.html', '内蒙古自治区': '15.html', '辽宁省': '21.html', '吉林省': '22.html', '黑龙江省': '23.html', '上海市': '31.html', '江苏省': '32.html', '浙江省': '33.html', '安徽省': '34.html', '福建省': '35.html', '江西省': '36.html', '山东省': '37.html', '河南省': '41.html', '湖北省': '42.html', '湖南省': '43.html', '广东省': '44.html', '广西壮族自治区': '45.html', '海南省': '46.html', '重庆市': '50.html', '四川省': '51.html', '贵州省': '52.html', '云南省': '53.html', '西藏自治区': '54.html', '陕西省': '61.html', '甘肃省': '62.html', '青海省': '63.html', '宁夏回族自治区': '64.html', '新疆维吾尔自治区': '65.html'}

随机开始进行递归调用。

由于省级没有对应编码,所以可以考虑单独处理。

反爬?这里是HTML解析的问题

                130671000000保定高新技术产业开发区
                130672000000保定白沟新城
                130681000000涿州市
                130682000000定州市
                130683000000安国市
                130684000000高碑店市
        130700000000张家口市
        130800000000承德市
        130900000000沧州市
        131000000000廊坊市
        131100000000衡水市
140000000000山西省
150000000000内蒙古自治区
210000000000辽宁省
220000000000吉林省
230000000000黑龙江省
310000000000上海市
320000000000江苏省
330000000000浙江省

并不全是。似乎一个重要原因是后面的省市都换用了table结构。每一层当中的tr标签都具有相对鲜明的特征,这些使用CSS select非常容易选出。

html的结构对我们编写爬虫来说非常关键。

对于特定问题过程化的好处

过程化可以减少对象的传递,比如在网页当中,两个链接并排时,我们可以两个两个处理它们,而不用考虑怎样构建更好的数据结构的问题。而直接将对应的tr中的td取出来就好。

另外我们没有认识到find_all返回的resultset是一个列表结构,最初只进行迭代遍历,吃了不少亏。

item_href = item_td_code.get("href")
item_code = item_td_code.get_text()
item_name = item_td_name.get_text()

比如后来我们采用了如上的语句即可,由于每一条有效数据都对应着一个tr,编码和名称分别对应两个td,故而我们可以非常容易地筛选出来。

异常处理:解决反爬

def get_html(url):
    """
    # get_html
    @Description:
    get html code by url 
    based on exception handling and anti-spider design 
    to realize totally data mining
    ---------
    @Param:
    a url str
    -------
    @Returns:
    a Beautifulsoup object
    GBK encoding and html.parser as resolver.
    -------
    """
    while True:
        try:
            response = requests.get(url, timeout=1) # set appropriate timeout to reduce the possibilities of anti-spider
            response.encoding = "GBK"
            if response.status_code == 200: # if access successfully
                return BeautifulSoup(response.text, "html.parser")
            else:
                continue
        except Exception:
            # if we add a print here, we could see that it actually fail somtimes
            continue

我们在输出中发现了诸如连续的1。后来确定,达到timeout之后确实会触发异常。

如果这时候我们在异常处理分支添加一个输出,那么我们将会发现超时是不时发生的,这时候我们重新开始爬取即可。进入while循环

计时分析

由于数据量极大,我们添加了log简单对比了几个平台之下的速度(1分钟):

powershell:2959
vscode:2187
wsl linux:2648
cmd: 3126

在这里插入图片描述

统计分析

共679237行

通过一次性遍历,把每个省的数据统计出来:

task 1

(利用缩进判断)
如果是一个省/直辖市:
	计数开始
否则:
	遍历基层并计数

统一显示:
	利用dataframe存储之后输出是比较方便的

注意,基层不一定是五层。我们应当通过最后三位数进行判断是否为基层。

了解:

在这里插入图片描述

在这里插入图片描述

task 2

如果是河南或者内蒙古:
	判断基层:
		如果是村委会:
			统计字频
去掉“村”“委”“会”
显示

利用dict进行统计

task 3

统计一个词典的词频:https://ask.csdn.net/questions/761132

读入词语
遍历词语中的单字:
	如果在要找的百家姓中:
		计数
	否则

词典的key可以作为是否查询的判据,用于判断一个词典是否为空,以及一个变量是否作为key出现在这个dict中。

由于需要判断倒数第二深层,其中部分地方只有四层,所以这个判断的行为也并不是统一的。这里我们考虑,基层的父层是倒数第二层。所以先将上一层的名称保存下来,如果进入了基层区域,那么上一条记录就必然是倒数第二层。

完整代码

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@file:stat_spider.py
@author: Honour-Van: fhn037@126.com
@date:2021/04/21 22:26:04
@description: get data from the website with friendly Chinese UI
'''

import requests
from bs4 import BeautifulSoup
from datetime import datetime

def format_time(cur_time: datetime, is_delta=True):
    """
    # format_time
    @Description:
    transform the datetime.datetime into assigned long format
    ---------
    @Param:
    cur_time: a datetime object, with full precision
    is_delta: a timedelta object, with full precision with less length
    -------
    @Returns:
    a str that with the form like yyyy-mm-dd hh:mm:ss
                        or h:mm:ss if object is timedelta
    -------
    """
    return str(cur_time)[:19] if is_delta else str(cur_time)[:7]


def get_html(url):
    """
    # get_html
    @Description:
    get html code by url 
    based on exception handling and anti-spider design 
    to realize totally data mining
    ---------
    @Param:
    a url str
    -------
    @Returns:
    a Beautifulsoup object
    GBK encoding and html.parser as resolver.
    -------
    """
    while True:
        try:
            response = requests.get(url, timeout=1) # set appropriate timeout to reduce the possibilities of anti-
            response.encoding = "GBK"
            if response.status_code == 200: # if access successfully
                return BeautifulSoup(response.text, "html.parser")
            else:
                continue
        except Exception:
            continue


def get_prefix(url):
    """
    @Description:
    get the prefix of a url
    to form the sub-level url
    ---------
    @Param:
    url:str
    -------
    @Returns:
    a substr end where '/' last time appears
    -------
    """
    return url[0:url.rfind("/") + 1]


indent = ""

# 递归抓取下一页面


def spider_next(url, lev):
    """
    # spider_next
    @Description:
    core function of spider
    with recursive structure to traverse all the nodes in it 
    ---------
    @Param:
    url: str
    lev: recursive level
    -------
    @Returns:
    recursion, void type
    -------
    """
    # choose spider_class in order to select specific table elements in specific page
    if lev == 2:
        spider_class = "city"
    elif lev == 3:
        spider_class = "county"
    elif lev == 4:
        spider_class = "town"
    else:
        spider_class = "village"
    # indent is used to format the output 
    global indent
    indent += "\t"
    has_cur_lev = 0 # becaution to that dongguan has only four levels, we use this to gap the missing lev
    for item in get_html(url).select("tr." + spider_class + "tr"): # select the assigned table row data
        item_td = item.select("td")
        item_td_code = item_td[0].select_one("a")
        item_td_name = item_td[1].select_one("a")
        if item_td_code is None: # some td has no link with it
            item_href = None
            item_code = item_td[0].get_text() # it can get the text even it has an enter symbol following it 
            item_name = item_td[1].get_text()
            if lev == 5:
                item_name = item_td[2].get_text() + item_td[1].get_text() 
                # the most childist ones has different output format with a identification code
        else:
            item_href = item_td_code.get("href")
            item_code = item_td_code.get_text()
            item_name = item_td_name.get_text()
        content2 = indent
        content2 += item_code + item_name
        has_cur_lev = 1
        print(content2, file=wFile)

        tcs = datetime.now() #time count
        if lev == 2 or lev == 3:
            print("["+format_time(tcs)+"] " + '*' *
                  (lev-1) + item_name + "开始爬取...")

        if item_href is not None: # recursion
            spider_next(get_prefix(url) + item_href, lev + 1)

        tce = datetime.now()
        if lev == 2 or lev == 3:
            print("["+format_time(tce)+"] " + '*' * (lev-1) +
                  item_name + "爬取完成,用时" + format_time((tce-tcs), False))
        if lev == 2:
            print("--------------------------------------------------------")
    indent = indent[:-1]
    if has_cur_lev is not True and lev != 5: # deal with those ones without full 5 levels, directly deep in
        spider_next(url, lev + 1)


if __name__ == '__main__':
    province_url = "http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2020/index.html"
    province_list = get_html(province_url).select('tr.provincetr a') # get the province list
    wFile = open("StatData.txt", "w", encoding="utf-8")
    start_time = datetime.now()
    str(start_time)[:19]
    try:
        for province in province_list: # traverse the province list, 
            # in fact it can be inserted into the recursion structure
            href = province.get("href")
            province_code = href[0:2].ljust(12, '0')
            province_name = province.get_text()
            content = province_code + province_name
            print(content, file=wFile)

            tps = datetime.now()
            print("["+format_time(tps)+"] "+province_name+"开始爬取...")

            spider_next(get_prefix(province_url) + href, 2) # start a province's spider

            tpe = datetime.now()
            print("["+format_time(tps)+"] "+province_name +
                  "爬取完成,用时" + format_time((tpe-tps), False))
            print("========================================================")
            
        print("总用时:" + format_time((datetime.now()-start_time), False))
    finally:
        wFile.close()

总结

一个思想:错误处理对于爬虫来说非常重要。

几个要点

  • requests爬虫的使用
  • 利用BeautifulSoup对网页进行解析
  • Python错误处理的使用
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
统计用区划代码城乡划分代码发布说明: 一、编制依据 2008年7月,国务院批复同意国家统计局与民政部、住建部、公安部、财政部、国土部、农业部共同制定的《关于统计上划分城乡的规定》(国函〔2008〕60号),自2008年8月1日实施,正式奠定了统计上划分城乡的理论依据和方法基础。随后,国家统计局印发《统计用区划代码城乡划分代码编制规则》(国统字〔2009〕91号)。 二、区划范围 统计用区划代码城乡划分代码区划范围,是国家统计局开展统计调查的区划范围。未包括我国台湾省、香港特别行政区、澳门特别行政区。 三、发布内容 12位统计用区划代码和3位城乡分类代码。 四、适用领域 《国务院关于统计上划分城乡规定的批复》(国函〔2008〕60号)明确指出:“本规定作为统计上划分城乡的依据,不改变现有的行政区划、隶属关系、管理权限和机构编制,以及土地规划、城乡规划等有关规定”。各级各部门在使用统计用区划代码城乡划分代码时,请务必结合实际情况。 五、几个具体问题的说明 (一)补充编制开发区统计汇总识别码情况。为满足统计调查工作组织和数据汇总的需要,国家统计局对一些符合条件的开发区编制了统计汇总识别码。统计汇总识别码在统计用区划代码的县级码段上编制,其码段为71~80。 (二)关于河北省沧州市任丘市的苟各庄镇、鄚州镇、七间房乡、保定市高阳县的龙化乡统计用区划代码临时调整情况的说明。按照河北省委、省政府关于对雄安新区周边部分区域实施托管的通知要求,沧州市任丘市的苟各庄镇、鄚州镇、七间房乡划归雄县实施统计上托管,保定市高阳县的龙化乡划归安新县实施统计上托管。为确保统计调查工作的顺利开展, 国家统计局对苟各庄镇、鄚州镇、七间房乡、龙化乡的统计用十二位区划代码进行了临时调整,具体调整为:鄚州镇代码由130982104000变更为130638106000;苟各庄镇代码由130982105000变更为130638107000;七间房乡代码由130982206000变更为130638205000;龙化乡代码由130628204000变更为130632203000。上述变更后的统计用区划代码为临时代码,待民政部门对雄安新区上述4个乡镇区划调整确认后,再将临时代码变更为正式统计用区划代码。 (三)关于黑龙江省大兴安岭地区县级单位统计用区划代码调整情况说明。民政部民函〔2018〕50号文件撤销黑龙江省大兴安岭地区漠河县(六位区划代码为232723),设立漠河市(六位区划代码为232701)。为执行国家标准,保证统计部门与民政部门名称相同的县级单位六位区划代码的一致性,国家统计局根据《统计用区划代码城乡划分代码编制规则》(国统字〔2009〕91号),调整黑龙江省大兴安岭地区所辖的加格达奇区、松岭区、新林区和呼中区的六位统计用区划代码,具体调整为:加格达奇区代码由232701变更为232761;松岭区代码由232702变更为232762;新林区代码由232703变更为232763;呼中区代码由232704变更为232764。 (四)此版本区划代码与第四次全国经济普查区划代码的相关说明。此版本区划代码是调查截止日期为2018年10月31日的统计用区划代码。由于第四次全国经济普查清查工作于2018年8月开始,四经普的清查和登记工作中采用2018年6月15日的统计用区划代码。第四次全国经济普查数据处理使用2018年10月31日的统计用区划代码
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值