朝小闇的博客

海上月是天上月,眼前人是心上人

爬虫之Scrapy(1.简要入门)

1.安装scrapy

  • 在cmd下一条语句即可完成安装;
1
pip install scrapy

据说在安装过程中可能遇到各种问题,由于我自己并没有遇到,所以略过,出现问题请自行百度

  • 直接在命令行输入scrapy可以查看相关指令:

image-20200909084009265

  • 输入scrapy bench用来验证安装是否成功,出现error则未成功,请自行百度解决方法;

2.scrapy原理

  • 直接上图:

image-20200910212314248

  • 讲解一下整个项目运行的过程,看完整篇博客再回来这里更有收获哟~
    • Spiders文件即为我们主要编写逻辑代码的区域,也就是爬虫文件;
    • 最初start_urls网址请求(request)被发送给引擎(Scrapy Engine);
    • 引擎通过判断这是一段网址请求而又将网址请求发送给调度器(Scheduler);
    • 调度器将网址请求进行入队操作(可能有很多网址)排好队再重新发回引擎;
    • 引擎将入队之后再出队的网址请求发送给下载器(Downloader);
    • 下载器直接从Inernet中下载请求网址对应的网页源代码,即HTML代码,这段代码被称为响应(response),没错就是后续代码中使用的response,下载器将响应返回给引擎;
    • 引擎再将响应返回给爬虫文件(Spiders);
    • 爬虫文件经过逻辑处理之后,再次发送请求给引擎,但这段请求可能是网址,如果是网址请求则再次重复上述过程,但也可能是数据,如果是数据,则引擎在接受到数据之后则将其转发给管道文件(Pipeline);
    • 最终由管道文件来确定是否将数据输出并且输出成何种类型的文件,也就是我们后面讲到的pipelines.py中的逻辑操作;

3.创建项目(直接上手实例)

  • 在cmd中进入文件所在位置,再执行指令用于创建项目:
1
2
3
4
# scrapy startproject 项目名	项目名一般由爬取网站的网站域名去除后缀再加上Spider单词组成

# 下面是我的实际执行命令,爬取网站为 http://www.itcast.cn/
scrapy startproject spiderStudy

4.创建爬虫

  • 进入新建的项目目录,执行命令用于创建爬虫文件:
1
2
3
4
5
# scrapy genspider 爬虫文件 网站域名
# 爬虫文件不要和项目名相同,规范是直接使用项目名去除Spider之后的单词,而网站域名则是爬虫采集的域名,即只有在此域名之下的网址才能被采集到,可以在文件中修改

# 实际执行命令
scrapy genspider itcast itcast.cn

5.项目结构

  • 如图所示:

image-20200909092153091

  • 目录结构:
    • spiders.data5u.py:爬虫文件,主要编写逻辑区域;
    • _ init_.py:初始化文件,一般不需要修改;
    • items.py:定义数据结构体;
    • middlewares:中间件文件;
    • pipelines.py:管道文件,从该文件中将数据传输保存为json、xls等文件;
    • settings.py:配置文件;

6.爬虫开始

6.1 解析爬虫类

1
2
3
4
5
6
7
8
# 创建爬虫类 继承自scrapy.Spider 也就是原理中的Spider爬虫文件类,是一个基础类
class ItcastSpider(scrapy.Spider):
name = 'itcast' #爬虫名
allowed_domains = ['itcast.cn'] #允许爬取的域名范围
start_urls = ['http://www.itcast.cn/channel/teacher.shtml'] #最初请求url的地址

def parse(self, response):
# 用来处理start_urls地址对应的请求响应

6.2 Selectors选择器

  • 是scrapy提取数据的一套机制,通过特定的XPath或者CSS表达式来选去HTML中的某个部分;
  • Selector四个基本方法:
    • 正则表达式:暂不涉及;
    • xpath():传入xpath()表达式,返回该表达式所对应的所有节点的selector list列表;
    • css():传入css表达式,返回该表达式所对应的所有节点的selector list列表;
    • extract():序列化该节点unicode字符串并返回list(),与get()使用效果相似;
  • 一般使用正则表达式和xpath即可,xpath请自行百度菜鸟或w3;

6.3 response.xpath

  • response是返回的响应,使用xpath对返回的数据进行选择:
1
2
3
4
def parse(self, response):
# 用来处理start_urls地址对应的请求响应
retName = response.xpath('//*[@class="main_bot"]/h2/text()')
print(retName)
  • xpath是如何选择的呢:

    • 进入到start_urls网页(这本就是parse函数最初得到的相应数据),具体请自行学习xpath:

    image-20200910201738870

  • 可以在cmd进入该项目目录中执行,也可直接在PyCharm Terminal执行:
1
2
#scrapy crawl 爬虫名
scrapy crawl itcast
  • 得到结果:

image-20200910195843555

6.4 LOG_LEVEL日志信息

  • 从上图中看见很多额外的信息,在settings.py文件中添加以下代码过滤日志信息:
1
2
3
4
LOG_LEVEL = 'WARNING'
# 该语句含义为只显示警告及以上级别(ERROR)的日志信息
# 日志信息有四个等级,从低到高分别为 DEBUG->INFO->WARNING->ERROR
# 对应含义为 调试信息->普通信息->警告->错误
  • 重新执行上述代码可以看到已经只剩下输出信息:

image-20200910200554769

6.5 extract()

  • 根据6.3中使用的语句/text()应该返回的结果是具体的文本内容,也就是上图中的data数据部分,但是却发现不经处理直接返回的数据是整个<Selector>列表,此时应该使用 extract()函数处理得到具体数据的列表值:
1
2
3
4
def parse(self, response):
# 用来处理start_urls地址对应的请求响应
retNames = response.xpath('//*[@class="main_bot"]/h2/text()').extract()
print(retName)
  • 再次执行就可以得到想要的信息啦:

image-20200910201223556

6.6 分组处理数据

  • 刚刚我们获得的数据是在具体节点下所有的h2文本数据列表,但如果我们要将所有对应的数据进行分组呢?
  • 只要使用一个变量用来存储所有的<Selector>列表(注意此处不能接extract()函数,否则得到的列表并不是响应数据response的类型则不能再下面调用),再使用for循环遍历列表中所有节点并依次获取数据即可:
1
2
3
4
5
6
7
8
def parse(self, response):
li_list = response.xpath('//*[@class="clears"]/li')
#不能使用extract(),否则类型不同于response响应类型,则不能被循环调用
for li in li_list:
item = {} #定义字典变量用来存储数据
item["name"] = li.xpath('./div[2]/h2/text()').extract() #. 表示从当前节点开始取
item["level"] = li.xpath('./div[2]/h2/span/text()').extract()
print(item)
  • 得到结果如图:

image-20200910203824629

  • 但是却发现输出数据的最前头和最后头都分别有四个空数据,这是为什么?

  • 回顾刚刚代码中的xpath('//*[@class="clears"]/li'),发现原网页中class属性不唯一!如图所示一共有四处节点使用了该class属性,而我们需要获取数据的节点只有一处,此处需要注意,class属性值并不唯一,可能被其它地方引用而导致我们获取无用数据:

image-20200910204220189

  • 修改之后代码如下:
1
2
3
4
5
6
7
8
def parse(self, response):
#只需要修改父节点一处即可
li_list = response.xpath('//*[@class="maincon"]/ul/li')
for li in li_list:
item = {} #定义字典变量用来存储数据
item["name"] = li.xpath('./div[2]/h2/text()').extract()
item["level"] = li.xpath('./div[2]/h2/span/text()').extract()
print(item)
  • 如图所示,成功解决问题:

image-20200910204627728

7 pipelines管道文件简要介绍

7.1 单爬虫使用

  • 刚刚我们获取的数据都是通过print()函数直接打印在终端上,那么接下来我们就要将这些文件通过pipelins管道文件传输出来,暂时也是使用print(),此处主要是简要讲述pipelines管道文件;
  • 管道文件其实早就初始化在我们的spider文件中,也就是其中的pipelines.py文件,但在正式使用之前,需要到settings.py文件中进行配置:
1
2
3
4
5
6
# 在settings.py文件中找到被注释掉的这段代码并去掉注释即可使用
ITEM_PIPELINES = {
#其中该字典元素的键名即为pipelines.py文件中的类名路径,后面的值300表示距离管道文件的距离,距离越近值越小表示越先经过该文件,可以设置多个值,通过不同的键值确定其顺序
'spiderStudy.pipelines.SpiderstudyPipeline': 300,
'spiderStudy.pipelines.SpiderstudyPipeline1': 301,
}
  • 配置完成后先在爬虫文件中将获取的数据返回给pipelines.py文件,只要通过一条语句即可完成:
1
2
3
4
5
for li in li_list:
item = {} #定义字典变量用来存储数据
item["name"] = li.xpath('./div[2]/h2/text()').extract()
item["level"] = li.xpath('./div[2]/h2/span/text()').extract()
yield item
  • 最后在pipelines.py文件中编写逻辑代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
class SpiderstudyPipeline:
# 该函数为默认接收并执行逻辑数据的函数,不可修改
def process_item(self, item, spider):
# 对所有返回的字典增加一个键值对
item["hello"] = "world"
# 函数结束后必须返回其值,否则无法继续使用
return item


class SpiderstudyPipeline1:
def process_item(self, item, spider):
print(item)
return item
  • 执行代码得到结果便可知其妙用:

image-20200910210416661

  • 结束之后我们再重新回过头看一下爬虫文件中的yield item语句,这段语句放在for循环之内,表示每次得到一对数据就返回给管道文件一次,那么可不可以将含有所有元素的字典列表一次性返回给管道文件呢?我们来尝试一下:
1
2
3
4
5
6
7
8
#新建一个list变量用于存储所有的item值
content_list = []
for li in li_list:
item = {} #定义字典变量用来存储数据
item["name"] = li.xpath('./div[2]/h2/text()').extract()
item["level"] = li.xpath('./div[2]/h2/span/text()').extract()
content_list.append(item)
yield content_list
  • 输出结果为:

image-20200910211436369

  • 直接报错,由此可知不能一次性返回一个列表;

其实最后可以总结一下yield传输值给pipelines.py文件时的过程,pipelines.py文件是对于每一次yield返回值都会进行一次逻辑运算的,而yield自身只能传输请求、字典或者None值,而不能传输list列表。

最后再讲一下extract()函数,这个函数调用时是直接返回list列表取得的所有值,而我们在分组单次数据获取时只需要获取一个值便可,一般会使用extract()[0](获取数组中第一个值)来替代上述写法,但是extract()[0]有一个弊端就是如果列表中没有值不会返回任何值(连空字符串都没有),但yield却不能返回这种类型的值而会导致报错,于是我们更常用extract_first()来替代上一种写法,这种写法在列表中没有值时会返回一个None字符串,可以直接被yield传输

7.2 多爬虫使用

  • 在同一个项目spiderStudy下,创建多个爬虫文件;

  • 可以创建多个pipeline类用于接收来自不同爬虫文件的数据:

    • 通过以下两种方式处理不同的数据:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      #通过一个类来说明
      #1.在每个爬虫文件传入的item值中定义一个字典元素“come_from”,并且赋值,通过此值判断
      class SpiderstudyPipeline:
      def process_item(self, item, spider):

      if item["come_from"] == "itcase":

      elif ...:

      else ...:


      #2.通过传入的spider参数判断,spider参数本身就是被传入的爬虫文件
      #其中spider.name变量就是spider爬虫文件最初定义的爬虫名
      if spider.name == "itcase":

      elif ...:

      else ...:





      return item

8.logging

8.1 定义

  • 手动调试并存储日志信息,有助于项目结束之后调试bug;

8.2 scrapy导入及使用

  • 第一种方式,直接使用原包:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 导入logging包
import logging

# 将item信息以warning等级的信息存储为日志信息显示,设置存储为文件之后就不会在终端显示而只存储至文件中
# warning 还可为 debug、info、error,配合settings.py中的LOG_LEVEL使用
for i in range(5):
item = {}
item["come_from"] = "itcast"
logging.warning(item)

>#输出如下
2020-09-11 19:44:37 [root] WARNING: {'come_from': 'itcast'}
2020-09-11 19:44:37 [root] WARNING: {'come_from': 'itcast'}
2020-09-11 19:44:37 [root] WARNING: {'come_from': 'itcast'}
2020-09-11 19:44:37 [root] WARNING: {'come_from': 'itcast'}
2020-09-11 19:44:37 [root] WARNING: {'come_from': 'itcast'}
  • 第二种方式,使用函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import logging

logger = logging.getLogger(__name__)
#全局定义一次,其它文件可以直接引用

for i in range(5):
item = {}
item["come_from"] = "itcast"
logger.warning(item)

>#输出如下
2020-09-11 19:48:18 [spiderStudy.spiders.itcast] WARNING: {'come_from': 'itcast'}
2020-09-11 19:48:18 [spiderStudy.spiders.itcast] WARNING: {'come_from': 'itcast'}
2020-09-11 19:48:18 [spiderStudy.spiders.itcast] WARNING: {'come_from': 'itcast'}
2020-09-11 19:48:18 [spiderStudy.spiders.itcast] WARNING: {'come_from': 'itcast'}
2020-09-11 19:48:18 [spiderStudy.spiders.itcast] WARNING: {'come_from': 'itcast'}

对比之后可以发现第二种函数调用方法可以显示出所传入参数来自于哪个爬虫文件或者管道文件,更加有利于信息整理,其实日志信息一般放至pipelines.py文件中用来统一处理数据信息

8.3 将logging文件存储至日志文件

  • settings.py文件中添加LOG_FILE = "./log.log"即可存储至代码目录下为log.log文件;

8.4 普通python文件调用logging包

1
2
3
4
5
6
7
8
9
10
import logging
logging.basicConfig(
level=logging.INFO,
format="" #百度查找具体格式即可
filename="./log.log"
) #设置日志输出的样式和格式,
logger = logging.getLogger(__name__) #实例化logger,在任何py文件中直接调用logger即可
# 调用格式 from log.a(省略.py) import logger
#用以下语句来观看结果
logger.info("info")

9.实现翻页请求

  • 注:由于原学习视频是18年的,而现在20年很多网站都设置反扒,不好使用原案例,直接贴代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def parse(self,response):
tr_list = response.xpath("//table[eclass='tablelist']/tr")[1:-1]
for tr in tr_list:
item = []
item["title"] = tr.xpath("./td[1]/a/text()").extract_first(
item["position"] = tr.xpath("./td[2]/text()").extract_first(
item[ "publish_date"] = tr.xpath("./td[5]/text()").extract_first()
yield item
#找到下一页的url地址
next_url = response.xpath("//a[@id='next']/@href").extract_first()
if next_url != "javascript:; ":
next_url = "http://hr.tencent.com/" +next_url
yield scrapy.Request(
next_url,
callback=self.parse
)

  • 并且从原网站中拿到请求头中的UserAgent数据贴到settings.py文件中:

  • 在网站Network中打开原网页的Response页面,注意与直接检查HTML代码中tbody标签是否有无,实质上我们请求得到的响应式前者网页源代码,而Response中可能没有tbody标签而导致无法使用;

10.scrapy.Request()

image-20200911202745519

-------- 本文结束 感谢阅读 --------