系列文章目录
上一篇:【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隔开,后面为该形式的地名数
01李 02王 03张 04刘 05陈 06杨 07赵 08黄 09周 10吴
11徐 12孙 13胡 14朱 15高 16林 17何 18郭 19马 20罗
21梁 22宋 23郑 24谢 25韩 26唐 27冯 28于 29董 30萧
31程 32曹 33袁 34邓 35许 36傅 37沈 38曾 39彭 40吕
41苏 42卢 43蒋 44蔡 45贾 46丁 47魏 48薛 49叶 50阎
51余 52潘 53杜 54戴 55夏 56钟 57汪 58田 59任 60姜
61范 62方 63石 64姚 65谭 66廖 67邹 68熊 69金 70陆
71郝 72孔 73白 74崔 75康 76毛 77邱 78秦 79江 80史
81顾 82侯 83邵 84孟 85龙 86万 87段 88漕 89钱 90汤
91尹 92黎 93易 94常 95武 96乔 97贺 98赖 99龚 100文
将上次三个小题的数据输出到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存储之后输出是比较方便的
注意,基层不一定是五层。我们应当通过最后三位数进行判断是否为基层。
了解:
- dataframe的创建:https://blog.csdn.net/qq_42067550/article/details/106148799
- dataframe基于dict添加:https://blog.csdn.net/ningyanggege/article/details/93331542
- dataframe.append(pd.Series):https://blog.csdn.net/sinat_29957455/article/details/84961936(使用这个方法才能重新命名index)
- dataframe输出:https://blog.csdn.net/qq_27133869/article/details/103709805
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错误处理的使用