上篇中写了Python爬虫的基础知识,并将我的所有博客文章标题和阅读量爬下来放进了文件中。详情点击:简单爬虫
这篇中将上个程序进阶一下,在多线程下,将所有的文章标题和阅读量对应着爬下来,并生成柱状图。再将函数封装到类中,通过指定用户名就可以爬取该用户的所有文章阅读量。
进阶详情
上一篇中的阅读量是在博客首页爬取的。但是经过我实际操作后发现,将首页的文章标题爬下来之后,会出现一个奇怪的标题叫:“帝都的凛冬”,这篇我不知道是从哪儿来的,也不是广告,也不是我写的文章,页面上也找不到它。所以这次我们去每一篇的内容中爬取标题和阅读数,即:
这次我们先在首页上将所有文章的链接取下来,放到一个列表中,再遍历列表,爬取每个网页中的标题和阅读数。
因为上一篇是从首页爬的,只有7个页面。但是这篇中我们要从博客的内容中爬取数据,所以有多少篇博客就要下载多少页面,这时串行下载就非常慢了。在这里我们就用最简单的多线程功能,通过多线程可以实现并行下载,大大缩短下载时间。
上一篇中爬去的数据存放到了文件中,非常难看,这篇中我们将数据进行可视化处理,放到一个柱状图中。
上一篇中的程序,只能爬我的文章,所以这里我们将程序封装到类中。通过输入用户名就可以爬取该用户的文章。
多线程
刚才说了,要从我的个人博客的首页将所有文章的链接爬取下来,放到一个列表中,这部分非常简单,只要能看懂我上一篇的简单爬虫,换两个正则表达式就可以实现,这里就不赘述了。假设链接已经爬取下来放到了列表中。
多线程就是开启多个线程,每个线程执行一个函数,它们之间是并行的。所以我们就将download函数扩充一下,使一个download函数可以取出列表中的一个链接,并将页面中的标题和阅读量爬取下来。这样可以实现并行爬取数据。
最大的问题就是,如何让一个线程知道它应该去下载列表中的哪个链接。如果不做什么措施,那么有可能出现一个链接被多个线程下载的情况。所以我们就可以使用列表的下标,每个线程给一个下标,这样他们就不会互相冲突了。这部分代码如下:
threads = []
while self.pos<self.link_set.__len__():
while len(threads) < 20 :
thread = threading.Thread(target=self.download, args=[self.pos])
thread.setDaemon(True)
threads.append(thread)
self.pos += 1
thread.start()
for thread in threads:
thread.join()
for thread in threads:
if not thread.is_alive():
threads.remove(thread)
用pos来代表当前应该下载的连接在列表中的下标,threads列表用于保存每个子线程,最大线程数为20。Thread函数用于创建一个线程,target用与指定去执行download函数,args用与给该线程的download函数传递一个参数self.pos。
setDaemon函数用于设置守护线程,即主线程结束后子线程就跟着结束。创建好之后将该线程加入到列表中,然后将pos下标加一,并运行该线程,子线程执行完函数后就被销毁了。
join函数的作用就是,20个线程内是并行的,20个与20个之间是串行的。即执行完这20个之后,才能执行下一波20个。is_alive函数用于判断该线程时候还存活。
download函数代码如下:
def download(self, pos=None, num_retries=3,user_agent="wswp"):
headers={"User-Agent":user_agent}
html=""
for j in range(num_retries):
try:
resp=requests.get(self.link_set[pos],headers=headers)
if resp.status_code==200:
html=resp.text
except Exception as e:
break
list=[]
try:
title = self.get_title(html)
good = self.get_good(html)
dic = {"title": title, "read": good,}
list.append(dic)
print(dic["title"]+" "+dic["read"])
with open(self.name+"_title.txt","a+",encoding="utf-8") as file:
file.write(dic["title"]+" "+dic["read"]+"\n")
except Exception as e:
print(e)
首先加入了user_Agent用户代理。是HTTP请求头的一部分,使服务器可以识别用户的操作系统版本、浏览器等各种信息。有的页面为了反爬虫,会对Python默认的用户代理进行封禁,所以这时我们就可以通过指定user_agent来避免被封禁用户代理。
然后加入了从页面中获取标题和阅读量的函数调用。再将其写入文件中。
可视化
Python中的第三方库pyecharts可以进行绘制各种图表。在这里我们选取柱状图来将我博客中阅读量和文章标题对应的显示出来。这里我只介绍最简单的用法。
在pyecharts中的柱状图默认y轴显示数字,x轴显示文字标签。所以我们先将每篇博客的访问量放到一个list中,再将每篇博客的标题放到另一个list中,然后做以下调用:
bar = Bar()
bar.add_xaxis(title_data)
bar.add_yaxis("访问量大于" + str(self.num) + "的博客", read_data)
bar.reversal_axis()
bar.render(self.name + ".html")
其中title_data表示文章标题,read_data表示阅读量。由于文章标题太长,X轴可能放不下,所以我将X轴和Y轴调换了一下,使其不会太拥挤。实现效果如下:
面向对象
主要是对函数进行封装。每个用户的文章内容网页链接的格式非常类似,我们只需修改其中的用户名即可,还可以设置只显示阅读量大于num的文章,程序源代码如下:
import re
import requests
import threading
from pyecharts.charts import Bar
class spider:
list = []
link_set = set()
pos=0
def __init__(self,name,num):
self.name=name
self.num=num
def get_links2(self,html):
# 将正则表达式编译之后可以使用findall函数 #
web_regex=re.compile(r"<span class=\"num\">[0-9]*</span>")
return web_regex.findall(html)
def get_num2(self,list):
num_regex=re.compile(r"[0-9]+")
s=""
for i in list:
s+=str(i)
return num_regex.findall(s)
def get_links(self,html):
# 将正则表达式编译之后可以使用findall函数 #
web_regex=re.compile(r"<a +href=[\'\"](.*?)[\'\"]")
return web_regex.findall(html)
def link_cwrael(self,start_url):
html=requests.get(start_url).text
link_regex = "https://blog.csdn.net/+"+self.name+"/article/details/"
for list in self.get_links(html):
if list and re.match(link_regex,list):
self.link_set.add(list)
def download(self, pos=None, num_retries=3,user_agent="wswp",proxies=None):
headers={"User-Agent":user_agent}
html=""
for j in range(num_retries):
try:
resp=requests.get(self.link_set[pos],headers=headers,proxies=proxies)
if resp.status_code==200:
html=resp.text
if 500<=resp.status_code<=600:
print(f"retry for {j+2} time")
except Exception as e:
break
list=[]
try:
title = self.get_title(html)
good = self.get_good(html)
dic = {"title": title, "read": good,}
list.append(dic)
print(dic["title"]+" "+dic["read"])
with open(self.name+"_title.txt","a+",encoding="utf-8") as file:
file.write(dic["title"]+" "+dic["read"]+"\n")
except Exception as e:
print(e)
def get_good(self,html):
web_regex = re.compile(r"<span class=\"read-count\">阅读数 .*</span>")
num=web_regex.findall(html)
num=num[0]
pos=num.find("阅读数")
pos2=num.find("</span>")
str=num[pos+3:pos2]
return str
def get_title2(self,i):
web_regex = re.compile(r">.*<")
title=web_regex.findall(i)
str=title[0]
str=str[1:-1]
return str
def get_title(self,html):
web_regex = re.compile(r"<h1 class=\"title-article\">.*</h1>")
title_list=web_regex.findall(html)
title=title_list[0]
return self.get_title2(title)
def start(self):
url = "https://blog.csdn.net/"
url2= "/article/list/"
for i in range(1, 8):
self.link_cwrael(url +self.name+url2+ str(i) + "?")
self.link_set=list(self.link_set)
threads = []
while self.pos<self.link_set.__len__():
while len(threads) < 20 :
thread = threading.Thread(target=self.download, args=[self.pos])
thread.setDaemon(True)
threads.append(thread)
self.pos += 1
thread.start()
for thread in threads:
thread.join()
for thread in threads:
if not thread.is_alive():
threads.remove(thread)
self.table()
def table(self):
file = open(self.name + "_title.txt", "r", encoding="utf-8")
title = []
read = []
list = file.readlines()
for i in list:
pos = None
end = i.__len__() - 1
while end >= 0:
if (i[end] == " "):
pos = end
break
end -= 1
pos2 = None
while end >= 0:
if (i[end] != " "):
pos2 = end + 1
break
end -= 1
title_str = i[:pos2]
j = title_str.__len__()
j = int(j / 2)
title_str = title_str[:j] + "\n" + title_str[j:]
title.append(title_str)
read_str = i[pos:]
read_str = read_str.strip()
read.append(int(read_str))
read_data = []
title_data = []
j = 0
for i in read:
if i > self.num:
read_data.append(i)
title_data.append(title[j])
j += 1
bar = Bar()
bar.add_xaxis(title_data)
bar.add_yaxis("访问量大于" + str(self.num) + "的博客", read_data)
bar.reversal_axis()
bar.render(self.name + ".html")
sp=spider("Q_M_X_D_D_",300)
sp.start()
GitHub地址:https://github.com/QMXDD/Crawler