Py学习  »  Python

JB的Python之旅-爬虫篇--requests&Scrapy

jb • 5 年前 • 466 次点击  

JB的Python之旅-爬虫篇--requests&Scrapy

前序
本来说好随缘更新的,因为想去写测试相关的文档,但想想,既然第一篇爬虫文章都发布吧,就继续完善吧~

上一章回顾: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("http://img2.jintiankansha.me/get2?src=http://pic1.zhimg.com/v2-2e92ebadb4a967829dcd7d05908ccab0_b.jpg")
type(r.content)
<class 'bytes'>
# 另存为 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)
<class 'str'>
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下面有详细介绍~

结构跟首页一致但我们获取的是标题,直接获取h2的title即可,如下:

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()返回的是字符串,不信?看看下面~

直接输出,系统提示must be str,not list,那行,那我们就str(XX),接下来继续看~

先看右上角,输出2个参数,一个是extract_first,一个是str(extract),然后再看输出内容的倒数4行
结果发现,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天,终于结束了,谢谢大家~


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/gQE4Yn7KP3
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/13250
 
466 次点击