Scrapy框架简介
scrapy框架
- Scrapy是用纯Python实现一个为了爬取网站数据、提取结构性数据而编写的应用框架,用途非常广泛。
- 框架的力量,用户只需要定制开发几个模块就可以轻松的实现一个爬虫,用来抓取网页内容以及各种图片,非常之方便。
- Scrapy 使用了
Twisted['twrstrd]
(其主要对手是Tornado)异步网络框架来处理网络通讯,可以加快我们的下载速度,不用自己去实现异步框架,并且包含了各种中间件接口,可以灵活的完成各种需求
scrapy是什么?
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。可以应用在包括数据挖掘,信息处理
或存储历史数据等一系列的程序中。
安装scrapy
安装非Python的依赖
sudo apt-get install python-dev python-pip libxm12-dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev(Ubuntu下安装)
pip install scrapy
安装过程中出错:
如果安装有错误!!!!
pip install Scrapy building 'twisted.test.raiser' extension error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual‐cpp‐build‐tools
解决方案:
http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
下载twisted对应版本的whl
文件(如我的Twisted‐17.5.0‐cp36‐cp36m‐win_amd64.whl),cp后面是python版本,amd64代表64位,运行命令:pip install C:\Users\...\Twisted‐17.5.0‐cp36‐cp36m‐win_amd64.whl
pip install Scrapy
- 如果再报错:
python ‐m pip install ‐‐upgrade pip
- 如果再报错 win32
解决方法:
pip install pypiwin32
- 再报错:使用anaconda
使用步骤:- 打开anaconda
- 点击environments
- 点击not installed
- 输入scrapy
- apply(应用)
- 在pycharm中选择anaconda的环境
Scrapy架构图(绿线是数据流向)
Scrapy Engine
(引擎):负责Spider
、Item Pipeline
、Downloader
、Scheduler
中间的通讯,信号、数据传递等。Scheduler
(调度器):它负责接受引擎发送过来的Request请求,并按照一定的方式进行整理排列,入队,当引擎 需要时,交还给引擎。Downloader
(下载器):负责下载Scrapy Engine
(引擎)发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine
(引擎),由引擎交给Spider
来处理。Item Pipeline
(管道):它负责处理Spider
中获取到的 Item(有用的数据,需要存储的),并进行进行后期处理(详细分析、过滤、存储等)的地方.Downloader middlewares
(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件。Spider Middlewares
(spider中间件):你可以理解为是一个可以自定扩展和操作引擎
和Spider
中间通信的功能组件(比如进入Spider
的Responses;和从Spider
出去的Requests)
scrapy架构流程
(1)引擎 ‐‐‐》自动运行,无需关注,会自动组织所有的请求对象,分发给下载器
(2)下载器 ‐‐‐》从引擎处获取到请求对象后,请求数据
(3)Spiders ‐‐‐》Spider类定义了如何爬取某个(或某些)网站。包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item)。 换句话说,Spider就是您定义爬取的动作及分析某个网页(或者是有些网页)的地方。
(4)调度器 ‐‐‐》有自己的调度规则,无需关注
(5)管道(Item Pipeline) ‐‐‐》最终处理数据的管道,会预留接口供我们处理数据.当Item在Spider中被收集之后,它将会被传递到Item Pipeline,一些组件会按照一定的顺序执行对Item的处理。每个itempipeline组件(有时称之为“Item Pipeline”)是实现了简单方法的Python类。他们接收到Item并通过它执行一些行为,同时也决定此Item是否继续通过pipeline,或是被丢弃而不再进行处理。
以下是item pipeline的一些典型应用:
清理HTML数据
验证爬取的数据(检查item包含某些字段)
查重(并丢弃)
将爬取结果保存到数据库中
Scrapy项目的运行流程
代码写好,程序开始运行
- 引擎: Hi!Spider,你要处理哪一个网站?
- Spider: 老大要我处理xxxx.com。
- 引擎: 你把第一个需要处理的URL给我吧。
- Spider: 给你,第一个URL是 xxxxxxx.com。
- 引擎: Hi!调度器,我这有request请求,你帮我排序入队一下
- 调度器: 好的,正在处理,你等一下。
- 引擎: Hi!调度器,把你处理好的request请求给我。
- 调度器: 给你,这是我处理好的request
- 引擎: Hi!下载器,你按照老大的
下载中间件
的设置帮我下载一下这个request请求 - 下载器: 好的!给你,这是下载好的东西。(如果失败:sorry,这个request下载失败了。然后
引擎
告诉调度器
,这个request下载失败了,你记录一下,我们待会儿再下载) - 引擎: Hi! spider,这是下载好的东西,并且已经按照老大的
下载中间件
处理过了,你自己处理一下(注意!这儿responses默认是交给 def parse() 这个函数处理的) - Spider: (处理完毕数据之后对于需要跟进的URL),Hi!引擎,我这里有两个结果,这个是我需要
跟进的URL,还有这个是我获取到的Item数据。 - 引擎: Hi !管道 我这儿有个Item你帮我处理一下!调度器!这是需要跟进URL你帮我处理下。然后从第四步开始循环,直到获取完老大需要全部信息。
- 管道、调度器: 好的,现在就做!
注意!只有当调度器
中不存在任何request了,整个程序才会停止,(也就是说,对于下载失败的URL,Scrapy也会重新下载。)
制作Scrapy爬虫一共四步
新建项目(scrapy start project xxx):新建一个新的爬虫项词。
明确目标(编写items.py):明确你想要抓取的目标。
制作爬虫(spiders/xxspider.py):制作爬虫开始爬取网页。
存储内容(pipelines.py):设计管道存储爬取内容。
1.创建scrapy项目:
终端输入 `scrapy startproject 项目名称`
2.项目组成:
- spiders
- __init__.py
- 自定义的爬虫文件.py ‐‐‐》由我们自己创建,是实现爬虫核心功能的文件
- _init_.py
- items.py ‐‐‐》定义数据结构的地方,是一个继承自scrapy.Item的类
- middlewares.py ‐‐‐》中间件 代理
- pipelines.py ‐‐‐》管道文件,里面只有一个类,用于处理下载数据的后续处理,默认是300优先级,值越小优先级越高(1‐1000)
- settings.py ‐‐‐》配置文件 比如:是否遵守robots协议,User‐Agent 定义
3.创建爬虫文件:
(1)跳转到spiders文件夹 cd 目录名字/目录名字/spiders
(2)scrapy genspider 爬虫名字 网页的域名
爬虫文件的基本组成:
- 继承scrapy.Spider类
- name = ‘baidu’ ‐‐‐》 运行爬虫文件时使用的名字
- allowed_domains ‐‐‐》爬虫允许的域名,在爬取的时候,如果不是此域名之下的url,会被过滤掉
- start_urls ‐‐‐》 声明了爬虫的起始地址,可以写多个url,一般是一个
- parse(self, response) ‐‐‐》解析数据的回调函数
- response.text ‐‐‐》响应的是字符串
- response.body ‐‐‐》响应的是二进制文件
- response.xpath() –‐》xpath方法的返回值类型是selector列表
- extract() ‐‐‐》提取的是selector对象的是data
- extract_first() ‐‐‐》提取的是selector列表中的第一个数据
- 继承scrapy.Spider类
4.运行爬虫文件:
`scrapy crawl 爬虫名称`
注意:应在spiders文件夹内执行
Scrapy框架终端基本命令
- scrapy bench 测试性能(pages/min)
- scrapy fetch ‘http://www.baidu.com’ 爬取百度页面的源代码,DEBUG信息(200)表示爬虫程序正常运行
- genspider 创建爬虫
- runspider 启动爬虫
- shell 使用scrapy的shell环境
- startproject 创建项目
- version 显示版本
- view 使用浏览器视图
- list 显示当前项目有多少个爬虫程序
入门案例
学习目标
- 创建一个Scrapy项目
- 定义提取的结构化数据(Item)
- 编写爬取网站的 Spider 并提取出结构化数据(Item)
- 编写 Item Pipelines 来存储提取到的Item(即结构化数据)
1.新建爬虫项目
在开始爬取之前,必须创建一个新的Scrapy项目。进入自定义的项目目录中,运行下列命令scrapy startproject mySpider
2.新建爬虫
跳转到mySpider/mySpider/spiders文件夹下
scrapy genspider 爬虫名 网页域名
网页域名的作用,让爬虫程序只在此域名下爬取
会在spiders文件夹下生成爬虫名.py文件
import scrapy
class ItcastSpider(scrapy.Spider): #Spider类
name = "itcast" # 爬虫名称
allowed_domains = ["www.itcast.cn"] # 爬虫运行的域名
start_urls = ["http://www.itcast.cn"] # 起始url,爬虫程序启动的第一次请求目的url
"""处理响应数据的方法"""
def parse(self, response):
print(response.body)
# pass
后面还会学习到CrawlSpider
类
3.运行和检测爬虫程序
- 先看项目能不能正常运行在修改代码
此时,输入scrapy会多一些可用指令
- scrapy check 爬虫名 检查爬虫是否正常
- scrapy crawl 爬虫名 启动爬虫程序
下面来简单介绍一下各个主要文件的作用:
- scrapy.cfg :项目的配置文件
- Itcast/:项目的Python模块,将会从这里引用代码
- Itcast/items.py:项目的目标文件
- Itcast/pipelines.py:项目的管道文件
- Itcast/settings.py:项目的设置文件
- Itcast/spiders/:存储爬虫代码目录
4.明确目标(Itcast/items.py)
我们打算抓取:http://www.itcast.cn/channel/teacher.shtml 网站里的所有讲师的姓名、职称和个人信息。
- 打开mySpider目录下的items.py,
- Item 定义结构化数据字段用来保存爬取到的数据,有点像Python中的dict,但是提供了一些额外的保护减少错误。
- 可以通过创建一个
scrapy.Item
类,并且定义类型为scrapy.Field
的类属性来定义一个 Item (可以理解成类似于ORM的映射关系)。 - 接下来,创建一个
ItcastItem
类,和构建item模型(model)。
5.制作爬虫
爬数据(Itcast/spiders/itcast.py)
在当前目录下输入命令,将在 myspider/spider 目录下创建一个名为 itcast 的爬虫,并指定爬取域的范围:Scrapy genspider itcast "itcast.cn"
打开 Itcast/spider
目录里的 itcast.py,默认增加了下列代码
import scrapy
class ItcastSpider(scrapy.Spider):
name = "itcast"
allowed_domains = ["www.itcast.cn"]
start_urls = ["http://www.itcast.cn"]
"""处理响应数据的方法"""
def parse(self, response):
# pass
其实也可以由我们自行创建 itcast.py 并编写上面的代码,只不过使用命令可以免去编写固定代码的麻烦
要建立一个Spider,你必须用scrapy.Spider类创建一个子类,并确定了三个强制的属性和一个方法。
name = “”
: 这个爬虫的识别名称,必须是唯一的,在不同的爬虫必须定义不同的名字。allow_domains = []
是搜索的域名范围,也就是爬虫的约束区域,规定爬虫只爬取这个域名下的网
页,不存在的URL会被忽略。start_urls = ()
:爬取的URL元祖/列表。爬虫从这里开始抓取数据,所以,第一次下载的数据将会从这些URL开始。其他子URL将会从这些起始URL中继承性生成。parse(self,response)
:解析的方法,每个初始URL完成下载后将被调用,调用的时候传入从每一个URL传回的Response对象来作为唯一参数,主要作用如下:- 负责解析返回的网页数据(response.body),提取结构化数据(生成item)
- 生成需要下一页的URL请求。
将start_urls的值修改为需要爬取的第一个url
start_urls =("http://ww.itcast.cn/channel/teacher.shtml")
修改parse()方法
def parse(self, response):
with open("teacher.html","w")as f:
f.write(response.text)
然后运行一下看看,在Itcast目录下执行
scrapy crawl itcast
是的,就是 itcast,看上面代码,它是 ItcastSpider 类的 name 属性,也就是使用 scrapy genspider 命令的爬虫名。
一个Scrapy爬虫项目里,可以存在多个爬虫。各个爬虫在执行时,就是按照 name 属性来区分。
运行之后,如果打印的日志出现[scrapy]INFO:Spider closed(finished)
,代表执行完成。之后当前文件夹中就出现了一个 teacher.html 文件,里面就是我们刚刚要爬取的网页的全部源代码信息
# 注意,Python2.x默认编码环境是ASCII码,当和取回的数据编码格式不一致时,可能会造成乱码;
# 我们可以指定保存内容的编码格式,一般情况下,我们可以在代码最上方添加:
import sys
reload(sys)
sys.setdefaultencoding("utf-8")
#这三行代码是Python2.x里解决中文编码的万能钥匙,经过这么多年的吐槽后Python3学乖了,默认编码是Unicode了..
取数据
爬取整个网页完毕,接下来的就是取数据过程了,首先观察
很明显可以看出网页结构如下图所示
<li>
<img src="images/teacher/javaee/20220302113627师老师高级讲师2009年入行.jpg">
<div class="li_txt">
<h3>师老师</h3>
<h4>高级讲师</h4>
<p>13年的项目开发和教育培训经验,精通Java EE的主流开发框架、Oracle和MySQL等关系型数据库。曾在中科院遥感应用研究所、慧点科技、达利本斯等公司担任软件开发工程师、项目总监,带团队做过边防部队、人寿集团、平安集团等多个企业的大型项目,之后在互联网公司知果科技担任开发经理,完成知果果网的核心产品开发。 </p>
</div>
</li>
所以,直接使用xpath来提取数据
我们先引用Itcast/items.py
里面的ItcastItem类
from Itcast.items import ItcastItem
导包从项目的根目录下开始
然后将我们得到的数据封装到一个Itcastitem对象中,可以保存每个老师的属性:
import scrapy
from Itcast.Itcast.items import ItcastItem
class ItcastSpider(scrapy.Spider): # Spider类
name = "itcast" # 爬虫名称
allowed_domains = ["www.itcast.cn"] # 爬虫运行的域名
start_urls = [
"http://www.itcast.cn/channel/teacher.shtml"
] # 起始url,爬虫程序启动的第一次请求目的url
"""处理响应数据的方法"""
def parse(self, response):
with open("teacher.html", "w", encoding="utf-8") as f:
f.write(response.text)
# 存放老师信息的集合
items = []
for each in response.xpath(
"//div[@class=' tea_txt']//ul//li//div[@class='li_txt']"
): # each是一个结点,对节点使用xpath方法需要加'./'表示当前节点下
# 将我们得到的数据封装成一个ItcastItem对象
item = ItcastItem()
# extract()方法返回的都是unicode字符串
name = each.xpath("./h3/text()").extract()
title = each.xpath("./h4/text()").extract()
info = each.xpath("./p/text()").extract()
# xpath返回的是包含一个元素的列表
item["name"] = name[0]
item["title"] = title[0]
item["info"] = info[0]
items.append(item)
# 直接返回最后数据
return items
我们先不处理管道,后面会详细解释
保存数据
scrapy保存信息的最简单的方法主要有四种,-o输出指定格式的文件,命令如下
如果spiders/itcast.py中使用return 返回的是一个ItcastItem对象,会自动识别给piplines管道来处理数据
如果return的是ItcastItem对象的列表,可以使用以下命令来进行持久化存储
# json格式,默认为Unicode编码
scrapy crawl itcast -o teachers.json
# json lines格式,默认为Unicode编码
scrapy crawl itcast -o teachers.jsonl
# csv 逗号表达式,可用Exce1打开
scrapy crawl itcast -o teachers.csv
# xml格式
scrapy crawl itcast -o teachers.xml
思考
如果将代码改成下面形式,结果完全一样。请思考yield在这里的作用
import scrapy
from Itcast.items import ItcastItem
class ItcastSpider(scrapy.Spider): # Spider类
name = "itcast" # 爬虫名称
allowed_domains = ["www.itcast.cn"] # 爬虫运行的域名
start_urls = [
"http://www.itcast.cn/channel/teacher.shtml"
] # 起始url,爬虫程序启动的第一次请求目的url
"""处理响应数据的方法"""
def parse(self, response):
with open("teacher.html", "w", encoding="utf-8") as f:
f.write(response.text)
# 存放老师信息的集合
# items = []
for each in response.xpath(
"//div[@class=' tea_txt']//ul//li//div[@class='li_txt']/"
):
# 将我们得到的数据封装成一个ItcastItem对象
item = ItcastItem()
# extract()方法返回的都是unicode字符串
name = each.xpath("./h3/text()").extract()
title = each.xpath("./h4/text()").extract()
info = each.xpath("./p/text()").extract()
# xpath返回的是包含一个元素的列表
item["name"] = name[0]
item["title"] = title[0]
item["info"] = info[0]
# items.append(item)
# 将获取到的数据交给piplines,后继续回来执行
yield item
# 直接返回最后数据
# return items
yield的作用是,执行到yield后,不会像return那样直接返回,函数结束,而是会在返回此次后继续到上次执行的位置继续执行,return每次调用返回的值都一样
带有 yield 的函数不再是一个普通函数,而是一个生成器 generator,可用于迭代
yield 是一个类似 return 的关键字,迭代一次遇到 yield 时就返回 yield 后面(右边)的值。重点是:下一次迭代时,从上一次迭代遇到的yield后面的代码(下一行)开始执行
简要理解:yield就是 return 返回一个值,并且记住这个返回的位置,下次迭代就从这个位置后(下一行)开始
管道处理保存数据(piplines.py)
要先在settings.py中启用管道后,itcast.py爬取的数据才会经过piplines管道进行处理保存
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
# useful for handling different item types with a single interface
import json
class ItcastPipeline:
def __init__(self) -> None:
self.f = open("itcast_piplines.json", "wb")
def process_item(self, item, spider): # item就是items.py返回的值
content = json.dumps(
dict(item), ensure_ascii=False
) # 把字典转换成json格式,ensure_ascii表示不把中文字符串当做ascii码,而是unicode
self.f.write(content.encode("utf-8"))
# 返回给引擎,告诉引擎处理完毕,给我下一个
return item
def close_spider(self, spider): # 整个爬虫程序关闭时做的事
self.f.close()
需要在**items.py
**中定义需要的属性
class ItcastItem(scrapy.Item):
# define the fields for your item here like:
name = scrapy.Field() # Field()类似字典
info = scrapy.Field()
title = scrapy.Field()
# pass
Item Pipline
当Item在Spider中被收集之后,它将会被传递到Item Pipeline,这些Item Pipeline组件按定义的顺序处理Item.
每个Item Pipeline都是实现了简单方法的Python类,比如决定此Item是丢弃而存储。以下是item pipeline的一些典型应用:
- 验证爬取的数据(检查item包含某些字段,比如说name字段)
- 查重(并丢弃)
- 将爬取结果保存到文件或者数据库中
import something # 确保这里的something是您实际想要导入的模块或包
class SomethingPipeline(object):
def __init__(self):
# 可选实现,做参数初始化等
# 这里可以添加初始化代码,例如连接数据库等
pass
def process_item(self, item, spider):
# item(Item 对象) - 被爬取的item
# spider(Spider 对象) - 爬取该item的spider
# 这个方法必须实现,每个item pipeline组件都需要调用该方法
# 方法必须返回一个 Item 对象,被丢弃的item将不会被之后的pipeline组件处理
# 这里可以添加处理item的代码,比如清洗数据、保存数据到数据库等
return item
def open_spider(self, spider):
# spider(Spider 对象) - 被开启的spider
# 可选实现,当spider被开启时,这个方法被调用
# 这里可以添加一些在spider启动时需要的初始化操作
pass
def close_spider(self, spider):
# spider(Spider 对象) - 被关闭的spider
# 可选实现,当spider被关闭时,这个方法被调用
# 这里可以添加一些清理代码,比如关闭数据库连接等
pass
Scrapy Shell
Scrapy终端是一个交互终端,我们可以在未启动spider的情况下尝试及调试代码,也可以用来测试XPath或CSS表达式,查看他们的工作方式,方便我们爬取的网页中提取的数据。
如果安装了IPython,Scrapy终端将使用 IPython(替代标准Python终端)。IPython终端与其他相比更为强大,提供智能的自动补全,高亮输出,及其他特性。(推荐安装IPython)
启动scrapyshell
进入项目的根目录,执行下列命令来启动shell
scrapy shell "http://ww.itcast.cn/channel/teacher.shtml"
Scrapy Shell根据下载的页面会自动创建一些方便使用的对象,例如Response
对象,以及selector
对象(对HTML及XML内容)。
当shell载入后,将得到一个包含response数据的本地response变量,输入response.body
将输出response的包体,输出response.headers 可以看到response的包头。
输入response.selector 时,将获取到一个response 初始化的类 Selector 的对象,此时可以通过使用 response.selector.xpath()
或response.selector.css()
来对 response 进行查询。
Scrapy也提供了一些快捷方式,例如 response.xpath()
或 response.css()
同样可以生效(如之前的案例)。
Selectors选择器
Scrapy Selectors 内置 XPath 和 Css Selector 表达式机制
Selector有四个基本的方法,最常用的还是xpath
xpath()
:传入xpath表达式,返回该表达式所对应的所有节点的selector list列表extract()
:序列化该节点为Unicode字符串并回listcss()
:传入CSS表达式,返回该表达式所对应的所有节点的selector list列表,语法同 BeautifulSoup4。re()
:根据传入的正则表达式对数据进行提取,返回Unicode字符串list列表
# 定位页面上的所有链接
//a
# 定位具有特定类名的元素,例如所有类名为"my-class"的<div>元素
//div[@class='my-class']
# 如果类名有多个值,定位包含特定类名的元素
//div[contains(concat(' ', normalize-space(@class), ' '), ' my-class ')]
# 定位包含特定文本的元素,例如所有包含"点击这里"文本的<a>元素
//a[contains(text(), '点击这里')]
# 定位具有特定属性值的元素,例如所有href属性中包含"example.com"的<a>元素
//a[contains(@href, 'example.com')]
# 定位某个特定元素下的所有直接子元素,例如<div id="container">下的所有<p>元素
//div[@id='container']/p
# 定位具有特定id的元素,例如id为"unique-id"的<div>元素
//div[@id='unique-id']
Spider
Spider类定义了如何爬取某个(或某些)网站。包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item)。
换句话说,Spider就是您定义爬取的动作及分析某个网页(或者是有些网页)的地方。
class scrapy.spider
是最基本的类,所有编写的爬虫必须继承这个类。
主要用到的函数及调用顺序为:
init_()
: 初始化爬虫名字和start_urls列表start_requests()
: 调用make_requests_from_url()
生成Requests对象交给Scrapy下载并返responseparse()
: 解析response,并返回 Item 或 Requests (需指定回调函数)。Item传给item pipline持久化 ,而Requests交由Scrapy下载,并由指定的回调函数处理(默认parse(),一直进行循环,直到处理完所有的数据为止。)
源码参考
# 所有爬虫的基类,用户定义的爬虫必须从这个类继承
class Spider(object_ref):
"""Base class for scrapy spiders. All spiders must inherit from this class."""
name: str
custom_settings: Optional[dict] = None
# 定义spider名字的字符串(string)。spider的名字定义了scrapy如何定位(并初始化)spider,所以其必须是唯一的
# name是spider最重要的属性,而且是必须的。
# 一般做法是以该网站(domain)(加或不加后缀)来命名spider。例如,如果spider爬取mywebsite.com,该spider名为mywebsite
def __init__(self, name: Optional[str] = None, **kwargs: Any):
if name is not None:
self.name = name
# 如果爬虫没有名字,中断后续操作则报错
elif not getattr(self, "name", None):
raise ValueError(f"{type(self).__name__} must have a name")
# python 对象或类型通过内置成员_dict 来存储成员信息
self.__dict__.update(kwargs)
# URL列表。当没有指定的URL时,spider将从该列表中开始进行爬取。 因此,第一个被获取到的页面的URL将是该列表的内容
if not hasattr(self, "start_urls"):
self.start_urls: List[str] = []
@property
def logger(self) -> logging.LoggerAdapter:
logger = logging.getLogger(self.name)
return logging.LoggerAdapter(logger, {"spider": self})
# 打印scrapy执行后的1og信息
def log(self, message: Any, level: int = logging.DEBUG, **kw: Any) -> None:
"""Log the given message at the given log level
This helper wraps a log call to the logger within the spider, but you
can use it directly (e.g. Spider.logger.info('msg')) or use any other
Python logger too.
"""
self.logger.log(level, message, **kw)
@classmethod
def from_crawler(cls, crawler: Crawler, *args: Any, **kwargs: Any) -> Self:
spider = cls(*args, **kwargs)
spider._set_crawler(crawler)
return spider
# 判断对象object的属性是否存在,不存在做断言处理
def _set_crawler(self, crawler: Crawler) -> None:
self.crawler = crawler
self.settings = crawler.settings
crawler.signals.connect(self.close, signals.spider_closed)
# 该方法将读取start_urls内的地址,并为每一个地址生成一个Request对象,交给Scrapy下载并返回Response#该方法仅调用一次
def start_requests(self) -> Iterable[Request]:
if not self.start_urls and hasattr(self, "start_url"):
raise AttributeError(
"Crawling could not start: 'start_urls' not found "
"or empty (but found 'start_url' attribute instead, "
"did you miss an 's'?)"
)
for url in self.start_urls:
yield self.make_requests_from_url(url)
def make_requests_from_url(self,url):
#start_requests()中调用,实际生成Request的函数。
#Request对象默认的回调函数为parse(),提交的方式为get
return Request(url, dont_filter=True)
def _parse(self, response: Response, **kwargs: Any) -> Any:
return self.parse(response, **kwargs)
# 默认的Request对象回调函数,处理返回的response.
# 生成Item或者Request对象。用户必须实现这个类
def parse(self, response: Response, **kwargs: Any) -> Any:
raise NotImplementedError(
f"{self.__class__.__name__}.parse callback is not defined"
)
@classmethod
def update_settings(cls, settings: BaseSettings) -> None:
settings.setdict(cls.custom_settings or {}, priority="spider")
@classmethod
def handles_request(cls, request: Request) -> bool:
return url_is_from_spider(request.url, cls)
@staticmethod
def close(spider: Spider, reason: str) -> Union[Deferred, None]:
closed = getattr(spider, "closed", None)
if callable(closed):
return cast(Union[Deferred, None], closed(reason))
return None
def __repr__(self) -> str:
return f"<{type(self).__name__} {self.name!r} at 0x{id(self):0x}>"
Python @property装饰器详解 - 贾志文 - 博客园 (cnblogs.com)
【Python】一文弄懂python装饰器(附源码例子)_python 装饰器-CSDN博客
Python 函数装饰器 | 菜鸟教程 (runoob.com)
name 定义spider名字的字符串。
例如,如果spider爬取 mywebsite.com,该spider通常会被命名为 mywebsite
allowed domains
包含了spider允许爬取的域名(domain)的列表,可选。
start urls
初始URL元组/列表。当没有制定特定的URL时,spider将从该列表中开始进行爬取。
start requests(self)
该方法必须返回一个可迭代对象(iterable)。该对象包含了spider用于爬取(默认实现是使用start urls 的
url)的第一个Request.当spider启动爬取并且未指定start urls时,该方法被调用。
parse(self, response)
当请求url返回网页没有指定回调函数时,默认的Request对象回调函数。用来处理网页返回的response,以及生成Item或者Request对象。
log(self,messagel.level, component])
使用 scrapy.log.msg()方法记录(log)message。更多数据请参见 logging
尝试腾讯招聘案例(普通版)
python爬虫(二十二)scrapy案例–爬取腾讯招聘数据_爬取腾讯社会招聘“数据分析”岗位的所有招聘信息-CSDN博客
我们用腾讯社招的网站搜索 | 腾讯招聘 (tencent.com)举例:
(http://hr.tencent.com/position.php?&start=0#a中#a表示锚点,定位到页面的哪个位置)
首先分析要爬取的数据有哪些
根节点的xpath路径为//div[@class='recruit-list']/a
初步确定要爬取的信息
职位名positionName: //div[@class='recruit-list']/a/div/span[1]
职位类别positionType: //div[@class='recruit-list']/a/p/span[3]
职位要求positionRequire://div[@class='recruit-list']/a/p/span[5]
工作地点workLocation: //div[@class='recruit-list']/a/div/span[2]
详细介绍positionInfo: //div[@class='recruit-list']/a/p[@class='recruit-text']
最后更新时间updateTime://div[@class='recruit-list']/a/p/span[7]
然后创建爬虫项目和程序
scrapy startproject Tencent
cd .\Tencent\
scrapy genspider tencent "tencent.com"
items.py
定义要采集的数据模型
# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html
import scrapy
class TencentItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
# 职位名
positionName = scrapy.Field()
# 职位类别
positionType = scrapy.Field()
# 职位要求
positionRequire = scrapy.Field()
# 工作地点
workLocation = scrapy.Field()
# 详细介绍
positionInfo = scrapy.Field()
# 最后更新时间
updateTime = scrapy.Field()
tencent.py
import scrapy
from Tencent.items import TencentItem
class TencentSpider(scrapy.Spider):
name = "tencent"
allowed_domains = ["tencent.com"]
baseUrl = "http://hr.tencent.com/search.html?index="
index = 1
start_url = [baseUrl + str(index)]
def parse(self, response):
node_list = response.xpath("//div[@class='recruit-list']/a")
for node in node_list:
item = TencentItem()
# 提取每个职位的信息
item["positionName"] = node.xpath("./div/span[1]/text()").extract()[0]
item["positionType"] = node.xpath(
"//div[@class='recruit-list']/a/p/span[3]/text()"
).extract()[0]
item["positionRequire"] = node.xpath(
"//div[@class='recruit-list']/a/p/span[5]/text()"
).extract()[0]
item["workLocation"] = node.xpath("./div/span[2]/text()").extract()[0]
item["positionInfo"] = node.xpath(
"//div[@class='recruit-list']/a/p[@class='recruit-text']/text()"
).extract()[0]
item["updateTime"] = node.xpath(
"//div[@class='recruit-list']/a/p/span[7]/text()"
).extract()[0]
yield item
# 此次硬编码,不是最佳方案,可在每一页中提取下一页url
if self.index < 282:
self.index += 10
url = self.baseUrl + str(self.index)
yield scrapy.Request(url=url, callback=self.parse)
# def parse_next(self,response):
# pass
start_url = [baseUrl + str(index)]
:拼接请求网址yield scrapy.Request(url=url, callback=self.parse)
: 对新网址发送请求,callback函数可自己定义- yield会把item/Request返回给引擎,引擎判断是交给管道保存还是继续进入请求队列
piplines.py
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
# useful for handling different item types with a single interface
import json
from itemadapter import ItemAdapter
class TencentPipeline:
def __init__(self):
self.f = open("data.json", "wb")
def process_item(self, item, spider):
print("\n" + item)
self.f.write(
(json.dumps(dict(item), ensure_ascii=False) + ",\n").encode("utf-8")
)
return item
def close_spider(self, spider):
self.f.close()
腾讯招聘(抓包)
什么是抓包
在网络爬虫的上下文中,抓包技术可以被用来分析和优化爬虫的性能。具体来说,爬虫开发者可以使用抓包工具(如
Wireshark、tcpdump等)来捕获爬虫程序与服务器之间的通信数据包。通过对这些数据包的分析,开发者可以了解爬虫
请求的发送情况、服务器的响应情况,以及请求和响应中携带的具体数据内容。
分析请求
打开腾讯招聘页面,右键源代码,随便搜索一个网页中的职位名称,结果为空,说明,职位信息是动态响应到界面中的;
此时,就要到网络中去抓包,按F11打开调试工具,点到网络项,XHR,然后刷新界面,逐个检查发现Query
开头的请求返回的响应数据(预览)中有我们想要的数据.
复制请求url,在另一窗口打开,不断删除参数,最后发现,只需要简单的https://careers.tencent.com/tencentcareer/api/post/Query?pageIndex=281&pageSize=10
就能访问到数据,再将相隔两页对比数据,发现pageIndex的值就是当前页面的索引,pageSize固定为10(每页展示的招聘信息总数)
所以,我们试探性的将pageIndex改为1,果然发现获取到了第一页的招聘信息
爬取数据
把上面请求的响应数据放到json解析器中去,解析想要的数据所处的格式
编写代码
在spiders/tencent.py
中
import json
import scrapy
from Tencent.items import TencentItem
class TencentSpider(scrapy.Spider):
name = "tencent"
allowed_domains = ["tencent.com"]
index = 1
start_urls = [
f"http://careers.tencent.com/tencentcareer/api/post/Query?pageIndex={index}&pageSize=10"
]
def parse(self, response):
print("======================================") # 输出一行,让你更容易看到输出信息,后面写完项目可以注释掉
print(response.text) # 返回json数据
data_dict = response.json() # 把json数据变成python字典类型
RecruitPostName = data_dict["Data"]["Posts"][0]["RecruitPostName"] # 解嵌套
print(RecruitPostName)
print("======================================")
经过一系列操作,最后成功获得想要的第一个职位信息
最终代码为
tencent.py
import json
import scrapy
from Tencent.items import TencentItem
class TencentSpider(scrapy.Spider):
name = "tencent"
allowed_domains = ["tencent.com"]
index = 1
start_urls = [
f"http://careers.tencent.com/tencentcareer/api/post/Query?pageIndex={index}&pageSize=10"
]
def parse(self, response):
print(response.text)
data_dict = response.json()
datas = data_dict["Data"]["Posts"] # 返回一个数组
for data in datas:
item = TencentItem()
item["positionName"] = data["RecruitPostName"]
item["workLocation"] = data["LocationName"]
item["positionType"] = data["CategoryName"]
item["positionInfo"] = data["Responsibility"]
item["PostURL"] = data["PostURL"]
item["updateTime"] = data["LastUpdateTime"]
yield item
if self.index < 282:
self.index += 1
url = f"http://careers.tencent.com/tencentcareer/api/post/Query?pageIndex={self.index}&pageSize=10"
yield scrapy.Request(url=url, callback=self.parse)
# def parse_next(self,response):
# pass
items.py
# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html
import scrapy
class TencentItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
# 职位名
positionName = scrapy.Field()
# 职位类别
positionType = scrapy.Field()
# 职位详情url
PostURL = scrapy.Field()
# 工作地点
workLocation = scrapy.Field()
# 详细介绍
positionInfo = scrapy.Field()
# 最后更新时间
updateTime = scrapy.Field()
piplines.py
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
# useful for handling different item types with a single interface
import json
from itemadapter import ItemAdapter
class TencentPipeline:
def __init__(self):
self.f = open("data.json", "w")
def process_item(self, item, spider):
print("\n" + str(item))
x = self.f.write((json.dumps(dict(item), ensure_ascii=False) + ",\n"))
print(f"成功写入{x}个字符")
return item
# def close_spider(self, spider):
self.f.close()
运行结果展示
CrawlSpider
独门秘笈
继承自scrapy.Spider
独门秘笈
CrawlSpider可以定义规则,再解析html内容的时候,可以根据链接规则提取出指定的链接,然后再向这些链接发送请求所以,如果有需要跟进链接的需求,意思就是爬取了网页之后,需要提取链接再次爬取,使用CrawlSpider
是非常合适的
提取链接
链接提取器,在这里就可以写规则提取指定链接scrapy.linkextractors.LinkExtractor( # 正则表达式 提取符合正则的链接 allow = (), # (不用)正则表达式 不提取符合正则的链接 deny = (), #(不用)允许的域名 allow_domains = (), #(不用)不允许的域名 deny_domains = (), # xpath,提取符合xpath规则的链接 restrict_xpaths = (), # 提取符合选择器规则的链接 restrict_css = ()
模拟使用
- 正则用法:
links1 = LinkExtractor(allow=r'list 23 \d+\.html')
- xpath用法:
links2 = LinkExtractor(restrict xpaths=r'//div[@class="x"]')
- css用法:
links3 = LinkExtractor(restrict css='.x')
- 正则用法:
提取连接
link.extract_links(response)
注意事项
【注1】callback只能写函数名字符串,callback=’parse item’
【注2】在基本的spider中,如果重新发送请求,那里的callback写的是callback=self.parse_itemfollow=true 是否跟进 就是按照提取连接规则进行提取
CrawlSpider案例
read.py
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapy_dushuwang.items import ScrapyDushuwangItem
class ReadSpider(CrawlSpider):
name = "read"
allowed_domains = ["www.dushu.com"]
start_urls = ["https://www.dushu.com/book/1107.html"]
# 正则表达式r"/book/\d+\.html"
rules = (Rule(LinkExtractor(allow=r"/book/\d+\.html"),
callback="parse_item",
follow=False)) # follow是要不要继续提取
def parse_item(self, response):
print("======================")
# 第一张图片没有懒加载,所以要加入图片源
# img_list[0] = "https://a.dushu.com/img/n142.png"
img_list = response.xpath('//div[@class="bookslist"]/ul//img/@data-original')
name_list = response.xpath('//div[@class="bookslist"]/ul//a/@title')
for item in range(len(img_list)):
if item == 1:
name = name_list.extract_first()
src = "https://a.dushu.com/img/n142.png"
else:
name = name_list[item].extract()
src = img_list[item].extract()
book = ScrapyDushuwangItem(name=name, src=src)
yield book
# item = {} # 建议使用在items文件中定义数据结构
# # item["domain_id"] = response.xpath('//input[@id="sid"]/@value').get()
# # item["name"] = response.xpath('//div[@id="name"]').get()
# # item["description"] = response.xpath('//div[@id="description"]').get()
# return item
# 创建新的请求并重新执行提取
# next_page = response.xpath('//a[contains(text(), "下一页")]/@href').get() # 获取下一页的链接
# if next_page:
# yield scrapy.Request(next_page, callback=self.parse_item) # 重新执行提取,回调函数为 self.parse_item
items.py
# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html
import scrapy
class ScrapyDushuwangItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
name = scrapy.Field()
src = scrapy.Field()
piplines.py
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
# useful for handling different item types with a single interface
class ScrapyDushuwangPipeline:
def open_spider(self, spider):
self.fp = open('book.json', 'w', encoding='utf-8')
def process_item(self, item, spider):
self.fp.write(str(item))
return item
def close_spider(self, spider):
self.fp.close()
import pymysql
# 加载settings文件
from scrapy.utils.project import get_project_settings
class MySqlPipline:
def open_spider(self, spider):
settings = get_project_settings()
self.host = settings['DB_HOST']
self.port = settings['DB_PORT']
self.user = settings['DB_USER']
self.password = settings['DB_PASSWORD']
self.name = settings['DB_NAME']
self.charset = settings['DB_CHARSET']
self.connect()
def connect(self):
self.conn = pymysql.connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
db=self.name,
charset=self.charset
)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
sql = 'insert into book(name,src) values("{}","{}")'.format(item['name'], item['src'])
# 执行sql语句
self.cursor.execute(sql)
# 提交
self.conn.commit()
return item
def close_spider(self, spider):
self.cursor.close()
self.conn.close()
settings.py
# Scrapy settings for scrapy_dushuwang project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
# https://docs.scrapy.org/en/latest/topics/settings.html
# https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
BOT_NAME = "scrapy_dushuwang"
SPIDER_MODULES = ["scrapy_dushuwang.spiders"]
NEWSPIDER_MODULE = "scrapy_dushuwang.spiders"
# 连接mysql参数
DB_HOST = '127.0.0.1'
# DB_HOST = 'localhost'
DB_PORT = 3306
DB_USER = 'root'
DB_PASSWORD = 'Xubin159753123'
DB_NAME = "spider01"
# 注意utf-8的-不允许写
DB_CHARSET = 'utf8'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
# USER_AGENT = "scrapy_dushuwang (+http://www.yourdomain.com)"
# Obey robots.txt rules
ROBOTSTXT_OBEY = True
# Configure maximum concurrent requests performed by Scrapy (default: 16)
# CONCURRENT_REQUESTS = 32
# Configure a delay for requests for the same website (default: 0)
# See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
# DOWNLOAD_DELAY = 3
# The download delay setting will honor only one of:
# CONCURRENT_REQUESTS_PER_DOMAIN = 16
# CONCURRENT_REQUESTS_PER_IP = 16
# Disable cookies (enabled by default)
# COOKIES_ENABLED = False
# Disable Telnet Console (enabled by default)
# TELNETCONSOLE_ENABLED = False
# Override the default request headers:
# DEFAULT_REQUEST_HEADERS = {
# "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
# "Accept-Language": "en",
# }
# Enable or disable spider middlewares
# See https://docs.scrapy.org/en/latest/topics/spider-middleware.html
# SPIDER_MIDDLEWARES = {
# "scrapy_dushuwang.middlewares.ScrapyDushuwangSpiderMiddleware": 543,
# }
# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
# DOWNLOADER_MIDDLEWARES = {
# "scrapy_dushuwang.middlewares.ScrapyDushuwangDownloaderMiddleware": 543,
# }
# Enable or disable extensions
# See https://docs.scrapy.org/en/latest/topics/extensions.html
# EXTENSIONS = {
# "scrapy.extensions.telnet.TelnetConsole": None,
# }
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
"scrapy_dushuwang.pipelines.ScrapyDushuwangPipeline": 300,
# MySqlPipline:"
"scrapy_dushuwang.pipelines.MySqlPipline": 301
}
# Enable and configure the AutoThrottle extension (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/autothrottle.html
# AUTOTHROTTLE_ENABLED = True
# The initial download delay
# AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
# AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
# AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
# AUTOTHROTTLE_DEBUG = False
# Enable and configure HTTP caching (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
# HTTPCACHE_ENABLED = True
# HTTPCACHE_EXPIRATION_SECS = 0
# HTTPCACHE_DIR = "httpcache"
# HTTPCACHE_IGNORE_HTTP_CODES = []
# HTTPCACHE_STORAGE = "scrapy.extensions.httpcache.FilesystemCacheStorage"
# Set settings whose default value is deprecated to a future-proof value
REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
FEED_EXPORT_ENCODING = "utf-8"
日志信息和日志等级
日志级别:
CRITICAL: 严重错误
ERROR: 一般错误
WARNING: 警告
INFO: 一般信息
DEBUG: 调试信息
默认的日志等级是DEBUG,只要出现了DEBUG或者DEBUG以上等级的日志,那么这些日志将会打印
settings.py
文件设置:默认的级别为 DEBUG,会显示上面所有的信息
在配置文件中settings.py
LOG FILE: 将屏幕显示的信息全部记录到文件中,屏幕不再显示,注意文件后缀一定是
.log
LOG LEVEL: 设置日志显示的等级,就是显示哪些,不显示哪些
scrapy的post请求
- scrapy中post请求是通过
spider/你定义的爬虫程序名.py
的start_request()
方法实现的
# post请求 如果没有参数 那么这个请求将没有任何意义
# 所以start urls 也没有用了
# parse方法也没有用了
# start_urls = ['https://fanyi.baidu.com/sug/']
# def parse(self, response):
# pass
- 重写start_requests方法:
def start_requests(self):
url ='https://fanyi.baidu.com/sug
data={
'kw':'final'}
yield scrapy.FormRequest(url=url,formdata=data,callback=self.parse_second)
def parse_second(self):
pass
- start requests的返回值:
scrapy.FormRequest(url=url.headers=headers, callback=self.parse_second, formdata=data)
- url: 要发送的post地址
- headers: 可以定制头信息
- callback: 回调函数
- formdata: post所携带的数据,这是一个字典
代理
到settings.py中,打开一个选项DOWNLOADER MIDDLEWARES={postproject.middlewares.Proxy':543,}
到middlewares.py中写代码
def process_request(self, request, spider):
request.meta['proxy']='https://113.68.202.10:9999'
return None