AoboSir 博客

与15年前的我比,我现在是大人;与15年后的我比,我现在还是个婴儿

Python3 大型网络爬虫实战 003 — Scrapy 大型静态图片网站爬虫项目实战 — 实战:爬取 169美女图片网 高清图片


[TOC]

开发环境

  • Python第三方库:lxml、Twisted、pywin32、scrapy
  • Python 版本:python-3.5.0-amd64
  • PyCharm软件版本:pycharm-professional-2016.1.4
  • 电脑系统:Windows 10 64位

如果你还没有搭建好开发环境,请到这篇博客


  • 本篇博客源代码GitHub里:这里

这一篇博客的目的是爬取 169美女图片网 里面的所有的“西洋美女”的高清图片。

Alt text


爬虫程序设计思路:

1 . 先得到 http://www.169bb.com/xiyangmeinv/ 页面里面所有的照片后面对应的URL网页链接()。

2 . 接着在得到的URL链接网页里面得到里面所有高清图片的下载地址,进行下载。

3 . 得到所有 “西洋美女” 网页的页数。

Alt text


观察网页 和 网页源代码

1 . 打开 169美女图片网:http://www.169bb.com/

2 . 我们的目的是爬取这个站点里面所有 “西洋美女” 的高清图片。所以点击进入“西洋美女” 标签里。(http://www.169bb.com/xiyangmeinv/%EF%BC%89

Alt text

3 . 观察这个页面,在页面最下面,显示了,当前一共311页。

Alt text

4 . 我们再来观察每页的网址有什么共同点,我们发现:

这样每页的网址是有规律的,按照这个规律,我们可以推测出“西洋美女” 的第120页的网址就应该是:http://www.169bb.com/xiyangmeinv/list_4_120.html 。事实的确是这样的。好。

5 . 现在,我们随便点击一个图片,进去看看这个美女的高清图片集。

里面都是高清的图片,并且有很多,并且,不止一页。就我随机点击的这个美女的链接就有11页,并且一页里面有5张左右的高清图片。

Alt text

6 . 并且它的每页的网址也是有规律的。

但是有的美女的网址里只有一页,比如这个:http://www.169bb.com/xiyangmeinv/2016/0103/5974.html


好了,现在这个目标网页,我们已经分析完了。现在就可以编程。


写程序

源代码GitHub里:这里

接下来我们为大家讲解大型图片爬虫项目编写实战。

Step 1 .

创建一个Scrapy爬虫项目:

1
scrapy startproject secondDemo

Alt text

创建一个scrapy爬虫文件,我们就在这个文件里面写爬取目标网站里面图片的爬虫程序。

1
2
cd secondDemo
scrapy genspider -t basic pic_169bb 169bb.com

Alt text


PyCharm 软件打开刚刚创建的 secondDemo 工程。

Alt text

Step 2 . items.py 文件里面的SeconddemoItem()函数里面创建一个对象,这个对象在其他的文件里面会使用到。

Alt text

Step 3 . 现在开始爬虫的编写。进入pic_169bb.py文件。

爬虫(pic_169bb.py文件)会自动的先爬首页(169bb.com),爬完首页之后,会自动的进入parse()回调函数。

这个回调函数中,我们需要写些东西。

先获取所有栏目的名字和地址。

Alt text

查看源代码:

Alt text

pic_169bb.py 文件中的 parse()回调函数中添加下面的代码:

1
2
    urldata = response.xpath("/html/body/div[@class='header']/div[@class='hd_nav']/div[@class='w1000']//a/@href").extract()
    print(urldata)

Alt text

现在运行一下,输出:

Alt text


继续进行下一次的爬取:

先导入一个模块

1
from scrapy.http import Request

爬取子栏目的第一页,即西洋美女网址的第一页。

1
2
3
4
5
6
    xiyangurldata = urldata[4]  # 获取西洋美女首页网址
    print(xiyangurldata)
    yield Request(url=xiyangurldata, callback=self.next)

def next(self, response):
    pass

Alt text

1
2
3
4
5
6
def next(self, response):
    page_title_list = response.xpath("/html/body//div[@class='w1000 box03']/ul[@class='product01']//li/a/@alt").extract()
    print(page_title_list)
    page_url_list = response.xpath("/html/body//div[@class='w1000 box03']/ul[@class='product01']//li/a/@href").extract()
    print(page_url_list)
    pass

Alt text

运行输出一下:

Alt text


Alt text

1
2
3
4
5
6
7
    page_num = response.xpath("//span[@class='pageinfo']//strong/text()").extract()[0] # 得到西洋美女总页数
    print(page_num)
    print(response.url)
    for i in range(1, int(page_num)+1):
        page_url = response.url + 'list_4_'+ str(i) + '.html' # 得到西洋美女每一个页面的网址
        print(page_url)
    pass

Alt text

运行输出一下:

Alt text

Alt text


继续下一次:

1
2
3
4
5
6
7
8
    for i in range(1, int(page_num)+1):
        page_url = response.url + 'list_4_'+ str(i) + '.html' # 得到西洋美女每一个页面的网址
        print(page_url)
        yield Request(url=page_url, callback=self.next2)
    pass

def next2(self, response):
    pass

Alt text


现在获取每一个美女的网页网址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def next2(self, response):
    page_title_list = response.xpath("/html/body//div[@class='w1000 box03']/ul[@class='product01']//li/a/@alt").extract()
    # print(page_title_list)
    page_url_list = response.xpath("/html/body//div[@class='w1000 box03']/ul[@class='product01']//li/a/@href").extract()
    # print(page_url_list)

    for i in range(0, len(page_url_list)):
        gril_page_url = page_url_list[i]
        print(gril_page_url)
        yield Request(url=gril_page_url, callback=self.next3)
    pass

def next3(self, response):
    pass

Alt text

运行程序看看:

Alt text


next3() 这个回调函数的功能就是得到一个美女网页里面的所有的页面的网址。

有的美女的网页里面只有一个页面,有的美女的网页里面有多个页面:

Alt text

Alt text

可以统一解决。


测试:

Alt text

Alt text

Alt text

输出:

Alt text


Alt text

Alt text

同样的回调函数

输出:

Alt text


所以,我们可以这样写程序:

当得到的页码为-3,说明这个美女的网页是单页的;如果得到的页码数不等于-3,说明这个美女的网页是多也的。

测试程序:

对于多页面的美女网页网址

Alt text

Alt text

运行输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
D:\WorkSpace\python_ws\python-large-web-crawler\secondDemo>scrapy crawl pic_169bb --nolog
10
http://www.169bb.com/xiyangmeinv/2016/0717/36463.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_5.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_10.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_9.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_6.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_8.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_7.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_3.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_4.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_2.html

D:\WorkSpace\python_ws\python-large-web-crawler\secondDemo>

对于单页面的美女网页:

1
2
3
def parse(self, response):
  ...
    yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0103/2268.html', callback=self.demo)

回调函输一样。

运行输出:

1
2
3
4
5
D:\WorkSpace\python_ws\python-large-web-crawler\secondDemo>scrapy crawl pic_169bb --nolog
-3
http://www.169bb.com/xiyangmeinv/2016/0103/2268.html

D:\WorkSpace\python_ws\python-large-web-crawler\secondDemo>

成功。

所以现在的爬虫代码应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# -*- coding: utf-8 -*-
import scrapy
from scrapy.http import Request

class Pic169bbSpider(scrapy.Spider):
    name = "pic_169bb"
    allowed_domains = ["169bb.com"]
    start_urls = ['http://169bb.com/']

    def parse(self, response):
        title_list = response.xpath("/html/body/div[@class='header']/div[@class='hd_nav']/div[@class='w1000']//a/text()").extract()
        # print(title_list)
        urldata = response.xpath("/html/body/div[@class='header']/div[@class='hd_nav']/div[@class='w1000']//a/@href").extract()
        #print(urldata)
        xiyang_title = title_list[4] # 获取西洋美女标签的文本内容
        xiyang_urldata = urldata[4]  # 获取西洋美女首页网址
        # print(xiyang_title, xiyang_urldata)
        yield Request(url=xiyang_urldata, callback=self.next)
        # yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0717/36463.html', callback=self.demo)
        # yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0103/2268.html', callback=self.demo)

    def next(self, response):
        page_num_str = response.xpath("//span[@class='pageinfo']//strong/text()").extract()[0] # 得到西洋美女总页数
        # print(page_num_str)
        # print(response.url)
        for i in range(1, int(page_num_str)+1):
            page_url = response.url + 'list_4_'+ str(i) + '.html' # 得到西洋美女每一个页面的网址
            # print(page_url)
            yield Request(url=page_url, callback=self.next2)
        pass

    def next2(self, response):
        page_title_list = response.xpath("/html/body//div[@class='w1000 box03']/ul[@class='product01']//li/a/@alt").extract()
        # print(page_title_list)
        page_url_list = response.xpath("/html/body//div[@class='w1000 box03']/ul[@class='product01']//li/a/@href").extract()
        # print(page_url_list)

        for i in range(0, len(page_url_list)):
            gril_page_url = page_url_list[i] # 得到西洋美女页面里面每一个美女的网页网址
            print(gril_page_url)
            yield Request(url=gril_page_url, callback=self.next3)
        pass

    def next3(self, response):
        rela_pages_list = response.xpath("//div[@class='dede_pages']/ul//li/a/text()").extract()
        pages_num = len(rela_pages_list) - 3
        # print(pages_num)
        self.getPic(response)
        if pages_num == -3:
            # pages_num = 1
            return
        for i in range(2, pages_num+1):
            girl_page_url = response.url.replace('.html', '_') + str(i) + '.html'
            # print(girl_page_url)
            yield Request(url=girl_page_url, callback=self.next4)
        pass

    # def demo(self, response):
    # #     rela_pages_list = response.xpath("//div[@class='dede_pages']/ul//li/a/text()").extract()
    # #     pages_num = len(rela_pages_list)-3
    # #     print(pages_num)
    # #     pass
    #     rela_pages_list = response.xpath("//div[@class='dede_pages']/ul//li/a/text()").extract()
    #     pages_num = len(rela_pages_list) - 3
    #     # print(pages_num)
    #     self.getPic(response)
    #     if pages_num == -3:
    #         # pages_num = 1
    #         return
    #     for i in range(2, pages_num+1):
    #         girl_page_url = response.url.replace('.html', '_') + str(i) + '.html'
    #         # print(girl_page_url)
    #         yield Request(url=girl_page_url, callback=self.next4)
    #     pass

    def next4(self, response):
        self.getPic(response)
        pass

    def getPic(self, response):
        print(response.url)
        pass

现在,我们需要在getPic() 函数中获取每一个美女网页的每一个页面里面的所有高清图片。

Alt text

1
2
3
4
5
6
def getPic(self, response):
    # print(response.url)
    item = SeconddemoItem()
    item['url'] = response.xpath("//div[@class='big-pic']/div[@class='big_img']//p/img/@src").extract()
    print(item['url'])
    pass

测试运行:

测试多页的美女网页:

先将parse()函数的最后一行改为:yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0717/36463.html', callback=self.demo)

Alt text

测试单页的美女网页:

parse()函数的最后一行改为:yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0103/2268.html', callback=self.demo)

Alt text

成功。


下载高清图片

OK,现在我们就已经得到了所有西洋美女的所有高清图片的下载地址,我们在piplines.py 文件中使用它们。

删除了next4()回调函数。在demo()回调函数里面调用的都是getPic()回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def demo(self, response):
#     rela_pages_list = response.xpath("//div[@class='dede_pages']/ul//li/a/text()").extract()
#     pages_num = len(rela_pages_list)-3
#     print(pages_num)
#     pass
    rela_pages_list = response.xpath("//div[@class='dede_pages']/ul//li/a/text()").extract()
    pages_num = len(rela_pages_list) - 3
    # print(pages_num)
    self.getPic(response)
    if pages_num == -3:
        # pages_num = 1
        return
    for i in range(2, pages_num+1):
        girl_page_url = response.url.replace('.html', '_') + str(i) + '.html'
        # print(girl_page_url)
        yield Request(url=girl_page_url, callback=self.getPic)
    pass

# error : yield 经过了一个中间函数,运行就有问题。我现在还不知道为什么
# def next4(self, response):
#     self.getPic(response)
#     pass

并将getPic()函数里面的item写到生成器里面:

1
2
3
4
5
6
7
def getPic(self, response):
    # print(response.url)
    item = SeconddemoItem()
    item['url'] = response.xpath("//div[@class='big-pic']/div[@class='big_img']//p/img/@src").extract()
    # print(item['url'])
    # pass
    yield item

测试:

测试单页的美女网页:

parse()函数的最后一行改为:yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0103/2268.html', callback=self.getPic)

成功。

测试多页的美女网页:

先将parse()函数的最后一行改为:yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0717/36463.html', callback=self.demo)

Alt text

也算算成功,因为有重复的文件名,所以自动替换,所以这里需要做修改。

我们可以使用美女图片网址里面的数字作为图片文件名的固定前缀来给图片命名,使用正则表达式获取网址的数字。


pipelines.py 文件 中的 process_item() 函数中使用正则表达式得到图片下载网址的数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import re
import urllib.request

class SeconddemoPipeline(object):
    def process_item(self, item, spider):
        # print(len(item['url']))
        for i in range(0, len(item['url'])):
            this_url = item['url'][i]
            id = re.findall('http://724.169pp.net/169mm/(.*?).jpg', this_url)[0]
            id = id.replace('/', '_')
            print(id)
            # file = 'D:/WorkSpace/python_ws/python-large-web-crawler/xiyangmeinv/' + str(i) + '.jpg'
            # print('Downloading :' , file)
            # urllib.request.urlretrieve(this_url, filename=file)
            # print('Final Download :' , file)
        return item

Alt text

要想使用 pipelines.py 文件 中的 SeconddemoPipeline 类,需要在 settings.py 文件里面设置 ITEM_PIPELINES 项:

1
2
3
ITEM_PIPELINES = {
   'secondDemo.pipelines.SeconddemoPipeline': 300,
}

Alt text

这里,我有一件事情不懂,关于正则表达式的: 网址里面的. 也是正则表达式中的工具字符,也是数据中中的内容,那么正则表达式是如何分辨它在这里是功能字符还是内容字符?

pic_169bb.py 文件的demo() 回调函数中,这样写才能获取到美女网页的第一页的图片地址:

1
2
3
4
5
6
7
8
    # error
    # self.getPic(response)
    # succes 为啥将下面的代码用self.getPic(response)的形式不能正常的获取到,而使用下面的代码却能获取到?
    item = SeconddemoItem()
    item['url'] = response.xpath("//div[@class='big-pic']/div[@class='big_img']//p/img/@src").extract()
    # print(item['url'])
    # pass
    yield item

Alt text

运行程序试试:

测试单页的美女网页:

parse()函数的最后一行改为:yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0103/2268.html', callback=self.getPic)

Alt text

成功。

测试多页的美女网页:

先将parse()函数的最后一行改为:yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0717/36463.html', callback=self.demo)

Alt text

都成功。


先测试下载图片:

pipelines.py 文件 中的 SeconddemoPipeline 类的process_item() 函数里面,添加代码:

1
2
3
4
5
6
7
8
9
10
11
12
def process_item(self, item, spider):
    # print(len(item['url']))
    for i in range(0, len(item['url'])):
        this_url = item['url'][i]
        id = re.findall('http://724.169pp.net/169mm/(.*?).jpg', this_url)[0]
        id = id.replace('/', '_')
        # print(id)
        file = 'D:/WorkSpace/python_ws/python-large-web-crawler/xiyangmeinv/' + id + '.jpg'
        print('Downloading :' , file)
        urllib.request.urlretrieve(this_url, filename=file)
        print('Final Download :' , file)
    return item

Alt text

运行程序,没有毛病:(除了下载速度有点慢)

Alt text


下载 169美女图片网 的所有西洋美女的图片

pic_169bb.py文件里, 将parse()函数的最后一行改为:yield Request(url=xiyang_urldata, callback=self.next)

Alt text

demo() 函数里面的所有代码复制一份到 next3()函数里:

Alt text

现在,运行程序:(最终的程序)

Alt text

Alt text

成功!


防反爬技术

Step 4 . 不遵循 robots.txt 协议。

settings.py 文件里面的 ROBOTSTXT_OBEY 项设置为:False

Alt text


Step 6 . 模仿浏览器

请先查看这篇博客:http://blog.csdn.net/github_35160620/article/details/52489709 里面是:六 . 设置 用户代理(user_agent)

settings.py 文件里面的 USER_AGENT 项设置为:浏览器的用户代理信息。

1
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.22 Safari/537.36 SE 2.X MetaSr 1.0'

Step 7 . 禁止缓存

settings.py 文件里面的 COOKIES_ENABLED 项设置为:False

1
COOKIES_ENABLED = False

搞定


需要升级的地方:(2016-11-27 19:34:34)

  1. 在易错的代码段加上异常检测程序
  2. 在下载图片的代码加上:超时异常检测程序
  3. 记录成功下载的、超时失败下载的、链接失败下载的 信息
  4. 添加断点续下功能。

Comments