目录
前言
在之前我们就说过了,爬虫爬下来的只是散乱的数据,还需要进一步的清洗和整合。Scrapy做到了清洗和整合数据,而Seaborn(一个用于数据可视化的包)可以将数据以更加友善可读的方式展现在受众面前。
目标
百度贴吧。准确地来说是百度贴吧某关键词搜索下的所有帖子的楼主的信息,包括:{用户名、性别、吧龄};和帖子名与帖子所属的贴吧。最后,我们会将这些数据用Seaborn可视化地展现出来。
编写Item类
class TiebaItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
author=scrapy.Field() #作者名
appname=scrapy.Field() #贴吧名
sex=scrapy.Field() #作者性别
year=scrapy.Field() #作者吧龄
pname=scrapy.Field() #帖子名
测试爬取目标
由于上一节已经介绍过,我们省略重复的部分,直接开始测试目标。随机选择了关键词“龙族”,我们以此为例开始目标测试。
我这里的搜索链接为:百度贴吧_龙族(Im 一朵罂粟花^ ^)(因为要只看主题帖,所以与直接搜索的链接不同)
1 搜索结果-帖子
css选择器选择p_title类,提取到的数据是列表形式,需要拼合。并且需要去掉列表的第一个内容,因为这是搜到的人。(可以把链接中的参数pn改成1,这样就没有人出现,这是我在测试中的意外发现)
需要提取的数据在::text中,是帖子的帖子名。
至于搜索页的下一页,其实可以通过更改搜索链接中的pn参数获取。
2 搜索结果-楼主和贴吧
对于楼主信息和贴吧信息的分别提取就比较困难,因为它们在几乎相同位置,并且周围有很多标签名和类名也相同。
但是,我们可以用:nth-child()来选择固定顺序的子标签。选择类名s_post,在其结果下选择子标签以及伪类font:nth-child(1),再获取其链接与内容,如下:
3 搜索结果-楼主信息
对于楼主的性别,只需要查看是否有类userinfo_sex_male/userinfo_sex_female,它们分别对应男/女性别的伪类;对于楼主的吧龄,可以提取类userinfo_userdata span:nth-child(2)。
4 屏蔽处理
有些帖子的楼主是被贴吧封禁的,这时候直接获取楼主信息往往会产生空段,如果加上了进一步的处理操作就会产生报错。为了避免这一情况,我们设置响应在css类筛选到page404(也就是贴吧提示用户不存在页的标签类)时给item赋一个约定好的值然后直接退出。
当然,贴吧在对屏蔽用户的返回中加入了一个重定向(302),也可以通过判断是否重定向来处理。
编写spider
这里我就直接上代码了,有疑问请看前面几节。
class JiayunSpider(scrapy.Spider):
name = 'jiayun'
allowed_domains = ['tieba.baidu.com']
keyname="%C1%FA%D7%E5" #爬取的关键词
start_urls = ['https://tieba.baidu.com/f/search/res?isnew=1&kw=&qw='+keyname+'&un=&rn=10&pn=1&sd=&ed=&sm=1&only_thread=1']
itemlist=[]
index=0 #游标
priority=10000 #优先级
count=-1
def parse(self, response):
pnamelist=response.css(".p_title")
authorlist=response.css(".s_post") #这两个长度应该是相同的
for i in range(len(pnamelist)):
item=TiebaItem()
item['pname']=''.join(pnamelist[i].css("::text").extract())
item['appname']=authorlist[i].css("font::text").extract()[0]
item['author']=authorlist[i].css("font::text").extract()[1]
self.count+=1
self.itemlist.append(item)
self.priority-=1
yield Request(response.urljoin("https://tieba.baidu.com/home/main?un="+item['author']),callback=self.author_parse,dont_filter=True,priority=self.priority,meta={"count":self.count})
next=response.css(".pager .next::attr(href)").extract()
if(next!=[]):
self.priority-=1
yield Request(response.urljoin(next[0]),callback=self.parse,priority=self.priority)
def author_parse(self,response): #使用meta传参,同步处理
if(response.css(".page404")!=[]): #楼主被屏蔽时的处理
self.itemlist[response.meta['count']]['sex']=""
self.itemlist[response.meta['count']]['year']=""
else:
if(response.css(".userinfo_sex_male")!=[]):
self.itemlist[response.meta['count']]['sex']="male"
else:
self.itemlist[response.meta['count']]['sex']="female"
self.itemlist[response.meta['count']]['year']=re.findall(r"\d+\.?\d*",response.css(".userinfo_userdata span:nth-child(2)::text").extract()[0])[0]
yield self.itemlist[response.meta['count']]
print("提交第",self.index+1,"个item。")
self.index+=1
# 令爬虫按照请求提交顺序执行
之前我们说过,scrapy是基于twisted编写的,也就是说它是异步的。所以在一些情况下,你提交的请求并不会依照提交顺序返回。这种情况下,如果我们的数据分散在多个解析器中提取,又要保持互相之间的相关性,那么直接按照返回顺序提交item显然是不行的。
为了解决这个问题,scrapy给request引入了一个可选参数priority,它是一个可以为负的数字,越高此请求的优先级越高。
但是,请求优先执行并不意味着其解析器也能优先执行完毕(这一点请读者牢记)。
为了解决解析器顺序的问题,可以通过request的meta参数传入字典来在不同的请求之间传值,在解析器中使用response.meta来读取传递的值。
编写pipeline
我们将数据存储为一个csv文件,以便seaborn读取。
1 什么是csv文件?
在查找资料中发现很多人孜孜以求地将这个文件类别描述的无比复杂,让人看了十分头大。事实上,将一个名词描述的无比复杂除了让读者看了厌烦之外起不到任何表现出作者的博学的作用(这句话送给某些作者,中文互联网垃圾数据满天飞有你们一半功劳,另一半是万事只会转载复制的copy侠)。
在我眼里,csv文件的定义可以如此描述:
- 1.csv文件存储数据,是纯文本文件。csv文件中每一条数据被称为“记录”。
- 2.csv文件中所有的记录都有同样的字段序列。请想象一个excel表格,它的每一条记录都是相同顺序的,并且在单元格为空时也需要表现出来而不能省略。
- 3.对csv文件的每一条记录而言,他们中的数据成员被分隔符(最常用的分隔符为,)分割开来,这些数据成员被称为“字段”。
2 如何编写csv文件?
我们使用python的csv包编写一个基础的csv文件。
我们用csv.writer(文件对象)来获取一个csv文件对象,调用其writerow(写入记录)方法来想csv文件中写入一条记录。
3 pipeline
class TiebaPipeline:
def __init__(self):
f=open("data.csv","w",encoding="utf-8")
self.csv=csv.writer(f) #创建数据集文件写对象
self.csv.writerow(["帖子名","作者","贴吧名","作者性别","作者吧龄"])
def process_item(self, item, spider):
self.csv.writerow([str(item['pname']),
str(item['author']),
str(item['appname']),
str(item['sex']),
item['year']
])
使用seaborn生成图表
*关于这部分我不会详细的讲解,毕竟要是详解涉及到的可就不止这一节了。
首先我们使用pandas读取csv数据,并且给matplotlib设置一个中文字体:
import pandas
import seaborn
import matplotlib.pyplot as pyplot
import matplotlib
font = {'family': 'MicroSoft Yahei',
'weight': 'bold',
'size': 12}
matplotlib.rc("font", **font)
data=pandas.read_csv("data.csv",na_values=[""])
过滤发帖过少的贴吧和作者,让数据更有参考性:
data=data.groupby(['贴吧名']).filter(lambda x:len(x)>=2)
data=data.groupby(['作者']).filter(lambda x:len(x)>=2)
绘制计数图:
seaborn.countplot(x=data['贴吧名']) #绘制计数图 【贴吧名】
seaborn.countplot(x=data['作者']) #绘制计数图 【作者】
绘制散点图:
seaborn.scatterplot(x=data['贴吧名'],y=data['作者']) #绘制散点图 【贴吧名/作者名】
最后显示生成的计数图,代码和图如下所示:
plot=seaborn.countplot(y=data['作者'],hue=data['作者性别']) #绘制计数图 【作者】
plot.set_title("关键词龙族在贴吧中的讨论热度与作者性别的关系")