前序
本来说好随缘更新的,因为想去写测试相关的文档,但想想,既然第一篇爬虫文章都发布吧,就继续完善吧~
上一章回顾:JB的Python之旅-爬虫篇--urllib和Beautiful Soup
看回之前写的爬虫计划:
关于后续爬虫的计划:
目前还处于初级的定向脚本编写,本文内容主要介绍requests库跟Scrapy框架;
计划下一篇是Selenium,再下一篇是分布式爬虫,后面加点实战,再看看怎么更新吧~
本文介绍:
上一篇主要介绍了urllib 跟 BeautifuSoap,练手的项目有小说网站及百度图片的爬取,对于日常工作来说,感觉是够了~
但只要有了解过爬虫,肯定听过requests跟Scrapy,这两个又是什么?
简单介绍,requests是一个第三方库,正如其名,就是处理请求相关内容,比起Python自带的urllib库,用起来相对的方便;
Scrapy呢,度娘给的介绍是:
Python开发的一个快速、高层次的屏幕抓取和web抓取框架,用于抓取web站点并从页面中提取结构化的数据;
Scrapy吸引人的地方在于它是一个框架,任何人都可以根据需求方便的修改。它也提供了多种类型爬虫的基类,如BaseSpider、sitemap爬虫等;
复制代码
那,request是跟scrapy区别在哪里?
从使用层面来说,单纯爬取一个网页的话,两者区别不大,本质是一样的,但scrapy是一个专业爬虫框架,假如需要派去1W个网站的时候,差异就出现了,需要做并发,监控存储,scrapy是可以做,但request这块貌似做不到;
从代码层面,scrapy里面可以使用requests的内容,会有较多二次封装,在使用上更加方便~
更多的内容就度娘吧,这里不展开了,留个大概印象就行了~
另外,本文还会使用另外一个网页解析方法--xpath,上篇文章我们学会了用beautifulsoup,那为什么要用xpath?
beautifulsoup是先把整个网页结构解析,然后再查找相关内容,而xpath是直接查找,不需要做额外的解析操作,
因此可以得出,在性能效率上,xpath是灰常灰常的快的~
requests库
本小节主要介绍下requests的库常见使用,以及会介绍一个内容实战~
1.简介
官方中文文档
Requests 是用Python语言编写,基于 urllib,采用 Apache2 Licensed 开源协议的 HTTP 库。
它比 urllib 更加方便,可以节约我们大量的工作,
看了下官方文档,有以下特性:
Keep-Alive & 连接池
国际化域名和 URL
带持久 Cookie 的会话
浏览器式的 SSL 认证
自动内容解码
基本/摘要式的身份认证
优雅的 key/value Cookie
自动解压
Unicode 响应体
HTTP(S) 代理支持
文件分块上传
流下载
连接超时
分块请求
支持 .netrc
Requests 支持 Python 2.6—2.7以及3.3—3.7,而且能在 PyPy 下完美运行。
看到就会觉得很厉害了,不明觉厉啊~
万事开头难,先安装走一波把 pip install requests,安装完就可以用了~
2.教程
直接上例子:
发送请求
import requests
r = requests.get(url='http://www.baidu.com') # 最基本的GET请求
r = requests.get(url='http://xxct.baidu.com/s', params={'wd':'python'}) #带参数的GET请求
r = requests.post(url='http://xx', data={'wd':'python'}) #带body的POST请求
r = requests.post('https://x', data=json.dumps({'wd': 'python'})) #带JSON的POST
#其他请求方式的用法
requests.get(‘https://XX’) #GET请求
requests.post(“http://XX”) #POST请求
requests.put(“http://XX”) #PUT请求
requests.delete(“http://XX”) #DELETE请求
requests.head(“http://XX”) #HEAD请求
requests.options(“http://XX”) #OPTIONS请求
复制代码
响应内容
请求返回的值是一个Response 对象,Response 对象是对 HTTP 协议中服务端返回给浏览器的响应数据的封装,响应的中的主要元素包括:状态码、原因短语、响应首部、响应体等等,这些属性都封装在Response 对象中。
# 状态码
response.status_code
200
# 原因短语
response.reason
'OK'
# 响应首部
for name,value in response.headers.items():
print("%s:%s" % (name, value))
Content-Encoding:gzip
Server:nginx/1.10.2
Date:Thu, 06 Apr 2017 16:28:01 GMT
# 响应内容
response.content
复制代码
查询参数
很多URL都带有很长一串参数,我们称这些参数为URL的查询参数,用"?"附加在URL链接后面,多个参数之间用"&"隔开,比如:http://www.baidu.com/?wd=python&key=20 ,现在你可以用字典来构建查询参数:
args = {"wd": python, "time": 20}
response = requests.get("http://www.baidu.com", params = args)
response.url
'http://www.baidu.com/?wd=python&time=20'
复制代码
请求首部
requests 可以很简单地指定请求首部字段 Headers,比如有时要指定 User-Agent 伪装成浏览器发送请求,以此来蒙骗服务器。直接传递一个字典对象给参数 headers 即可。
r = requests.get(url, headers={'user-agent': 'Mozilla/5.0'})
复制代码
请求体
requests 可以非常灵活地构建 POST 请求需要的数据,
如果服务器要求发送的数据是表单数据,则可以指定关键字参数 data,
如果要求传递 json 格式字符串参数,则可以使用json关键字参数,参数的值都可以字典的形式传过去。
作为表单数据传输给服务器
payload = {'key1': 'value1', 'key2': 'value2'}
r = requests.post("http://www.baidu.com", data=payload)
复制代码
作为 json 格式的字符串格式传输给服务器
import json
url = 'http://www.baidu.com'
payload = {'some': 'data'}
r = requests.post(url, json=payload)
复制代码
响应内容
响应体在 requests 中处理非常灵活,
与响应体相关的属性有:content、text、json()。
content 是 byte 类型,适合直接将内容保存到文件系统或者传输到网络中
r = requests.get("https://pic1.zhimg.com/v2-2e92ebadb4a967829dcd7d05908ccab0_b.jpg")
type(r.content)
# 另存为 test.jpg
with open("test.jpg", "wb") as f:
f.write(r.content)
复制代码
text 是 str 类型,比如一个普通的 HTML 页面,需要对文本进一步分析时,使用 text。
r = requests.get("https://www.baidu.com")
type(r.text)
re.compile('xxx').findall(r.text)
复制代码
如果使用第三方开放平台或者API接口爬取数据时,返回的内容是json格式的数据时,那么可以直接使用json()方法返回一个经过json.loads()处理后的对象。
>>> r = requests.get('https://www.baidu.com')
>>> r.json()
复制代码
代理设置
当爬虫频繁地对服务器进行抓取内容时,很容易被服务器屏蔽掉,因此要想继续顺利的进行爬取数据,使用代理是明智的选择。如果你想爬取墙外的数据,同样设置代理可以解决问题,requests 完美支持代理。
import requests
proxies = {
'http': 'http://XX',
'https': 'https://XX',
}
requests.get('https://foofish.net', proxies=proxies, timeout=5)
复制代码
超时设置
requests 发送请求时,默认请求下线程一直阻塞,直到有响应返回才处理后面的逻辑。
如果遇到服务器没有响应的情况时,问题就变得很严重了,它将导致整个应用程序一直处于阻塞状态而没法处理其他请求。 r = requests.get("http://www.google.coma", timeout=5)
3.实战1 爬取青春妹子网站
这次爬取的网站是:http://www.mmjpg.com/,直接F12看了下,所有的信息都集中在ul区块下的img区块里:
分析下网页的结构,基本是,这样的:
1)每一页会有15个图集
2)每一个图集里有N张图片
而我们需要的就是下载图片,那就是需要先获取网页分页->图集分页->图片下载页,爬取的思路:
1)获取当前网址的图集地址
2)获取当前网站的下载地址
3)下载图片
先以首页为例子,获取首页的所有图集地址:
import requests
from lxml import html
def Get_Page_Number():
url = 'http://www.mmjpg.com'
response = requests.get(url).content
#调用requests库,获取二进制的相应内容。
#注意,这里使用.text方法的话,下面的html解析会报错.这里涉及到.content和.text的区别了。简单说,如果是处理文字、链接等内容,建议使用.text,处理视频、音频、图片等二进制内容,建议使用.content。
selector = html.fromstring(response)
#使用lxml.html模块构建选择器,主要功能是将二进制的服务器相应内容response转化为可读取的元素树(element tree)。lxml中就有etree模块,是构建元素树用的。如果是将html字符串转化为可读取的元素树,就建议使用lxml.html.fromstring,毕竟这几个名字应该能大致说明功能了吧。
urls = []
#准备容器
for i in selector.xpath("//ul/li/a/@href"):
#利用xpath定位到所有的套图的详细地址
urls.append(i)
#遍历所有地址,添加到容器中
return urls
复制代码
这里的urls,就是图集的地址,接下来就是获取套图地址的标题
这里有同学有疑问,为什么用xpatch,而不是用beautifulsoup?
单一例子来首,两者都是用来解析网页的,差别不大,主要是想熟悉了解下,关于xpath的内容,scrapy下面有详细介绍~
def Get_Image_Title():
# 现在进入到套图的详情页面了,现在要把套图的标题和图片总数提取出来
url = "http://www.mmjpg.com/mm/1367"
response = requests.get(url).content
selector = html.fromstring(response)
image_title = selector.xpath("//div[@class='article']/h2/text()")[0]
# 需要注意的是,xpath返回的结果都是序列,所以需要使用[0]进行定位
return image_title
复制代码
接下来获取图片的数量:
同样的套图页,直接点位到坐标处,会发现a标签的倒数第二个区块就是图集的最后一页,直接取数就行,代码如下:def Get_Image_Count():
url = "http://www.mmjpg.com/mm/1367"
response = requests.get(url).content
selector = html.fromstring(response)
image_count = selector.xpath("//div[@class='page']/a[last()-1]/text()")[0]
return image_count
复制代码
接下来就是获取图片的下载地址:
并且点击不同页码的图片,变化的是最后一位: http://www.mmjpg.com/mm/1365/XX因此想获取图集下的所有图片链接,就是先获取有多少页(上面的方法就可以啦),然后找到img获取下载链接,代码如下:
def Get_Image_Url():
url = "http://www.mmjpg.com/mm/1367"
response = requests.get(url).content
selector = html.fromstring(response)
image_links = []
image_aount = selector.xpath("//div[@class='page']/a[last()-1]/text()")[0]
for i in range(int(image_aount)):
image_url = "http://www.mmjpg.com/mm/1367/"+str(i+1)
response = requests.get(image_url).content
sel = html.fromstring(response)
image_download_link = sel.xpath("//div[@class='content']/a/img/@src")[0]
# 这里是单张图片的最终下载地址
image_links.append(str(image_download_link))
return image_links
复制代码
最后,就是下载文件啦~
def Download_Image(image_title,image_links):
num = 1
amount = len(image_links)
for i in image_links:
filename = "%s%s.jpg" % (image_title,num)
print('正在下载图片:%s第%s/%s张,' % (image_title, num, amount))
#用于在cmd窗口上输出提示,感觉可以增加一个容错函数,没想好怎么写
urllib.request.urlretrieve(requests.get(i,headers=header), "%s%s%s.jpg" % (dir, image_title, num))
num += 1
复制代码
跑起来后发现,爬下来的图都是这样的,但是把url信息输出看了下,链接都没问题啊,奇怪了~
然后用PC玩了下,链接跟爬取的都没问题,这种情况只有网站做了反爬虫了;
既然如此,那我们修改下策略,上面在下载文件的时候,用的是urlretrieve,但是现在需要在下载图片时请求下,而且urlretrieve没有可以设置请求的参数,因此不适用本场景;
urlretrieve(url, filename=None, reporthook=None, data=None)
参数 finename 指定了保存本地路径(如果参数未指定,urllib会生成一个临时文件保存数据。)
参数 reporthook 是一个回调函数,当连接上服务器、以及相应的数据块传输完毕时会触发该回调,我们可以利用这个回调函数来显示当前的下载进度。
参数 data 指 post 到服务器的数据,该方法返回一个包含两个元素的(filename, headers)元组,filename 表示保存到本地的路径,header 表示服务器的响应头。
复制代码
那我们就改成用wirte的方式去写入图片:
with open(dir+filename, 'wb') as f:
#以二进制写入的模式在本地构建新文件
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.32 Safari/537.36'}
f.write(requests.get(i,headers=header).content)
复制代码
执行后发现,依然是下载同一张图片,那说明靠UA还不够,再看下请求头信息:
也没有什么特别的,既然如此,就把整个头部模拟一模一样,最后发现,还需要Referer这个参数:对应的值,貌似就是上面的i,因此修改成这样: for i in image_links:
filename = "%s%s.jpg" % (image_title, num)
print('正在下载图片:%s第%s/%s张,' % (image_title, num, amount))
# 用于在cmd窗口上输出提示,感觉可以增加一个容错函数,没想好怎么写
with open(dir+filename, 'wb') as f:
#以二进制写入的模式在本地构建新文件
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.32 Safari/537.36',
'Referer':i}
f.write(requests.get(i,headers=header).content)
复制代码
执行后发现,每张图片大小都不一样,嗯,终于成功啦~
整体代码如下:
import requests
from lxml import html
import os
import urllib
dir = "mmjpg/"
def Get_Page_Number(num):
if (int(num) < 2):
url = 'http://www.mmjpg.com'
else:
url = 'http://www.mmjpg.com/home/' + num
response = requests.get(url).content
# 调用requests库,获取二进制的相应内容。
# 注意,这里使用.text方法的话,下面的html解析会报错.这里涉及到.content和.text的区别了。简单说,如果是处理文字、链接等内容,建议使用.text,处理视频、音频、图片等二进制内容,建议使用.content。
selector = html.fromstring(response)
# 使用lxml.html模块构建选择器,主要功能是将二进制的服务器相应内容response转化为可读取的元素树(element tree)。lxml中就有etree模块,是构建元素树用的。如果是将html字符串转化为可读取的元素树,就建议使用lxml.html.fromstring,毕竟这几个名字应该能大致说明功能了吧。
urls = []
# 准备容器
for i in selector.xpath("//ul/li/a/@href"):
# 利用xpath定位到所有的套图的详细地址
urls.append(i)
# 遍历所有地址,添加到容器中
return urls
def Get_Image_Title(url):
# 现在进入到套图的详情页面了,现在要把套图的标题和图片总数提取出来
response = requests.get(url).content
selector = html.fromstring(response)
image_title = selector.xpath("//div[@class='article']/h2/text()")[0]
# 需要注意的是,xpath返回的结果都是序列,所以需要使用[0]进行定位
return image_title
def Get_Image_Count(url):
response = requests.get(url).content
selector = html.fromstring(response)
image_count = selector.xpath("//div[@class='page']/a[last()-1]/text()")[0]
return image_count
def Get_Image_Url(url):
response = requests.get(url).content
selector = html.fromstring(response)
image_links = []
image_aount = selector.xpath("//div[@class='page']/a[last()-1]/text()")[0]
for i in range(int(image_aount)):
image_url = url + "/" + str(i + 1)
response = requests.get(image_url).content
sel = html.fromstring(response)
image_download_link = sel.xpath("//div[@class='content']/a/img/@src")[0]
# 这里是单张图片的最终下载地址
image_links.append(str(image_download_link))
return image_links
def Download_Image(image_title, image_links):
num = 1
amount = len(image_links)
if not os.path.exists(dir):
os.makedirs(dir)
for i in image_links:
if not os.path.exists(dir+image_title):
os.makedirs(dir+image_title)
print('正在下载图片:%s第%s/%s张,' % (image_title, num, amount))
# 用于在cmd窗口上输出提示,感觉可以增加一个容错函数,没想好怎么写
filename = image_title+"/"+str(num)+".jpg"
#创建文件名
with open(dir+filename, 'wb') as f:
#以二进制写入的模式在本地构建新文件
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.32 Safari/537.36',
'Referer':i}
f.write(requests.get(i,headers=header).content)
# urllib.request.urlretrieve(requests.get(i,headers=header), "%s%s%s.jpg" % (dir, image_title, num))
#如果使用这种方式爬,网站会返回防盗链接,爬的图片都一样,因此需要爬的时候UA做下处理,而urlretrieve并没有设置请求头的方式,因此不适用本案例
num += 1
if __name__ == '__main__':
page_number = input('请输入需要爬取的页码:')
for link in Get_Page_Number(page_number):
Download_Image(Get_Image_Title(link), Get_Image_Url(link))
复制代码
整体代码与演示代码有点出入,主要是整理了一下,主体思路不变~效果如下:
该实战完毕,主要还是能整理出思路,问题则不大~
Scrapy
自己单独写爬虫的话,会用很多重复的代码,比如设置headers,代理IP,文件保存,虽然每次手动写,都觉得好爽的,但今天来介绍一个出名的爬虫框架--Scrapy
1.为什么要用爬虫框架?
如果你对爬虫的基础知识有了一定了解的话,那么是时候该了解一下爬虫框架了。那么为什么要使用爬虫框架?
1)学习框架的根本是学习一种编程思想,而不应该仅仅局限于是如何使用它。从了解到掌握一种框架,其实是对一种思想理解的过程。
2)框架也给我们的开发带来了极大的方便。许多条条框框都已经是写好了的,并不需要我们重复造轮子,我们只需要根据自己的需求定制自己要实现的功能就好了,大大减少了工作量。 。
2.简介
官网文档:http://scrapy-chs.readthedocs.io/zh_CN/0.24/intro/overview.html
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。
可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。
其最初是为了 页面抓取 (更确切来说, 网络抓取 )所设计的,
也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。
3.scrapy特点:
1)scrapy基于事件的机制,利用twisted的设计实现了非阻塞的异步操作。
这相比于传统的阻塞式请求,极大的提高了CPU的使用率,以及爬取效率。 2)配置简单,可以简单的通过设置一行代码实现复杂功能。 3)可拓展,插件丰富,比如分布式scrapy + redis、爬虫可视化等插件。 4)解析方便易用,scrapy封装了xpath等解析器,提供了更方便更高级的selector构造器,可有效的处理破损的HTML代码和编码。
4.scrapy安装
pip install scrapy
复制代码
在Windows上安装时可能会出现错误,提示找不到Microsoft Visual C++。这时候我们需要到它提示的网站visual-cpp-build-tools下载VC++ 14编译器,安装完成之后再次运行命令即可成功安装Scrapy。
官网链接:http://landinghub.visualstudio.com/visual-cpp-build-tools
但实际电脑还是会一直报找不到Microsoft Visual C++,后来网上查询后,使用Anaconda安装就好,安装包如下:
链接:https://pan.baidu.com/s/1a-VxwaR56iQQu108wqz2zA,密码:r1oz
下载完后直接安装,安装完成后直接CMD命令行输入conda install scrapy,安装完成后是这样的:
验证的话,直接输入scrapy -h,能显示内容即可~
在Ubuntu安装的时候,中途出现一个错误:fatal error: 'Python.h' file not found 需要另外安装python-dev,该库中包含Python的头文件与静态库包, 要根据自己的Python版本进行安装:
sudo apt-get install python3.4-dev
复制代码
5. Scrapy框架路过了解下
架构图
模块介绍:
1)Engine。引擎,处理整个系统的数据流处理、触发事务,是整个框架的核心。
2)Item。项目,它定义了爬取结果的数据结构,爬取的数据会被赋值成该Item对象。
3)Scheduler。调度器,接受引擎发过来的请求并将其加入队列中,在引擎再次请求的时候将请求提供给引擎。
4)Downloader。下载器,下载网页内容,并将网页内容返回给蜘蛛。Spiders。蜘蛛,其内定义了爬取的逻辑和网页的解析规则,它主要负责解析响应并生成提取结果和新的请求。
5)Item Pipeline。项目管道,负责处理由蜘蛛从网页中抽取的项目,它的主要任务是清洗、验证和存储数据。
6)Downloader Middlewares。下载器中间件,位于引擎和下载器之间的钩子框架,主要处理引擎与下载器之间的请求及响应。
7)Spider Middlewares。蜘蛛中间件,位于引擎和蜘蛛之间的钩子框架,主要处理蜘蛛输入的响应和输出的结果及新的请求。
执行过程
1)引擎首先打开一个网站,找到处理该网站的Spider,并向该Spider请求第一个要爬取的URL。
2)引擎从Spider中获取到第一个要爬取的URL,并通过Scheduler以Request的形式调度。
3)引擎向Scheduler请求下一个要爬取的URL。
4)Scheduler返回下一个要爬取的URL给引擎,引擎将URL通下载器中间件转发给Downloader下载。
5)一旦页面下载完毕,Downloader生成该页面的Response,并将其通过下载器中间件发送给引擎。
6)引擎从下载器中接收到Response,并将其通过Spider Middlewares发送给Spider处理。
7)Spider处理Response,并返回爬取到的Item及新的Request给引擎。
8)引擎将Spider返回的Item给Item Pipeline,将新的Request给Scheduler。
9)重复第二步到最后一步,直到Scheduler中没有更多的Request,引擎关闭该网站,爬取结束。
6.项目结构
新建的项目结构如下:
ScrapyStudy/
scrapy.cfg # 项目的配置文件
ScrapyStudy/ # 该项目的python模块,代码都加在里面
__init__.py
items.py # 项目中的item文件
pipelines.py # 项目中pipelines文件
settings.py # 项目的设置文件
spiders/ # 方式spider代码的目录
__init__.py
复制代码
文件描述如下:
1)scrapy.cfg:它是Scrapy项目的配置文件,其内定义了项目的配置文件路径、部署相关信息等内容。
2)items.py:它定义Item数据结构,所有的Item的定义都可以放这里。
3)pipelines.py:它定义Item Pipeline的实现,所有的Item Pipeline的实现都可以放这里。
4)settings.py:它定义项目的全局配置。
5)middlewares.py:它定义Spider Middlewares和Downloader Middlewares的实现。
6)spiders:其内包含一个个Spider的实现,每个Spider都有一个文件
7.制作爬虫步骤
1)新建项目(scrapy startproject xxx): 新建一个新的爬虫项目
2)明确目标(编写items.py): 明确你想要抓取的目标
3)制作爬虫(spiders/xxsp der.py): 制作爬虫开始爬取网页
4)存储内容(pipelines.py): 设计管道存储爬取内容
8.实战1 使用scrapy输出廖雪峰官网的title
根据7得知,制作爬虫需要4个步骤,那现在就以实战来介绍这4个步骤;
1)创建项目:
scrapy startproject 项目名
复制代码
这样就代表新建完成了
然后用pycharm打开了建立的项目后,就开始体验啦~
以廖雪峰python官网为例子,输出title信息;
再spiders目录下新增一个文件,比如liaoxuefeng.py,代码如下:
import scrapy
class LiaoxuefengSpider(scrapy.Spider):
# 这里是将爬虫定义为scrapy.Spider这个类下的一个实例。
# Spider这个类定义了爬虫的很多基本功能,我们直接实例化就好,
# 省却了很多重写方法的麻烦。
name = 'lxf'
#这是爬虫的名字,这个非常重要。
start_urls = ['http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000']
#这是爬虫开始干活的地址,必须是一个可迭代对象。
def parse(self, response):
#爬虫收到上面的地址后,就会发送requests请求,在收到服务器返回的内容后,就将内容传递给parse函数。在这里我们重写函数,达到我们想要的功能。
titles = response.xpath("//ul[@class='uk-nav uk-nav-side']//a/text()").extract()
#这是廖雪峰老师python教程的标题列表。我们利用xpath解析器对收到的response进行分析,从而提取出我们需要的数据。//XXX表示任何任何目录下的XXX区块,/XXX表示子目录下的XXX区块,XXX[@class=abc]表示带有class=abc属性值的XXX区块,/text()表示获取该区块的文本。最后加上.extract()表示将内容提取出来。
for title in titles:
print (title)
#这个没什么说的了,直接遍历,然后打印标题。
复制代码
然后进入cmd,在项目的根目录下运行scrapy crawl lxf(这个lxf就是刚才liaoxuefeng.py文件中的name字段,千万不要弄错了),运行成功,当观察发现,并没有所需要的内容,直接提示“503 Service Unavailable”,
根据经验,这是因为没有设置请求头导致的;
那在setting.py,找到USER_AGENT这个参数,默认是注释的,取消注释后,提供value,比如: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.104 Safari/537.36 Core/1.53.4843.400 QQBrowser/9.7.13021.400
spider必须定义三个属性:
-name: 用于区别Spider。 该名字必须是唯一的,您不可以为不同的Spider设定相同的名字。
-start_urls: 包含了Spider在启动时进行爬取的url列表。 因此,第一个被获取到的页面将是其中之一。 后续的URL则从初始的URL获取到的数据中提取。
-parse() 是spider的一个方法。 被调用时,每个初始URL完成下载后生成的 Response 对象将会作为唯一的参数传递给该函数。 该方法负责解析返回的数据,提取数据(生成item)以及生成需要进一步处理的URL的 Request 对象。
然后再次执行scrapy crawl lxf,就会打印当前页面所有的目录名称:
上面提及到一个知识点:取出网页中想要的信息
Scrapy中使用一种基于XPath和CSSDE表达式机制:Scrapy Selectors 来提取出网页中我们所需的数据。
Selector是一个选择,有四个基本方法:
1)xpath():传入xpath表达式,返回该表达式对应的所有节点的selector list列表;
2)css():传入CSS表达式,返回该表达式对应的所有及诶点的selector list列表;
3)extract():序列化该节点为unicode字符串并返回list;
4)re():根据传入的正则表达式对数据进行提取,返回unicode字符串list列表;
这里顺道学下XPath的基本语法:
首先XPath中的路径分为绝对路径与相对路径:
绝对路径:用**/,表示从根节点开始选取;
相对路径:用//,表示选择任意位置的节点,而不考虑他们的位置;
另外可以使用*通配符来表示未知的元素;除此之外还有两个选取节点的:
.:选取当前节点;..:当前节点的父节点;
接着就是选择分支进行定位了,比如存在多个元素,想唯一定位,
可以使用[]**中括号来选择分支,下标是从1开始算的哦!
比如可以有下面这些玩法:
1)/tr/td[1]:取第一个td
2)/tr/td[last()]:取最后一个td
3)/tr/td[last()-1]:取倒数第二个td
4)/tr/td[position()<3]:取第一个和第二个td
5)/tr/td[@class]:选取拥有class属性的td
6)/tr/td[@class='xxx']:选取拥有class属性为xxx的td
7)/tr/td[count>10]:选取 price 元素的值大于10的td
然后是选择属性,其实就是上面的这个**@** 可以使用多个属性定位,可以这样写:/tr/td[@class='xxx'][@value='yyy'] 或者**/tr/td[@class='xxx' and @value='yyy']**
接着是常用函数:除了上面的last(),position(),外还有: contains(string1,string2):如果前后匹配返回True,不匹配返回False; text():获取元素的文本内容 start-with():从起始位置匹配字符串
回顾
第一个实战项目就到此结束啦~
是不是很简单?
遇到一个问题:
在用pycharm打开scrapy项目后,scrapy一直在显示红色报错,当时介意了很久,心想着,本地用都能用,为什么你这边报错?
结果后来发现,原来scrapy不是在pycharm上执行的,报错也不需要管~本地确认scrapy有安装就行了~
9.实战2 使用scrapy输出廖雪峰官网的title
1)创建项目,直接在需要的目录下执行scrapy startproject 项目名
2)在spiders目录下新建爬虫文件,比如本例叫LiaoxuefengSpider,创建后,需要思考在里面填写什么?
填写需要爬取的网站;
设置爬虫名称,这里注意,该名称是全局唯一的,不允许重复;
界面解析;
内容输出;
因此不难写出下面的代码;
import scrapy
class LiaoxuefengSpider(scrapy.Spider):
# 这里是将爬虫定义为scrapy.Spider这个类下的一个实例。
# Spider这个类定义了爬虫的很多基本功能,我们直接实例化就好,
# 省却了很多重写方法的麻烦。
name = 'lxf'
#这是爬虫的名字,这个非常重要。
start_urls = ['https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000']
#这是爬虫开始干活的地址,必须是一个可迭代对象。
def parse(self, response):
#爬虫收到上面的地址后,就会发送requests请求,在收到服务器返回的内容后,就将内容传递给parse函数。在这里我们重写函数,达到我们想要的功能。
titles = response.xpath("//ul[@class='uk-nav uk-nav-side']//a/text()").extract()
#这是廖雪峰老师python教程的标题列表。我们利用xpath解析器对收到的response进行分析,从而提取出我们需要的数据。//XXX表示任何任何目录下的XXX区块,/XXX表示子目录下的XXX区块,XXX[@class=abc]表示带有class=abc属性值的XXX区块,/text()表示获取该区块的文本。最后加上.extract()表示将内容提取出来。
for title in titles:
print (title)
#这个没什么说的了,直接遍历,然后打印标题。
复制代码
编写后,在项目目录下执行scrapy crawl 爬虫名称,如scrapy crawl lxf,就会执行,但是会发现报错:
从截图信息,看到服务器返回503,根据经验,这是因为没有设置请求headers导致,因此有两种方案:1)在爬虫文件里设置请求headers
2)打开项目里的settings.py,找到 USER_AGENT,默认是被注释的,关闭注释,并且给一个默认的UA即可,如:
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
再次输入scrapy crawl爬虫名称,发现能正常显示了,如下:
这就说明脚本能正常执行,同时也说明scrapy第一个例子成功啦~
重要的信息重复强调,爬虫名称是全局唯一的~
10.实战3 使用scrapy爬取电影图片
先创建项目:scrapy startproject 项目名
创建完项目之后,在spiders创建一个文件,创建后,项目结构如下:
想爬的url链接是这个:http://www.id97.com/movie/,先看看我们想爬什么~
从上图看出,可以爬的东西有,电影名称,类型,分数以及封面链接
既然都已经决定了,那还记得,哪个文件是用来存放目标的吗?没错,就是item.py
import scrapy
class MovieScrapyItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
articleUrl = scrapy.Field()
movieName = scrapy.Field()
scoreNumber = scrapy.Field()
style = scrapy.Field()
复制代码
不难得出上面的内容,那我们继续分析网页结构~
直接点击,发现每一部电影的数据都是在独立的class里面,这个class叫col-xs-1-5 col-sm-4 col-xs-6 movie-item这也意味着,上面需要的内容,都可以独立获取了,那div这块可以这样写了
def parse(self,response):
selector = Selector(response)
divs = selector.xpath("//div[@class='col-xs-1-5 col-sm-4 col-xs-6 movie-item']")
复制代码
回顾上面内容,什么是Selector?
Scrapy提取数据有自己的一套机制。它们被称作选择器(seletors),因为他们通过特定的 XPath 或者 CSS 表达式来“选择” HTML文件中的某个部分;
刚刚上面提及到了,所有的电影内容都在不同的class里面,但都叫col-xs-1-5 col-sm-4 col-xs-6 movie-item,因此只需要直接找这个class, 那返回的就是所有div信息;
那只需要写个for,即可获取每个div的信息
for div in divs:
yield self.parse_item(div)
复制代码
知识点,yield是啥?
yield的作用是把parse函数变成一个发生器(generator),每次函数执行会返回一个迭代对象(iterable 对象)。听上去是不是跟return一样好像一样的感觉?
return返回的是函数返回值,而yield返回的是一个生成器~
关于如何更好理解yield,这边贴一个外链,感兴趣的同学可以了解:
http://pyzh.readthedocs.io/en/latest/the-python-yield-keyword-explained.html
扯远了,上面的代码,parse_item这个方法不是自带的,是自己写的,用来处理的就是针对每个电影div做详细的数据获取,那继续看分析吧~
随便挑一个打开,就能看到我们需要的内容了,标题,封面url,分数,类型,其中,标题跟url可以直接获取到,而分数跟类型还要做下处理~
首先,第一步,获取数据,如下:
item['articleUrl'] = div.xpath('div[@class="movie-item-in"]/a/img/@data-original ').extract_first()
item['movieName'] = div.xpath('div[@class="movie-item-in"]/a/@title').extract_first()
item['scoreNumber'] = div.xpath('div[@class="movie-item-in"]/div[@class="meta"]/h1/em/text()').extract_first()
item['style']= div.xpath(
'string(div[@class="movie-item-in"]/div[@class="meta"]/div[@class="otherinfo"])').extract_first(
default="")
复制代码
上面这样获取是不是就行了?不是的,分数那是不对的,因为通过xpath获取到的分数是这样的: - 6.8分,
而我们需要的是6.8,那以意味着分数需要二次处理,也很简单,直接正则提取就行了,不详细说明,源码如下:
def parse_item(self,div):
item = MovieScrapyItem()
#封面url
item['articleUrl'] = div.xpath('div[@class="movie-item-in"]/a/img/@data-original ').extract_first()
#电影名称
item['movieName'] = div.xpath('div[@class="movie-item-in"]/a/@title').extract_first()
#分数,但需要处理
score = div.xpath('div[@class="movie-item-in"]/div[@class="meta"]/h1/em/text()').extract_first()
item['scoreNumber'] = self.convertScore(score)
#类型
style = div.xpath(
'string(div[@class="movie-item-in"]/div[@class="meta"]/div[@class="otherinfo"])').extract_first(
default="")
item['style'] = style
#用来提取分数的
def convertScore(self, str):
list = re.findall(r"\d+\.?\d*", str)
if list:
return list.pop(0)
else:
return 0
复制代码
有同学会有一个疑问,extract_first()跟extract()区别在哪里?
extract()返回的是数据,extract_first()返回的是字符串,不信?看看下面~
结果发现,str(extract)输出的就是一个数据,如果用于拼接,还需要额外处理的" ".join(str(extract))这样处理,那还不如直接一个是extract_first()返回字符串来的快~
那如果我们想翻页继续爬呢?当然没问题啦~
直接点击下一页,它的结构如下,是在一个叫pager-gb的class里面,而下一页的按钮就在倒数第二个li里面~
#定位到页数这样
pages = selector.xpath("body/div/div[@class='pager-bg']/ul/li")
#下一页
nextPageUrl = self.host + (pages[- 2].xpath('a/@href').extract_first())
复制代码
这里的self.host是文件开头定义好的,是"http://www.id97.com",用于拼接的;
这样就能获取到下一页的链接了,那接下来就是再次发起请求即可;
import 下Request,直接执行即可;
yield Request(nextPageUrl,callback=self.parse)
复制代码
完整代码文末贴出,别着急=。=
至此,爬虫部分搞定了,item里面的内容就是我们要的东西了~
那,能否把这些信息保存到csv里面?
当然可以,没问题,而且还不需要改动代码哦~
直接在执行命令的时候,加多几个参数即可~
scrapy crawl 项目名 -o xx.csv
复制代码
这样就能生成csv文件~但是打开后,辣眼睛啊~都乱码?
网上找了下,原因是:
微软的软件打开文件默认都是 ANSI 编码(国内就是 GBK),UTF-8 的 csv 文件在 execl 中打开时解码自然就乱码了~
解决方案呢?技术层面貌似找了好久都没找到,就是encode啥指定编码都不行,因此只能这样: 用记事本打开刚刚保存的csv,点击另存为,不再使用utf-8,然后再打开就好了~
既然都有url了,那能存拿url出来进行保存?
没问题,都可以满足~
保存图片有2种方式:
1)urlib.request.urlretrieve
2)scrapy内置的ImagePipeline
第一种不需要解释了,上篇文章已经有说明,使用场景就是在获取url的时候,直接下载,效率会慢点:
if item['articleUrl']:
file_name = "%s.jpg"%(item['movieName'])
file_path = os.path.join("F:\\pics", file_name)
urllib.request.urlretrieve(item['articleUrl'], file_path)
复制代码
第二种,需要修改pipelines.py这个文件,直接重写即可,规则自己定义:
class MovieScrapyPipeline(ImagesPipeline):
# def process_item(self, item, spider):
# return item
# 在工作流程中可以看到,管道会得到图片的URL并从项目中下载。
# # 为了这么做,你需要重写 get_media_requests() 方法,并对各个图片URL返回一个Request:
def get_media_requests(self, item, info):
# 这里把item传过去,因为后面需要用item里面的name作为文件名
yield Request(item['articleUrl'])
#修改图片生存名称规则
def file_path(self, request, response=None, info=None):
item = request.meta['item']
image_guid = request.url.split('/')[-1] # 倒数第一个元素
filenames = "full/%s/%s" % (item['name'], image_guid)
# print(filename)
return filenames
复制代码
修改完pipelinse后,还要修改下settings.py文件;
找到ITEM_PIPELINES把注释去掉,启用pinelines, 把我们自定义的PicPipeLine加上,还有顺道设置下下载图片的存放位置:
ITEM_PIPELINES = {
'movie_scrapy.pipelines.MovieScrapyPipeline':300,
}
IMAGES_STORE = "F:\pics"
复制代码
然后再执行,图片的哔哩吧啦的下载啦~
这里遇到一个问题,自带的ImagePipeline保存图片功能,同一份代码,在Linux下可以下载图片,但是在Windows下就不能下载,
但是代码没报错,没找到原因,所以后来才用urlretrieve下载的,不知道有同学知道原因吗?
源码如下: movie_spiders.py
import scrapy
from scrapy.selector import Selector
from scrapy import Request
from movie_scrapy.items import MovieScrapyItem
import re
import os
import urllib
class movie_spiders(scrapy.Spider):
name = "movie"
host = "http://www.id97.com"
start_urls = ["http://www.id97.com/movie/"]
def parse(self,response):
selector = Selector(response)
divs = selector.xpath("//div[@class='col-xs-1-5 col-sm-4 col-xs-6 movie-item']")
pages = selector.xpath("body/div/div[@class='pager-bg']/ul/li")
for div in divs:
yield self.parse_item(div)
nextPageUrl = self.host + (pages[- 2].xpath('a/@href').extract_first())
yield Request(nextPageUrl,callback=self.parse)
def parse_item(self,div):
item = MovieScrapyItem()
item['articleUrl'] = div.xpath('div[@class="movie-item-in"]/a/img/@data-original ').extract_first()
item['movieName'] = div.xpath('div[@class="movie-item-in"]/a/@title').extract_first()
score = div.xpath('div[@class="movie-item-in"]/div[@class="meta"]/h1/em/text()').extract_first()
item['scoreNumber'] = self.convertScore(score)
style = div.xpath(
'string(div[@class="movie-item-in"]/div[@class="meta"]/div[@class="otherinfo"])').extract_first(
default="")
item['style'] = style
#下载图片使用
if item['articleUrl']:
file_name = "%s.jpg"%(item['movieName'])
file_path = os.path.join("F:\\pics", file_name)
urllib.request.urlretrieve(item['articleUrl'], file_path)
return item
def convertScore(self, str):
list = re.findall(r"\d+\.?\d*", str)
if list:
return list.pop(0)
else:
return 0
复制代码
items.py
import scrapy
class MovieScrapyItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
articleUrl = scrapy.Field()
movieName = scrapy.Field()
scoreNumber = scrapy.Field()
style = scrapy.Field()
复制代码
pipelines.py
# -*- coding: utf-8 -*-
from scrapy.pipelines.images import ImagesPipeline
from scrapy.http import Request
class MovieScrapyPipeline(ImagesPipeline):
# def process_item(self, item, spider):
# return item
# 在工作流程中可以看到,管道会得到图片的URL并从项目中下载。
# # 为了这么做,你需要重写 get_media_requests() 方法,并对各个图片URL返回一个Request:
def get_media_requests(self, item, info):
# 这里把item传过去,因为后面需要用item里面的name作为文件名
yield Request(item['articleUrl'])
#修改图片生存名称规则
def file_path(self, request, response=None, info=None):
item = request.meta['item']
image_guid = request.url.split('/')[-1] # 倒数第一个元素
filenames = "full/%s/%s" % (item['name'], image_guid)
# print(filename)
return filenames
复制代码
settings.py # -- coding: utf-8 --
BOT_NAME = 'movie_scrapy'
SPIDER_MODULES = ['movie_scrapy.spiders']
NEWSPIDER_MODULE = 'movie_scrapy.spiders'
ROBOTSTXT_OBEY = True
ITEM_PIPELINES = {
'movie_scrapy.pipelines.MovieScrapyPipeline':300,
}
IMAGES_STORE = "F:\pics"
复制代码
小结
本章大方向学习了requests跟scrapy,但细节点还是不少:
xpath,yield,反爬虫,网页结构分析,为后续爬虫做下预热学习;
下文预告:
本来有些实战例子是想选择知乎等平台的,还是发现登录的时候需要各种验证码,比如说选择倒立的图片等等,
所以下篇文章就以模拟登录,如何跨过验证码为前提去做,初步想法是覆盖英文/数字验证码,滑动验证码,倒立点击验证码以及12306验证码~
碎片时间写了10天,终于结束了,谢谢大家~