scrapy框架爬虫
scrapy框架爬虫简介
安装scrapy
windows系统在命令行模式下输入pip install scrapy
命令即可。
建立scrapy项目
- 进入命令行窗体,在D盘中建立一个文件夹,例如example,进入d:\example然后执行命令:
scrapy startproject demo
,该命令建立一个名称为demo的scrapy项目。 - scrapy项目建立后会在d:\demo中建立demo文件夹,同时下面还有另外一个demo子文件夹,有一些必要的项目文件。
- 为了测试这个scrapy项目,先建立一个web网站,可以在d:\example中建立一个
server.py
程序,如下:
import flask
app=flask.Flask(__name__)
@app.route("/")
def index():
return "测试scrapy"
if __name__ == "__main__":
app.run()
- 在d:\example\demo\demo\spider文件夹中建立一个自己的python文件,例如
MySpider.py
,程序如下:
import scrapy
class MySpider(scrapy.Spider):
name="mySpider"
def start_request(self):
url = "http://127.0.0.1:5000"
yield scrapy.Request(url=url,callback=self.parse)
def parse(self,response):
print(response.url)
data=response.body.decode()
print(data)
- 在d:\example\demo\demo文件夹中建立一个执行程序如
run.py
,程序如下:
from scrapy import cmdline
cmdline.execute("scrapy crawl mySpider -s LOG_ENABLED=False".split())
- 保存这些程序并运行
run.py
,可以看到结果:
http://127.0.0.1:5000
测试scrapy
由此可见程序MySpider.py是访问了我们自己的Web网站并爬取了网站的网页。下面来分析这个程序:
(1). import scrapy
引入scrapy程序包,这个包中有一个请求对象Request与一个响应对象Response类。
(2).
class MySpider(scrapy.Spider):
name = "mySpider"
任何一个爬虫程序类都继承与scrapy.Spider
类,任何一个爬虫程序都有一个名字,这个名字在整个爬虫项目是唯一的,此项目中的爬虫名字为“mySpider”。
(3)
def start_request(self):
url = "http://127.0.0.1:5000"
yield scrapy.Request(url=url,callback=self.parse)
这个地址url是爬虫程序的入口地址,这个start_request函数是程序的入口函数。程序开始时确定要爬取的网站地址,然后建立一个scrapy.Request请求类,向这个类提供url参数,指明要爬取的网页地址。爬取网页完成后就执行默认的回调函数parse。
scrapy的执行过程是异步进行的,即指定一个url网址开始爬取数据时,程序不用一直等待这个网站的响应,如果网站迟迟不响应,那么整个程序就卡死了。但是scrapy提供一个回调函数,网站什么时候响应就什么时候调用这个回调函数,这样有利于对响应时间很长的网站的爬取。
(4)
def parse(self,response):
print(response.url)
data=response.body.decode()
print(data)
回调函数parse包含一个scrapy.Request类的对象response,它是网站响应的一切信息,其中response.url就是网站的网址,response.body是网站响应的二进制数据,即网页的内容。通过decode()解码后变成字符串,就可以用print输出了。
(5) run.py
的作用
程序MySpider.py
只是一个类,不能单独执行,要执行这个爬虫程序就必须使用scrapy中专门的命令scrapy crawl。可以在命令窗口,在d:\example\demo\demo中执行命令:scrapy crawl mySpider -s LOG_ENABLED=False
,那么就可以看到执行后的结果。注:其中mySpider
指爬虫程序的名称,后面的参数是不现实调试信息。但是由于窗口的切换比较繁琐,因此设计一个python的run.py
,专门用来执行MySpider.py
程序。
入口函数与入口地址
在程序中使用了入口函数:
def start_request(self):
url = "http://127.0.0.1:5000"
yield scrapy.Request(url=url,callback=self.parse)
实际上这个函数可以用start_urls的入口地址来代替:
start_urls = "http://127.0.0.1:5000"
入口地址可有多个,因此start_urls是一个列表。入口函数与入口地址的作用是一样的。
Python的yield语句
- 在入口函数,有一条yield语句,主要作用是返回一个值等待被取走。
- 实例:
def fun():
s = ['a','b','c']
for x in s:
yield x
print("fun End")
f = fun()
print(f)
for e in f:
print(e)
程序结果:
<generator object fun at 0x0000003EA99BD728>
a
b
c
fun End
由此可见,fun返回一个generator的对象,这种对象包含一系列的元素,可以使用for循环提取,执行循环的过程如下:
for e in f:
print(e)
- 第一次 for e in f 循环,f执行到yield语句,就返回一个值’a’,for循环从f抽取的元素是’a’,然后e=‘a’,打印a。fun中执行到yield时会等待yield返回的值被抽走,同时fun停留在yield语句,一旦被抽走,再次循环,yield返回’b’。
- 第二次循环 for e in f 循环,抽取f函数的’b’元素,打印出’b’,然后f中循环继续yield返回’c’元素。
- 第三次循环 for e in f 循环,抽取f函数的’c元素,打印出’c’,然后f中循环就结束了,显示fun End,随后for e in f循环中f也没有元素可以继续抽走了,也结束,随后程序结束。
- 只要包含yield语句的函数都返回一个generator的可循环对象,执行到yield语句时只是返回一个值,等待调用循环抽取,一旦调用循环抽取后,函数又继续进行。这个过程非常类似两个线程的协助过程,当提供数据的一方准备好数据把yield提交数据时,就等待另外一方把数据抽取走,如果不抽走yield就一直等待,一旦抽走后数据提供方继续它的程序,一直等到出现下一次yield或者程序结束。scrapy的框架使用的是异步执行的过程,因此大量使用yield语句。
Scrapy中查找HTML元素
Scrapy中查找HTML元素(1)
- 在scrapy中有强大的查找HTML元素的功能,那就是使用xpath方法。xpath方法使用xPath语法,比BeautifulSoup的select要灵活而且速度快。
- 使用xpath查找HTML中的元素
#从scrapy中引入Selector类,这个类就是选择查找类
from scrapy.selector import Selector
htmlText='''
<html><body>
<bookstore>
<book>
<title lang="eng">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title lang="eng">Learning XML</title>
<price>39.99</price>
</book>
</bookstore>
</body></html>
'''
selector=Selector(text=htmlText)
#查看selector的类型
print(type(selector))
print(selector)
#在文档中查找所有的<title>元素,其中“//”表示文档中的任何位置。
s=selector.xpath("//title")
print(type(s))
print(s)
程序结果:
<class 'scrapy.selector.unified.Selector'>
<Selector xpath=None data=u'<html><body>\n<bookstore>\n<book>\n\t<title '>
<class 'scrapy.selector.unified.SelectorList'>
[<Selector xpath='//title' data=u'<title lang="eng">Harry Potter</title>'>, <Selector xpath='//title' data=u'<title lang="eng">Learning XML</title>'>]
(1).selector=Selector(text=htmlText)
使用htmlText的文字建立Selector类,就是装载HTML文档,文档装载后就形成一个Selector对象,就可以使用xpath查找元素。
(2). selector.xpath("//tagName")
表示在文档中搜索<tagName>
的tags,形成一个Selector的列表。
-
由此可见一般selector搜索一个的HTML元素的方法是:
selector.xpath("//tagName")
-
在装载HTML文档后
selector=Selector(text=htmlText)
得到的selector是对应全文档顶层的元素<html>
的,其中"//"表示全文档搜索,结果是一个Selector的列表,哪怕只有一个元素也成一个列表。 -
selector.xpath("//body")搜索到元素,结果是一个Selector的列表,包含一个Selector元素;
-
selector.xpath("//title")
搜索到两个<title>
元素,结果是Selector的列表,包含2个Selector元素; -
selector.xpath("//book")
搜索到两个元素,结果是Selector的列表,包含2个Selector元素;
Scrapy中查找HTML元素(2)
- 使用“//”表示文档下面的所有节点元素,用“/”表示当前节点的下一级节点元素。
- “//”与“/”的使用
selector.xpath("//bookstore/book")
搜索<bookstore>
下一级的<book>
元素,找到2个;selector.xpath("//body/book")
搜索<body>
的下一级<book>
元素,结果为空;selector.xpath("//body//book")
搜索<body>
下的<book>
元素,找到2个;selector.xpath("/body//book")
搜索文档下一级的<body>
下的<book>
元素,找结果为空,因为文档的下一级是<html>
元素,而不是<body>
;selector.xpath("/html/body//book")
或者selector.xpath("/html//book")
搜索<book>
元素,找到2个;selector.xpath("//book/title")
搜索文档中所有<book>
下一级的<title>
元素,找到2个,结果跟selector.xpath("//title")
、selector.xpath("//bookstore//title")
一样;selector.xpath("//book//price")
与selector.xpath("//price")
结果一样,都是找到2个<price>
元素。
- 使用"."表示当前节点元素,使用xpath可以连续调用,如果前一个xpath返回一个Selector的列表,那么这个列表可以继续调用xpath,功能是为每个列表元素调用xpath,最后结果是全部元素调用xpath的汇总。
- 使用"."进行xpath连续调用
from scrapy.selector import Selector
htmlText='''
<html><body>
<bookstore>
<book>
<title>Novel</title>
<title lang="eng">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title>TextBook</title>
<title lang="eng">Learning XML</title>
<price>39.99</price>
</book>
</bookstore>
</body></html>
'''
selector=Selector(text=htmlText)
s=selector.xpath("//book").xpath("./title")
for e in s:
print(e)
程序结果:
<Selector xpath='./title' data=u'<title>Novel</title>'>
<Selector xpath='./title' data=u'<title lang="eng">Harry Potter</title>'>
<Selector xpath='./title' data=u'<title>TextBook</title>'>
<Selector xpath='./title' data=u'<title lang="eng">Learning XML</title>'>
selector.xpath("//book")
首先搜索到文档中所有<book>
元素,总共有2个,然后再次调用xpath("./title")
,就是从当前元素<book>
开始往下一级搜索<title>
,每个<book>
都找到2个<title>
,因此结果有4个<title>
。- 注意如果
xpath
连续调用时不指定是从前一个xpath
的结果元素开始的,那么默认是从全文档开始的,结果就会不一样,例如:s=selector.xpath("//book").xpath("/title")
结果就是空的,因为后面的xpath("/title")
从文档开始搜索<title>
。s=selector.xpath("//book").xpath("//title")
结果有10个元素,因为每个<book>
都驱动xpath<"//title">
在全文档搜索<title>
元素,每次都搜索到5个元素。
Scrapy中查找HTML元素(3)
-
如果xpath返回的是一个Selector对象列表:
- 调用extract()函数会得到这些对象的元素文本的列表
- 使用extract_first()获取列表中第一个元素值,如果列表为空,extract_first()的值为None。
-
xpath返回的是一个单一的Selector对象:
- 调用extract()函数就可以得到Selector对象对应的元素的文本值。
- 单一的Selector对象没有extract_first()函数。
from scrapy.selector import Selector
htmlText='''
<html>
<body>
<bookstore>
<book id="b1">
<title lang="english">Harry Potter</title>
<price>29.99</price>
</book>
<book id="b2">
<title lang="chinese">learning-XML</title>
<price>39.98</price>
</book>
</bookstore>
</html>
'''
selector=Selector(text=htmlText)
s=selector.xpath("//book/price")
print(type(s),s)
s=selector.xpath("//book/price").extract()
print(type(s),s)
s=selector.xpath("//book/price").extract_first()
print(type(s),s)
程序结果:
<class 'scrapy.selector.unified.SelectorList'> [<Selector xpath='//book/price' data='<price>29.99</price>'>, <Selector xpath='//book/price' data='<price>39.98</price>'>]
<class 'list'> ['<price>29.99</price>', '<price>39.98</price>']
<class 'str'> <price>29.99</price>
由此可见:s=selector.xpath("//book/price")
得到的是SelectorList列表;
s=selector.xpath("//book/price").extract()
得到的是<price>
元素的Selector对象对应的<price>
元素的文本组成的列表,即[<price>29.99</price>', u'<price>39.98</price>]
;s=selector.xpath("//book/price").extract_first()
得到的是<price>
元素的文本组成的列表的第一个元素,是一个文本,即<price>29.99</price>
。
- xpath使用"/@attrName"得到一个Selector元素的attrName属性节点对象,属性节点对象也是一个Selector对象,通过extract()函数获取其属性值。
- 对上述的doc文档获取元素属性值:
selector=Selector(text=htmlText)
s=selector.xpath("//book/@id")
print(s)
print(s.extract())
for e in s:
print(e.extract())
程序结果:
[<Selector xpath='//book/@id' data='b1'>, <Selector xpath='//book/@id' data='b2'>]
['b1', 'b2']
b1
b2
由此可见:s=selector.xpath("//book/@id")
结果是2个<book>
的id属性组成的SelectorList列表,即属性也是一个Selector对象;print(s.extract())
结果是的id属性的两个Selector对象的属性文本值的列表,即['b1','b2']
;e.extract()
获取对象e的属性值。
- xpath使用"/text()"得到一个Selector元素包含的文本值,文本值节点对象也是一个Selector对象,通过extract()获取文本值。
- 获取上述doc文档的节点文本值
selector=Selector(text=htmlText)
s=selector.xpath("//book/title/text()")
print(s)
print(s.extract())
for e in s:
print(e.extract())
程序结果:
[<Selector xpath='//book/title/text()' data='Harry Potter'>,<Selector xpath='//book/title/text()' data='Learning-XML'>]
['Harry Potter','Learning-XML']
Harry Potter
Learning-XML
由此可见:s=selector.xpath("//book/title/text()")
结果也是SelectorList列表,即文本也是一个节点;print(s.extract())
结果是文本节点的字符串值的列表,即['Harry Potter','Learning-XML']
;最后的for语句,每个e是一个Selector对象,因此extract()获取对象的属性值。值得注意的是如果一个element的元素包含的文本不是单一的文本,那么可能会产生多个文本值。
- xpath使用
"tag[condition]"
来限定一个tag元素,其中condition是由这个tag的属性、文本等计算出的一个逻辑值。如果有多个条件,可以写成:"tag[condition1][condition2]...[conditionN]"
或者"tag[condition1] and [condition2] and ... and [conditionN]"
。 - xpath可以使用position()来确定其中一个元素的限制,这个选择序号是从1开始的,不是从0开始编号的,还可以通过and、or等构造复杂表达式。
- xpath使用星号
"*"
代表任何Element节点,不包括Text、Comment的节点。 - xpath使用
"@*"
代表任何属性。 - xpath使用
"element/parent::*"
选择element的父节点,这个节点只有一个。如果写成"element/parent::tag"
,就是指定element的tag父节点,除非element的父节点正好是tag节点,不然就为None。 - xpath使用
"element/following-sibling::*"
搜索element后面的同级的所有兄弟节点,使用"element/following-sibling::*[position()=1]"
搜索element后面的同级的第一个兄弟节点。 - xpath使用
"element/preceding-sibling::*"
搜索element前面的同级的所有兄弟节点,使用"element/preceding-sibling::*[position()=1]"
搜索element前面的同级的第一个兄弟节点。
Scrapy爬取与存储数据
建立Web网站
使用scrapy startproject demo
语句创建一个demo的scrapy项目,在项目中创建一个server.py
文件,用来编写网站内容,这个网站有一个网页,返回基本计算机教材,flask程序如下:
import flask
app = flask.Flask(__name__)
@app.route("/")
def index():
html="""
<books>
<book>
<title>Python程序设计</title>
<author>James</author>
<publisher>人民邮电出版社</publisher>
</book>
<book>
<title>Java程序设计</title>
<author>Robert</author>
<publisher>人民邮电出版社</publisher>
</book>
<book>
<title>MySQL数据库</title>
<author>Steven</author>
<publisher>高等教育出版社</publisher>
</book>
</books>
"""
return html
if __name__=="__main__":
app.run()
编写数据项目类
程序爬取的数据是多本教材,每本教材有名称与作者,因此要建立一个教材的类,类中包括教材名称title,作者author与出版社publisher。在demo\demo目录下有一个items.py文件就是用来设计数据项目类的,打开文件,并改成如下形式:
import scrapy
class BookItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
author = scrapy.Field()
publisher = scrapy.Field()
其中BookItem是我们设计的教材项目类,这个类必须从scrapy.Itrm类继承,在类中定义教材的字段项目,每个字段项目都是一个scrapy.Field对象,定义了3个字段项目,分别用来存储title、author以及publisher。注:可以通过item[“title”]、item[“author”]、item[“publisher”]来获取或设置各个字段的值。
编写爬虫程序myspider
在demo\demo\spider目录下新建myspider.py
文件,编写程序如下:
import scrapy
from scrapy import Selector
#从demo文件夹的items.py文件中引入BookItem类的定义
from demo.items import BookItem
class MySpider(scrapy.Spider):
name = "mySpider"
start_urls = ["http://127.0.0.1:5000"]
def parse(self, response):
try:
#得到网站数据并建立Selector对象,搜索到所有的<book>节点的元素
data = response.body.decode()
selector = Selector(text=data)
books = selector.xpath("//book")
for book in books:
item = BookItem()
#对于每个<book>节点,在它的下面搜索<title>、<author>、<publisher>节点,并取出它们的文本,组成一个BookItem对象
item['title'] = book.xpath("./title/text()").extract_first()
item['author'] = book.xpath("./author/text()").extract_first()
item['publisher'] = book.xpath("./publisher/text()").extract_first()
#向上一级调用函数返回
yield item
#向上一级调用函数返回,scrapy把BookItem对象推送给与items.py同目录下的pipeline.py文件中的数据管道执行类取处理数据
except Exception as e:
print(e)
编写数据管道处理类
在scrapy框架中的demo\demo目录下有一个pipeline.py文件,用来数据管道处理文件,打开这个文件可以看到一个默认的管道类,修改并设计管道程序如下:
class BookPipeline(object):
count=0
def process_item(self, item, spider):
BookPipeline.count+=1
try:
if BookPipeline.count==1:
fobj=open("books.txt","wt")
else:
fobj=open("books.txt","at")
print(item["title"],item["author"],item["publisher"])
fobj.write(item["title"]+","+item["author"]+","+item["publisher"]+"\n")
fobj.close()
except Exception as e:
print(e)
return item
说明:这个类,命名为BookPipeline,它继承自object类,类中最重要的函数是process_item函数,scrapy爬取数据开始会建立一个BookPipeline类对象,然后每爬取一个数据类BookItem项目item,mySpider程序就会把这个对象推送给BookPipeline对象,同时调用process_item函数一次。process_item函数的参数中的item就是推送来的数据,于是就可以在这个函数中保存完整的爬取数据了。
设置scrapy的配置文件
mySpider程序执行后每爬取一个item项目是如何推送给BookPipeline对象并调用process_item函数的呢?前提是必须设置一个通道。在demo文件夹中一个settings.py的设置文件,打开这个文件可以看到很多设置项目,大部分都是用#注释的语句,找到 语句ITEM_PIPLINES的项目,把它设置如下形式:
# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'demo.pipelines.BookPipeline': 300,
}
其中ITEM_PIPLINES是一个字典,把关键字改成 ‘demo.pipelines.BookPipeline’,而BookPipelines就是在pipelines.py文件中设计的数据管道类的名称,后面的300是一个默认的整数,可以是任何整数。这样设置后就可以连通爬虫mySpider程序通过yield返回的每项数据推送给pipelines.py程序的BookPipeline类,并执行process_item函数了。
运行
在demo中创建run.py文件,内容见上。运行run.py可得到一个book.txt文件。打开文件,则可见爬取存储数据是否成功。
编写Scrapy爬虫程序
建立Web网站
- books.html
<h3>计算机</h3>
<ul>
<li><a href="database.html">数据库</a> </li>
<li><a href="program.html">程序控制</a> </li>
<li><a href="network.html">计算机网络</a> </li>
</ul>
- database.html
<h3>数据库</h3>
<ul>
<li><a href="mysql.html">MySQL数据库</a> </li>
</ul>
<a href="books.html">返回首页</a>
- program.html
<h3>程序设计</h3>
<ul>
<li><a href="python.html">Python程序设计</a> </li>
<li><a href="java.html">Java程序设计</a> </li>
</ul>
<a href="books.html">返回首页</a>
- network.html
<h3>计算机网络</h3>
<a href="books.html">返回首页</a>
- mysql.html
<h3>MySQL数据库</h3>
<a href="books.html">返回首页</a>
- python.html
<h3>Python程序设计</h3>
<a href="books.html">返回首页</a>
- java.html
<h3>Java程序设计</h3>
<a href="books.html">返回首页</a>
- Web服务器程序
flask.py
import flask
import os
app = flask.Flask(__name__)
def getFile(fileName):
data = b""
if os.path.exists(fileName):
fobj = open(fileName,"rb")
data = fobj.read()
fobj.close()
return data
@app.route("/")
def index():
return getFile("books.html")
@app.route("/<section>")
def process(section):
data = ""
if section != "":
data = getFile(section)
return data
if __name__ == "__main__":
app.run()
编写scrapy爬虫程序
程序如下
import scrapy
from scrapy import Selector
class MySpider(scrapy.Spider):
name = "mySpider"
#入口地址,访问这个网址成功后会回调parse函数
start_urls = ["http://127.0.0.1:5000"]
#定义一个回调函数,该函数的response对象包含了网站返回的信息
def parse(self, response):
try:
print(response.url)
#网站返回的response.body的二进制函数,要decode转为文本,然后建立Selector对象
data = response.body.decode()
selector = Selector(text=data)
#获取网页中的<h3>标题的文本
print(selector.xpath("//h3/text()").extract_first())
#获取所有的<a href=...>链接的href值,组成links列表
links = selector.xpath("//a/@href").extract()
for link in links:
url = response.urljoin(link)
yield scrapy.Request(url,callback=self.parse)
except Exception as e:
print(e)
运行run.py程序,得到如下结果:
http://127.0.0.1:5000
计算机
http://127.0.0.1:5000/database.html
数据库
http://127.0.0.1:5000/network.html
计算机网络
http://127.0.0.1:5000/program.html
程序设计
http://127.0.0.1:5000/books.html
计算机
http://127.0.0.1:5000/mysql.html
MySQL数据库
http://127.0.0.1:5000/python.html
Python程序设计
http://127.0.0.1:5000/java.html
Java程序设计
说明:scrapy会自动筛选已经访问过的网站,访问links的每个link,通过urljoin函数与response.url地址组合成完整的url地址,再次建立Resquest对象,回调函数仍然为parse,即这个parse函数会被递归调用。其中使用了yield语句返回每个Request对象。