Scrapy Spiders

Spiders

Spider 类定义了如何爬取某个(或某些)网站。包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item)。 换句话说,Spider 就是您定义爬取的动作及分析某个网页(或者是有些网页)的地方。

对spider来说,爬取的循环类似下文:

  1. 以初始的 URL 初始化 Request,并设置回调函数。当该 response 下载完毕并返回时,将生成 response ,并作为参数传给该回调函数。spider 中初始的 request 是通过调用 start_requests() 来获取的。 start_requests() 读取 start_urls 中的 URL, 并以 parse 为回调函数生成 Request 。

  2. 在回调函数内分析返回的(网页)内容,返回 Item 对象、 dict 、Request 或者一个包含三者的可迭代容器。返回的 Request 对象之后会经过 Scrapy 处理,下载相应的内容,并调用设置的回调函数进行下一步的操作。

  3. 在回调函数内,您可以使用 选择器(Selectors) (您也可以使用 BeautifulSoup, lxml 或者您想用的任何解析器) 来分析网页内容,并根据分析的数据生成 item。

  4. 最后,由 spider 返回的 item 将被存到数据库(由某些 Item Pipeline 处理)或使用 Feed exports 存入到文件中。

该循环对任何类型的spider都(多少)适用。

scrapy.Spider

Spider 是最简单的 spider。每个其他的 spider 必须继承自该类(包括 Scrapy 自带的其他 spider 以及您自己编写的 spider )。 Spider 并没有提供什么特殊的功能。 其仅仅提供了 start_requests() 的默认实现,读取并请求 spider 属性中的 start_urls, 并根据返回的结果(resulting responses)调用 spider 的 parse 方法。

  • name

    定义 spider 名字的字符串(string)。spider 的名字定义了 Scrapy 如何定位(并初始化) spider,所以其必须是唯一的。 不过您可以生成多个相同的 spider 实例(instance),这没有任何限制。 name 是 spider 最重要的属性,而且是必须的。

    如果该 spider 爬取单个网站(single domain),一个常见的做法是以该网站(domain)(加或不加后缀)来命名 spider。 例如,如果 spider 爬取 mywebsite.com ,该 spider 通常会被命名为 mywebsite

  • allowed_domains

    可选。包含了 spider 允许爬取的域名(domain)列表(list)。 当 OffsiteMiddleware 启用时, 域名不在列表中的URL不会被跟进。

  • start_urls

    URL 列表。当没有制定特定的 URL 时,spider 将从该列表中开始进行爬取。 因此,第一个被获取到的页面的 URL 将是该列表之一。 后续的URL将会从获取到的数据中提取。就是爬虫的入口网址。

  • custom_settings

    该设置是一个 dict .当启动 spider 时,该设置将会覆盖项目级的设置. 由于设置必须在初始化(instantiation)前被更新,所以该属性 必须定义为 class 属性.

  • crawler

    该属性在初始化 class 后,由类方法 from_crawler() 设置, 并且链接了本 spider 实例对应的 Crawler 对象.

    Crawler 包含了很多项目中的组件, 作为单一的入口点 (例如插件,中间件,信号管理器等).

  • start_requests()

    该方法必须返回一个可迭代对象(iterable)。该对象包含了 spider 用于爬取的第一个 Request。

    当 spider 启动爬取并且未指定 URL 时,该方法被调用。 当指定了 URL 时,make_requests_from_url() 将被调用来创建 Request 对象。 该方法仅仅会被 Scrapy 调用一次,因此您可以将其实现为生成器。

    该方法的默认实现是使用 start_urls 中的 url 生成 Request。

    如果您想要修改最初爬取某个网站的 Request 对象,您可以重写(override)该方法。 例如,如果您需要在启动时以 POST 登录某个网站,你可以这么写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class MySpider(scrapy.MySpider):
    name = 'myspider'

    def start_request(self):
    return [scrapy.FormRequest("http://www.example.com/login",
    formdata={'user': 'john', 'pass': 'secret'},
    callback=self.logged_in)]

    def logged_in(self, response):
    pass
  • make_requests_from_url(url)

    该方法接受一个 URL 并返回用于爬取的 Request 对象。 该方法在初始化 request 时被 start_requests() 调用,也被用于转化 url 为 request 。

    默认未被复写(overridden)的情况下,该方法返回的 Request 对象中, parse() 作为回调函数,dont_filter 参数也被设置为开启。

  • parse(response)

    当 response 没有指定回调函数时,该方法是 Scrapy 处理下载的 response 的默认函数。

    parse 负责处理 response 并返回处理的数据以及(/或)跟进的 URL。 Spider 对其他的 Request 的回调函数也有相同的要求。

    该方法及其他的 Request 回调函数必须返回一个包含 Request、dict 或 Item 的可迭代的对象。

  • log(message[, level, component])

    使用 scrapy.log.msg() 方法记录(log)message。 log 中自动带上该 spider 的 name 属性。

  • closed(reason)

    当 spider 关闭时,该函数被调用。 该方法提供了一个替代调用 signals.connect() 来监听 spider_closed 信号的快捷方式。

Spider arguments

Spider arguments are passed through the crawl command using the -a option. For example:

scrapy crawl myspider -a category=electronics

1
2
3
4
5
6
7
8
import scrapy

class MySpider(scrapy.Spider):
name = 'myspider'

def __init__(self, category=None, *args, **kwargs):
super(MySpider, self).__init__(*args, **kwargs)
self.start_urls = ['http://www.example.com/categories/%s' % category]

Generic Spiders

CrawlSpider

class scrapy.spiders.CrawlSpider

爬取一般网站常用的 spider 。其定义了一些规则(rule)来提供跟进 link 的方便的机制。 也许该 spider 并不是完全适合您的特定网站或项目,但其对很多情况都使用。 因此您可以以其为起点,根据需求修改部分方法。当然您也可以实现自己的 spider。

除了从 Spider 继承过来的(您必须提供的)属性外,其提供了一个新的属性:

  • rules

    一个包含一个(或多个) Rule 对象的集合(list)。 每个 Rule 对爬取网站的动作定义了特定表现。 Rule 对象在下边会介绍。 如果多个rule匹配了相同的链接,则根据他们在本属性中被定义的顺序,第一个会被使用。

该spider也提供了一个可复写(overrideable)的方法:

  • parse_start_url(response)

    当start_url的请求返回时,该方法被调用。 该方法分析最初的返回值并必须返回一个 Item 对象或者 一个 Request 对象或者 一个可迭代的包含二者对象。

爬取规则(Crawling rules)

class scrapy.spiders.Rule(link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=None)

  • link_extractor 是一个 Link Extractor 对象。 其定义了如何从爬取到的页面提取链接。

  • callback 是一个 callable 或 string (该spider中同名的函数将会被调用)。 从 link_extractor 中每获取到链接时将会调用该函数。该回调函数接受一个 response 作为其第一个参数, 并返回一个包含 Item 以及(或) Request 对象(或者这两者的子类)的列表(list)。

  • cb_kwargs 包含传递给回调函数的参数(keyword argument)的字典。

  • follow 是一个布尔(boolean)值,指定了根据该规则从 response 提取的链接是否需要跟进。 如果 callback 为None, follow 默认设置为 True ,否则默认为 False 。

  • process_links 是一个 callable 或 string (该spider中同名的函数将会被调用)。 从 link_extractor 中获取到链接列表时将会调用该函数。该方法主要用来过滤。

  • process_request 是一个 callable 或 string (该spider中同名的函数将会被调用)。 该规则提取到每个 request 时都会调用该函数。该函数必须返回一个request或者None。 (用来过滤request)

CrawlSpider样例

接下来给出配合rule使用CrawlSpider的例子:

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
import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class MySpider(CrawlSpider):
name = 'myspider'
allowed_domains = ['example.com']
start_urls = ['http://www.example.com']

rules = [
# 提取匹配 'category.php' (但不匹配 'subsection.php') 的链接并跟进链接(没有callback意味着follow默认为True)
Rule(LinkExtractor(allow=('category\.php', ), deny=('subsection\.php', ))),

# 提取匹配 'item.php' 的链接并使用spider的parse_item方法进行分析
Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item'),
]

def parse_item(self, response):
self.logger.info('Hi, this is an item page! %s', response.url)

item = scrapy.Item()
item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)')
item['name'] = response.xpath('//td[@id="item_name"]/text()').extract()
item['description'] = response.xpath('//td[@id="item_description"]/text()').extract()
return item

该 spider 将从 example.com 的首页开始爬取,获取 category 以及 item 的链接并对后者使用 parse_item 方法。 当 item 获得返回(response)时,将使用 XPath 处理 HTML 并生成一些数据填入 Item 中。

XMLFeedSpider

class scrapy.spiders.XMLFeedSpider

XMLFeedSpider 被设计用于通过迭代各个节点来分析 XML 源(XML feed)。 迭代器可以从 iternodes , xml , html 选择。 鉴于 xml 以及 html 迭代器需要先读取所有DOM再分析而引起的性能问题, 一般还是推荐使用 iternodes 。 不过使用 html 作为迭代器能有效应对错误的 XML。

您必须定义下列类属性来设置迭代器以及标签名(tag name):

  • iterator

    用于确定使用哪个迭代器的string。可选项有:

    • iternodes - 一个高性能的基于正则表达式的迭代器
    • html - 使用 Selector 的迭代器。 需要注意的是该迭代器使用DOM进行分析,其需要将所有的DOM载入内存, 当数据量大的时候会产生问题。
    • xml - 使用 Selector 的迭代器。 需要注意的是该迭代器使用DOM进行分析,其需要将所有的DOM载入内存, 当数据量大的时候会产生问题。

    默认值为 iternodes

  • itertag

    一个包含开始迭代的节点名的string。例如:
    itertag = 'product'

  • namespaces

    一个由 (prefix, url) 元组(tuple)所组成的 list。 其定义了在该文档中会被 spider 处理的可用的 namespace 。 prefix 及 uri 会被自动调用 register_namespace() 生成 namespace。

    您可以通过在 itertag 属性中制定节点的 namespace。

    例如:

    1
    2
    3
    4
    5
    class YourSpider(XMLFeedSpider):

    namespaces = [('n', 'http://www.sitemaps.org/schemas/sitemap/0.9')]
    itertag = 'n:url'
    # ...

除了这些新的属性之外,该spider也有以下可以覆盖(overrideable)的方法:

  • adapt_response(response)

    该方法在 spider 分析 response 前被调用。您可以在 response 被分析之前使用该函数来修改内容(body)。 该方法接受一个 response 并返回一个 response (可以相同也可以不同)。

  • parse_node(response, selector)

    当节点符合提供的标签名时(itertag)该方法被调用。 接收到的 response 以及相应的 Selector 作为参数传递给该方法。 该方法返回一个 Item 对象或者 Request 对象 或者一个包含二者的可迭代对象(iterable)。

  • process_results(response, results)

    当 spider 返回结果(item或request)时该方法被调用。 设定该方法的目的是在结果返回给框架核心(framework core)之前做最后的处理, 例如设定 item 的 ID 。其接受一个结果的列表(list of results)及对应的 response。 其结果必须返回一个结果的列表(list of results)(包含Item或者Request对象)。

XMLFeedSpider例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from scrapy.spiders import XmlFeedSpider
from myproject.items import TestItem

class MySpider(XmlFeedSpider):
name = 'example.com'
allowed_domains = ['example.com']
start_urls = ['http://www.example.com/feed.xml']
iterator = 'iternodes' # This is actually unnecessary, since it's the default value
itertag = 'item'

def parse_node(self, response, node):
self.logger.info('Hi, this is a <%s> node!: %s', self.itertag, ''.join(node.extract()))

item = TestItem()
item['id'] = node.xpath('@id').extract()
item['name'] = node.xpath('name').extract()
item['description'] = node.xpath('description').extract()
return item

简单来说,我们在这里创建了一个 spider ,从给定的 start_urls 中下载 feed , 并迭代 feed 中每个 item 标签,输出,并在 Item 中存储有些随机数据。

CSVFeedSpider

class scrapy.spiders.CSVFeedSpider

该 spider 除了其按行遍历而不是节点之外其他和 XMLFeedSpider 十分类似。 而其在每次迭代时调用的是 parse_row() 。

  • delimiter
    在CSV文件中用于区分字段的分隔符。类型为string。 默认为 ‘,’ (逗号)。

  • quotechar
    A string with the enclosure character for each field in the CSV file Defaults to ‘“‘ (quotation mark).

  • headers
    在CSV文件中包含的用来提取字段的行的列表。参考下边的例子。

  • parse_row(response, row)
    该方法接收一个 response 对象及一个以提供或检测出来的 header 为键的字典(代表每行)。 该 spider 中,您也可以覆盖 adapt_response 及 process_results 方法来进行预处理(pre-processing)及后(post-processing)处理。

CSVFeedSpider例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from scrapy.spiders import CSVFeedSpider
from myproject.items import TestItem

class MySpider(CSVFeedSpider):
name = 'example.com'
allowed_domains = ['example.com']
start_urls = ['http://www.example.com/feed.csv']
delimiter = ';'
quotechar = "'"
headers = ['id', 'name', 'description']

def parse_row(self, response, row):
self.logger.info('Hi, this is a row!: %r', row)

item = TestItem()
item['id'] = row['id']
item['name'] = row['name']
item['description'] = row['description']
return item

SitemapSpider

class scrapy.spiders.SitemapSpider

SitemapSpider 使您爬取网站时可以通过 Sitemaps 来发现爬取的URL。

其支持嵌套的 sitemap,并能从 robots.txt 中获取 sitemap 的url。

  • sitemap_urls
    包含您要爬取的 url 的 sitemap 的 url 列表(list)。 您也可以指定为一个 robots.txt ,spider 会从中分析并提取url。

  • sitemap_rules
    一个包含 (regex, callback) 元组的列表(list):

    • regex 是一个用于匹配从sitemap提供的url的正则表达式。 regex 可以是一个字符串或者编译的正则对象(compiled regex object)。

    • callback指定了匹配正则表达式的url的处理函数。 callback 可以是一个字符串(spider中方法的名字)或者是callable。

    例如:sitemap_rules = [('/product/', 'parse_product')]

    规则按顺序进行匹配,之后第一个匹配才会被应用。

    如果您忽略该属性,sitemap中发现的所有url将会被 parse 函数处理。

  • sitemap_follow

    一个用于匹配要跟进的sitemap的正则表达式的列表(list)。其仅仅被应用在使用 Sitemap index files 来指向其他 sitemap文件的站点。

    默认情况下所有的 sitemap 都会被跟进。

  • sitemap_alternate_links

    指定当一个 url 有可选的链接时,是否跟进。 有些非英文网站会在一个 url 块内提供其他语言的网站链接。

    例如:

    1
    2
    3
    4
    <url>
    <loc>http://example.com/</loc>
    <xhtml:link rel="alternate" hreflang="de" href="http://example.com/de"/>
    </url>

    sitemap_alternate_links 设置时,两个 URL 都会被获取。 当 sitemap_alternate_links 关闭时,只有 http://example.com/ 会被获取。

    默认 sitemap_alternate_links 关闭。

  • SitemapSpider样例

    简单的例子: 使用 parse 处理通过sitemap发现的所有url:

    1
    2
    3
    4
    5
    6
    7
    from scrapy.spiders import SitemapSpider

    class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/sitemap.xml']

    def parse(self, response):
    pass # ... scrape item here ...

    用特定的函数处理某些url,其他的使用另外的callback:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from scrapy.spiders import SitemapSpider

    class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/sitemap.xml']
    sitemap_rules = [
    ('/product/', 'parse_product'),
    ('/category/', 'parse_category'),
    ]

    def parse_product(self, response):
    pass # ... scrape product ...

    def parse_category(self, response):
    pass # ... scrape category ...

    跟进 robots.txt 文件定义的 sitemap 并只跟进包含有 ..sitemap_shop 的 url:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from scrapy.spiders import SitemapSpider

    class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/robots.txt']
    sitemap_rules = [
    ('/shop/', 'parse_shop'),
    ]
    sitemap_follow = ['/sitemap_shops']

    def parse_shop(self, response):
    pass # ... scrape shop here ...

    在SitemapSpider中使用其他url:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    from scrapy.spiders import SitemapSpider

    class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/robots.txt']
    sitemap_rules = [
    ('/shop/', 'parse_shop'),
    ]

    other_urls = ['http://www.example.com/about']

    def start_requests(self):
    requests = list(super(MySpider, self).start_requests())
    requests += [scrapy.Request(x, self.parse_other) for x in self.other_urls]
    return requests

    def parse_shop(self, response):
    pass # ... scrape shop here ...

    def parse_other(self, response):
    pass # ... scrape other here ...

Scrapy命令行

新建项目

scrapy startproject Demo

该命令会在当前目录下建立一个名为 Demo 的 scrapy 项目

控制项目

cd Demo 进入到项目目录中

scrapy genspider changoal changoal.cn
创建一个新的 spider,该命令会在 spiders 文件夹下新建一个叫 changoal.py 的文件。文件里有以下内容:

1
2
3
4
5
6
7
8
9
import scrapy

class ChangoalSpider(scrapy.Spider):
name = "changoal"
allowed_domains = ["changoal.cn"]
start_urls = ['http://changoal.cn/']

def parse(self, response):
pass

可用的工具命令

scrapy -h 可以查看所有可用的命令。

Scrapy提供了两种类型的命令。一种必须在Scrapy项目中运行(针对项目(Project-specific)的命令),另外一种则不需要(全局命令)。全局命令在项目中运行时的表现可能会与在非项目中运行有些许差别(因为可能会使用项目的设定)。

全局命令:

  • startproject
  • settings
  • runspider
  • shell
  • fetch
  • view
  • version

项目(Project-only)命令:

  • crawl
  • check
  • list
  • edit
  • parse
  • genspider
  • bench

startproject

  • 语法: scrapy startproject <project_name>
  • 是否需要项目: no

project_name 文件夹下创建一个名为 myproject 的 Scrapy 项目。
scrapy startproject myproject

genspider

  • 语法: scrapy genspider [-t template] <name> <domain>
  • 是否需要项目: yes

在当前项目中创建spider。

这仅仅是创建spider的一种快捷方法。该方法可以使用提前定义好的模板来生成spider。您也可以自己创建spider的源码文件。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ scrapy genspider -l
Available templates:
basic
crawl
csvfeed
xmlfeed

$ scrapy genspider -d basic
import scrapy

class $classname(scrapy.Spider):
name = "$name"
allowed_domains = ["$domain"]
start_urls = (
'http://www.$domain/',
)

def parse(self, response):
pass

$ scrapy genspider -t basic example example.com
Created spider 'example' using template 'basic' in module:
mybot.spiders.example

crawl

  • 语法:scrapy crawl <spider>
  • 是否需要项目: yes

使用spider进行爬取。
scrapy crawl myspider

check

  • 语法: scrapy check [-l] <spider>
  • 是否需要项目: yes

运行contract检查。(不懂有什么用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ scrapy check -l
first_spider
* parse
* parse_item
second_spider
* parse
* parse_item

$ scrapy check
[FAILED] first_spider:parse_item
>>> 'RetailPricex' field is missing

[FAILED] first_spider:parse
>>> Returned 92 requests, expected 0..4

list

  • 语法: scrapy list
  • 是否需要项目: yes

列出当前项目中所有可用的spider。每行输出一个spider。

edit

  • 语法: scrapy edit <spider>
  • 是否需要项目: yes
    使用 EDITOR 中设定的编辑器编辑给定的spider

该命令仅仅是提供一个快捷方式。开发者可以自由选择其他工具或者IDE来编写调试spider。

fetch

  • 语法: scrapy fetch <url>
  • 是否需要项目: no

使用Scrapy下载器(downloader)下载给定的URL,并将获取到的内容送到标准输出。

该命令以spider下载页面的方式获取页面。例如,如果spider有 USER_AGENT 属性修改了 User Agent,该命令将会使用该属性。

因此,您可以使用该命令来查看spider如何获取某个特定页面。

该命令如果非项目中运行则会使用默认Scrapy downloader设定。

scrapy fetch http://www.changoal.cn

view

  • 语法: scrapy view <url>
  • 是否需要项目: no

在浏览器中打开给定的URL,并以Scrapy spider获取到的形式展现。 有些时候spider获取到的页面和普通用户看到的并不相同。因此该命令可以用来检查spider所获取到的页面,并确认这是您所期望的。

scrapy view http://www.changoal.cn

shell

  • 语法: scrapy shell [url]
  • 是否需要项目: no

以给定的URL(如果给出)或者空(没有给出URL)启动Scrapy shell。

scrapy shell http://www.changoal.cn

parse

  • 语法: scrapy parse <url> [options]
  • 是否需要项目: yes

获取给定的URL并使用相应的 spider 分析处理。如果您提供 --callback 选项,则使用 spider 的该方法处理,否则使用 parse

支持的选项:

  • --spider=SPIDER: 跳过自动检测spider并强制使用特定的spider
  • --a NAME=VALUE: 设置spider的参数(可能被重复)
  • --callback or -c: spider中用于解析返回(response)的回调函数
  • --pipelines: 在pipeline中处理item
  • --rules or -r: 使用 CrawlSpider 规则来发现用来解析返回(response)的回调函数
  • --noitems: 不显示爬取到的item
  • --nolinks: 不显示提取到的链接
  • --nocolour: 避免使用pygments对输出着色
  • --depth or -d: 指定跟进链接请求的层次数(默认: 1)
  • --verbose or -v: 显示每个请求的详细信息

settings

  • 语法: scrapy settings [options]
  • 是否需要项目: no

获取Scrapy的设定。

在项目中运行时,该命令将会输出项目的设定值,否则输出Scrapy默认设定。

1
2
3
4
$ scrapy settings --get BOT_NAME
scrapybot
$ scrapy settings --get DOWNLOAD_DELAY
0

runspider

  • 语法: scrapy runspider <spider_file.py>
  • 是否需要项目: no

在未创建项目的情况下,运行一个编写在Python文件中的spider。

scrapy runspider myspider.py

version

  • 语法: scrapy version [-v]
  • 是否需要项目: no

输出Scrapy版本。配合 -v 运行时,该命令同时输出Python, Twisted以及平台的信息,方便bug提交。

自定义项目命令

您也可以通过 COMMANDS_MODULE 来添加您自己的项目命令。您可以以 scrapy/commandsScrapy commands 为例来了解如何实现您的命令。

Scrapy 入门

scrapy 介绍

scrapy 是一个基于 Python 语言的爬虫框架。有了框架的存在,我们可以更方便的爬虫了,框架帮我们封装好了下载等模块,而且更重要的是框架使用了异步的模式,加快了爬虫的速度。

环境搭建

在命令行中执行 conda install Scrapy 这样就安装好了 Scrapy 模块,是不是特别简单。ahahahha

开始

接下来就要开始我们的 scrapy 之路了。

新建项目

使用 Scrapy 第一步:创建项目,命令行进入你需要放置项目的目录,然后执行

scrapy startproject ScrapyDemo #ScrapyDemo是项目名称

目录结构

这时会在目录下多出一个 ScrapyDemo 文件夹,这就是 Scrapy 项目了,项目的结构如下

1
2
3
4
5
6
7
8
9
10
11
.
|-- ScrapyDemo
| `-- ScrapyDemo
| |-- spiders
| | `-- __init__.py
| |-- __init__.py
| |-- items.py
| |-- middlewares.py
| |-- pipelines.py
| `-- settings.py
`-- scrapy.cfg

目录说明:

  • ScrapyDemo(外层) 项目总目录
  • ScrapyDemo(内层) 项目目录
  • scrapy.cfg 项目的配置文件
  • spiders 放置我们的爬虫代码的目录
  • items.py 定义我们需要获取的字段
  • middlewares.py
  • pipelines.py 用来定义存储
  • settings.py 项目设置文件

还有一点要注意的是,Scrapy 默认是不能在 IDE 中调试的,所以要在项目总目录下新建一个 entrypoint.py 文件,写下以下内容:

1
2
3
4
from scrapy.cmdline import execute

# 前两个参数是不变的,第三个参数是自己定义的 spider 的名字
execute(['scrapy', 'crawl', 'dingdian'])

现在整个目录应该是这样(盗图..)

架构

下面我们先来看一下框架的架构图

Scrapy Engine: 这是引擎,负责Spiders、ItemPipeline、Downloader、Scheduler中间的通讯,信号、数据传递等等!(像不像人的身体?)

Scheduler(调度器): 它负责接受引擎发送过来的requests请求,并按照一定的方式进行整理排列,入队、并等待Scrapy Engine(引擎)来请求时,交给引擎。

Downloader(下载器):负责下载Scrapy Engine(引擎)发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spiders来处理,

Spiders:它负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器),

Item Pipeline:它负责处理Spiders中获取到的Item,并进行处理,比如去重,持久化存储(存数据库,写入文件,总之就是保存数据用的)

Downloader Middlewares(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件

Spider Middlewares(Spider中间件):你可以理解为是一个可以自定扩展和操作引擎和Spiders中间‘通信’的功能组件(比如进入Spiders的Responses;和从Spiders出去的Requests)

数据在整个Scrapy的流向:

程序运行的时候,

引擎:Hi!Spider, 你要处理哪一个网站?

Spiders:我要处理23wx.com

引擎:你把第一个需要的处理的URL给我吧。

Spiders:给你第一个URL是XXXXXXX.com

引擎:Hi!调度器,我这有request你帮我排序入队一下。

调度器:好的,正在处理你等一下。

引擎:Hi!调度器,把你处理好的request给我,

调度器:给你,这是我处理好的request

引擎:Hi!下载器,你按照下载中间件的设置帮我下载一下这个request

下载器:好的!给你,这是下载好的东西。(如果失败:不好意思,这个request下载失败,然后引擎告诉调度器,这个request下载失败了,你记录一下,我们待会儿再下载。)

引擎:Hi!Spiders,这是下载好的东西,并且已经按照Spider中间件处理过了,你处理一下(注意!这儿responses默认是交给def parse这个函数处理的)

Spiders:(处理完毕数据之后对于需要跟进的URL),Hi!引擎,这是我需要跟进的URL,将它的responses交给函数 def xxxx(self, responses)处理。还有这是我获取到的Item。

引擎:Hi !Item Pipeline 我这儿有个item你帮我处理一下!调度器!这是我需要的URL你帮我处理下。然后从第四步开始循环,直到获取到你需要的信息,

注意!只有当调度器中不存在任何request了,整个程序才会停止,(也就是说,对于下载失败的 URL,Scrapy会重新下载。)

写代码

建立一个项目之后:

第一件事就是在 items.py 文件中定义我们需要的字段,用来临时存储要保存的数据,方便以后数据的持久化存储,比如 数据库 文本文件等。

第二件事是在 spiders.py 中编写自己的爬虫代码

第三件事是在 pipelines.py 中存储自己的数据

建议:在大家调试的时候在settings.py中取消下面几行的注释:

1
2
3
4
5
HTTPCACHE_ENABLED = True
HTTPCACHE_EXPIRATION_SECS = 0
HTTPCACHE_DIR = 'httpcache'
HTTPCACHE_IGNORE_HTTP_CODES = []
HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

上面代码的作用是 Scrapy 会缓存你有的 Requests! 当你再次请求时,如果存在缓存文档则返回缓存文档,而不是去网站请求,这样既加快了本地调试速度,也减轻了 网站的压力。一举多得

定义字段

要根据自己要爬取的内容来定义字段。比如,在这里我们要爬的是小说站点就需要定义,小说名字,作者,小说地址,连载状态,字数,文章类别 等字段。

就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ScrapydemoItem(scrapy.Item):
# 小说名字
name = scrapy.Field()
# 作者
author = scrapy.Field()
# 小说地址
novelurl = scrapy.Field()
# 连载状态
serialstatus = scrapy.Field()
# 连载字数
serialnumber = scrapy.Field()
# 类别
category = scrapy.Field()
# 编号
novel_id = scrapy.Field()

编写 spider

在 spiders 目录下新建一个 ScrapyDemo.py 文件,并导入我们需用的模块

1
2
3
4
5
6
7
import scrapy 
import re
from bs4 import BeautifulSoup
from scrapy.http import Request
from ScrapyDemo.items import ScrapydemoItem

class Muspider(scrapy.Spider):

我们需要从一个地址入手开始爬取,我在顶点小说上没有发现有全站小说地址,但是我找到每个分类地址全部小说:

玄幻魔幻:http://www.x23us.com/class/1_1.html

武侠修真:http://www.x23us.com/class/2_1.html

都市言情:http://www.x23us.com/class/3_1.html

历史军事:http://www.x23us.com/class/4_1.html

侦探推理:http://www.x23us.com/class/5_1.html

网游动漫:http://www.x23us.com/class/6_1.html

科幻小说:http://www.x23us.com/class/7_1.html

恐怖灵异:http://www.x23us.com/class/8_1.html

散文诗词:http://www.x23us.com/class/9_1.html

其他:http://www.x23us.com/class/10_1.html

全本:http://www.x23us.com/quanben/1

好啦!入口地址我们找到了,现在开始写第一部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import scrapy
import re
from bs4 import BeautifulSoup
from scrapy.http import Request
from ScrapyDemo.items import ScrapydemoItem

class Myspider(scrapy.Spider):
name = 'ScrapyDemo'
allowed_domains = ['x23us.com']
bash_url = 'http://www.x23us.com/class/'
bashurl = '.html'

def start_requests(self):
for i in range(1, 11):
url = self.bash_url + str(i) + '_1' + self.bashurl
yield Request(url,self.parse)

def parse(self, response):
print(response.text)

首先我们创建一个类 Myspider;这个类继承自 scrapy.Spider

定义了一个 allowed_domains ;这个不是必须的;但是在某写情况下需要用得到,比如使用爬取规则的时候就需要了;它的作用是只会跟进存在于 allowed_domains 中的 URL。不存在的 URL 会被忽略。

第九行定义的 name 是之前我们在 entrypoint.py 文件中的第三个参数 此 name 在整个项目中有且只能有一个、名字不可重复!!!

之后可以运行 entrypoint.py 文件来检查一下代码时候可以正常工作。

请求就这么轻而易举的实现了啊!简直So Easy!

继续 继续!

我们需要历遍所有页面才能取得所有的小说页面连接:

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
import scrapy
import re
from bs4 import BeautifulSoup
from scrapy.http import Request
from ScrapyDemo.items import ScrapydemoItem


class Muspider(scrapy.Spider):
name = 'ScrapyDemo'
allowed_domains = ['x23us.com']
bash_url = 'http://www.x23us.com/class/'
bashurl = '.html'

def start_requests(self):
for i in range(1, 11):
url = self.bash_url + str(i) + '_1' + self.bashurl
yield Request(url, self.parse)

def parse(self, response):
max_span = BeautifulSoup(response.text, 'lxml').find('div', class_='pagelink').find_all('a')[-1].get_text()
bashurl = str(response.url)[:-6]
for num in range(1, int(max_span) + 1):
url = bashurl + str(num) + self.bashurl
yield Request(url, self.get_name)

def get_name(self, response):
novels = BeautifulSoup(str(response), 'lxml').find_all('tr', bgcolor='#F2F2F2')
for navel in novels:
a = navel.find('a')
name = a['title']
url = a['href']
yield Request(url, callback=self.get_chapterurl, meta={'name': name, 'url': url})

def get_chapterurl(self, response):
item = ScrapydemoItem()
item['name'] = str(response.meta['name']).replace('\xa0', '')
item['novelurl'] = response.meta['url']
category = BeautifulSoup(response.text, 'lxml').find('table').find('a').get_text()
author = BeautifulSoup(response.text, 'lxml').find('table').find_all('td')[1].get_text()
bash_url = BeautifulSoup(response.text, 'lxml').find('p', class_='btnlinks').find('a', class_='read')['href']
name_id = str(bash_url)[-6:-1].replace('/', '')
item['category'] = str(category).replace('/', '')
item['author'] = str(author).replace('/', '')
item['name_id'] = name_id
return item

自定义 Pipeline

做一个自定义的MySQL的Pipeline。首先为了能好区分框架自带的Pipeline,我们把MySQL的Pipeline单独放到一个目录里面。

pipelines.py 这个是我们写存放数据的文件

sql.py 看名字就知道,需要的sql语句。

首先是需要的 MySQL 表,

1
2
3
4
5
6
7
8
9
DROP TABLE IF EXISTS `dd_name`;
CREATE TABLE `dd_name` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`xs_name` varchar(255) DEFAULT NULL,
`xs_author` varchar(255) DEFAULT NULL,
`category` varchar(255) DEFAULT NULL,
`name_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4;

记得在 settings.py 文件中定义好 MySQL 的配置文件

1
2
3
4
5
MYSQL_HOSTS = '127.0.0.1'
MYSQL_USER = 'root'
MYSQL_PORT = '3306'
MYSQL_PASSWORD = '****'
MYSQL_DB = 'xiaoshuo'

在开始写sql.py之前,我们需要安装一个Python操作MySQL的包,来自MySQL官方的一个包:点我下载

下载完成后解压出来,从 cmd 进入该目录的绝对路径,然后 python setup.py install ;即可完成安装(记得以管理员身份打开命令行)

sql.py

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
import mysql.connector

from ScrapyDemo import settings

MYSQL_USER = settings.MYSQL_USER
MYSQL_PASSWORD = settings.MYSQL_PASSWORD
MYSQL_HOST = settings.MYSQL_HOSTS
MYSQL_PORT = settings.MYSQL_PORT
MYSQL_DB = settings.MYSQL_DB

cnx = mysql.connector.connect(user=MYSQL_USER, password=MYSQL_PASSWORD, host=MYSQL_HOST, database=MYSQL_DB)
cur = cnx.cursor(buffered=True)


class Sql:
@classmethod
def insert_dd_name(cls, xs_name, xs_author, category, name_id):
sql = 'INSERT INTO dd_name (`xs_name`,`xs_author`,`category`,``name_id) VALUES (%(xs_name)s,%(xs_author)s,,%(category)s,,%(name_id)s)'
VALUE = {
'xs_name': xs_name,
'xs_author': xs_author,
'category': category,
'name_id': name_id
}
cur.execute(sql, VALUE)
cnx.commit()

@classmethod
def select_name(cls, name_id):
sql = 'SELECT EXISTS(SELECT 1 FROM dd_name WHERE name_id=%(name_id)s)'
value = {
'name_id': name_id
}
cur.execute(sql, value)
cnx.commit()

来开始写pipeline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from ScrapyDemo.items import ScrapydemoItem
from .sql import Sql


class ScrapyPipeline(object):
def process_item(self, item, sqider):
if isinstance(item, ScrapydemoItem):
name_id = item['name_id']
ret = Sql.select_name(name_id)
if ret[0] == 1:
print('已经存在了')
pass
else:
xs_name = item['xs_name']
xs_author = item['xs_author']
category = item['category']
Sql.insert_dd_name(xs_name, xs_author, category, name_id)
print('开始存小说标题')

定义了一个process_item函数并有,item和spider这两个参数(请注意啊!这两玩意儿 务必!!!要加上!!千万不能少!!!!务必!!!务必!!!

搞完!下面我们启用这个Pipeline在settings中作如下设置:

后面的 1 是优先级程度(1-1000随意设置,数值越低,组件的优先级越高)

下面我们开始还剩下的一些内容获取:小说章节 和章节内容

首先我们在 item.py 中新定义一些需要获取内容的字段:

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
import scrapy

class ScrapydemoItem(scrapy.Item):
# 小说名字
name = scrapy.Field()
# 作者
author = scrapy.Field()
# 小说地址
novelurl = scrapy.Field()
# 连载状态
serialstatus = scrapy.Field()
# 连载字数
serialnumber = scrapy.Field()
# 类别
category = scrapy.Field()
# 编号
novel_id = scrapy.Field()


class Content(scrapy.Item):
id_name = scrapy.Field()
chaptercontent = scrapy.Field()
num = scrapy.Field()
chapter_url = scrapy.Field()
chapter_name = scrapy.Field()

继续编写Spider文件:

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
def get_chapterurl(self, response):
item = ScrapydemoItem()
item['name'] = str(response.meta['name']).replace('\xa0', '')
item['novelurl'] = response.meta['url']
category = BeautifulSoup(response.text, 'lxml').find('table').find('a').get_text()
author = BeautifulSoup(response.text, 'lxml').find('table').find_all('td')[1].get_text()
bash_url = BeautifulSoup(response.text, 'lxml').find('p', class_='btnlinks').find('a', class_='read')['href']
name_id = str(bash_url)[-6:-1].replace('/', '')
item['category'] = str(category).replace('/', '')
item['author'] = str(author).replace('/', '')
item['name_id'] = name_id
yield item
yield Request(bash_url, callback=self.get_chapter, meta={'name_id': name_id})

def get_chapter(self, response):
urls = re.findall(r'<td class="L"><a href="(.*?)">(.*?)</a></td>', response.text)
num = 0
for url in urls:
num = num + 1
chapterurl = response.url + url[0]
chaptername = url[1]
yield Request(chapterurl, callback=self.get_chaptercontent, meta={
'num': num,
'name_id': response.meta['name_id'],
'chapter_url': chapterurl,
'chaptername': chaptername
})

def get_chaptercontent(self, response):
item = Content()
item['id_name'] = response.meta['name_id']
item['num'] = response.meta['num']
item['chapter_url'] = response.meta['chapter_url']
item['chapter_name'] = response.meta['chaptername']
content = BeautifulSoup(response.text, 'lxml').find('dd', id='contents').get_text()
item['content'] = str(content).replace('\xa0', '')
return item

注意 13\14 行,这个地方返回item是不能用return的哦!用了就结束了,程序就不会继续下去了,得用 yield

下面我们来写存储这部分spider的Pipeline:

数据表:

1
2
3
4
5
6
7
8
9
10
11
DROP TABLE IF EXISTS `dd_chaptername`;
CREATE TABLE `dd_chaptername` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`xs_chaptername` varchar(255) DEFAULT NULL,
`xs_content` text,
`id_name` int(11) DEFAULT NULL,
`num_id` int(11) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2726 DEFAULT CHARSET=gb18030;
SET FOREIGN_KEY_CHECKS=1;


下面是Pipeline:

有小伙伴注意,这儿比上面一个Pipeline少一个判断,因为我把判断移动到Spider中去了,这样就可以减少一次Request,减轻服务器压力。

改变后的Spider长这样:

妹子图爬虫第四弹

多线程多进程爬虫

之前我们的程序都可以用,但是速度太慢了,因为我们只用了一个线程一个进程来进行爬虫,大部分时间都在等待,效率太低。所以接下来我们要加快我们的爬虫效率。

同上篇,我们用数据库的方式来解决线程以及进程间的通信问题。

要爬取的 url 总共有三种状态:

  • OUTSTANDING(初始状态)
  • PROCESSING(正在下载状态)
  • COMPLETE(下载完成状态)

当一个所有初始的URL状态都为outstanding;当开始爬取的时候状态改为:processing;爬取完成状态改为:complete;失败的URL重置状态为:outstanding。为了能够处理URL进程被终止的情况、我们设置一个计时参数,当超过这个值时;我们则将状态重置为outstanding。下面是代码实现:

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
83
84
85
86
87
88
from datetime import datetime, timedelta
from pymongo import MongoClient, errors

class MogoQueue():

OUTSTANDING = 1 ##初始状态
PROCESSING = 2 ##正在下载状态
COMPLETE = 3 ##下载完成状态

def __init__(self, db, collection, timeout=300):##初始mongodb连接
self.client = MongoClient()
self.Client = self.client[db]
self.db = self.Client[collection]
self.timeout = timeout

def __bool__(self):
"""
这个函数,我的理解是如果下面的表达为真,则整个类为真
至于有什么用,后面我会注明的(如果我的理解有误,请指点出来谢谢,我也是Python新手)
$ne的意思是不匹配
"""
record = self.db.find_one(
{'status': {'$ne': self.COMPLETE}}
)
return True if record else False

def push(self, url, title): ##这个函数用来添加新的URL进队列
try:
self.db.insert({'_id': url, 'status': self.OUTSTANDING, '主题': title})
print(url, '插入队列成功')
except errors.DuplicateKeyError as e: ##报错则代表已经存在于队列之中了
print(url, '已经存在于队列中了')
pass
def push_imgurl(self, title, url):
try:
self.db.insert({'_id': title, 'statue': self.OUTSTANDING, 'url': url})
print('图片地址插入成功')
except errors.DuplicateKeyError as e:
print('地址已经存在了')
pass

def pop(self):
"""
这个函数会查询队列中的所有状态为OUTSTANDING的值,
更改状态,(query后面是查询)(update后面是更新)
并返回_id(就是我们的URL),MongDB好使吧,^_^
如果没有OUTSTANDING的值则调用repair()函数重置所有超时的状态为OUTSTANDING,
$set是设置的意思,和MySQL的set语法一个意思
"""
record = self.db.find_and_modify(
query={'status': self.OUTSTANDING},
update={'$set': {'status': self.PROCESSING, 'timestamp': datetime.now()}}
)
if record:
return record['_id']
else:
self.repair()
raise KeyError

def pop_title(self, url):
record = self.db.find_one({'_id': url})
return record['主题']

def peek(self):
"""这个函数是取出状态为 OUTSTANDING的文档并返回_id(URL)"""
record = self.db.find_one({'status': self.OUTSTANDING})
if record:
return record['_id']

def complete(self, url):
"""这个函数是更新已完成的URL完成"""
self.db.update({'_id': url}, {'$set': {'status': self.COMPLETE}})

def repair(self):
"""这个函数是重置状态$lt是比较"""
record = self.db.find_and_modify(
query={
'timestamp': {'$lt': datetime.now() - timedelta(seconds=self.timeout)},
'status': {'$ne': self.COMPLETE}
},
update={'$set': {'status': self.OUTSTANDING}}
)
if record:
print('重置URL状态', record['_id'])

def clear(self):
"""这个函数只有第一次才调用、后续不要调用、因为这是删库啊!"""
self.db.drop()

接下来就需要来爬取所有的链接地址来存进我们的数据库里了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Download import request
from mongodb_queue import MogoQueue
from bs4 import BeautifulSoup


spider_queue = MogoQueue('meinvxiezhenji', 'crawl_queue')
def start(url):
response = request.get(url, 3)
Soup = BeautifulSoup(response.text, 'lxml')
all_a = Soup.find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
url = a['href']
spider_queue.push(url, title)
"""上面这个调用就是把URL写入MongoDB的队列了"""

if __name__ == "__main__":
start('http://www.mzitu.com/all')

"""这一段儿就不解释了哦!超级简单的"""
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
83
84
85
import os
import time
import threading
import multiprocessing
from mongodb_queue import MogoQueue
from Download import request
from bs4 import BeautifulSoup

SLEEP_TIME = 1

def mzitu_crawler(max_threads=10):
crawl_queue = MogoQueue('meinvxiezhenji', 'crawl_queue') ##这个是我们获取URL的队列
##img_queue = MogoQueue('meinvxiezhenji', 'img_queue')
def pageurl_crawler():
while True:
try:
url = crawl_queue.pop()
print(url)
except KeyError:
print('队列没有数据')
break
else:
img_urls = []
req = request.get(url, 3).text
title = crawl_queue.pop_title(url)
mkdir(title)
os.chdir('D:\mzitu\\' + title)
max_span = BeautifulSoup(req, 'lxml').find('div', class_='pagenavi').find_all('span')[-2].get_text()
for page in range(1, int(max_span) + 1):
page_url = url + '/' + str(page)
img_url = BeautifulSoup(request.get(page_url, 3).text, 'lxml').find('div', class_='main-image').find('img')['src']
img_urls.append(img_url)
save(img_url)
crawl_queue.complete(url) ##设置为完成状态
##img_queue.push_imgurl(title, img_urls)
##print('插入数据库成功')

def save(img_url):
name = img_url[-9:-4]
print(u'开始保存:', img_url)
img = request.get(img_url, 3)
f = open(name + '.jpg', 'ab')
f.write(img.content)
f.close()

def mkdir(path):
path = path.strip()
isExists = os.path.exists(os.path.join("D:\mzitu", path))
if not isExists:
print(u'建了一个名字叫做', path, u'的文件夹!')
os.makedirs(os.path.join("D:\mzitu", path))
return True
else:
print(u'名字叫做', path, u'的文件夹已经存在了!')
return False

threads = []
while threads or crawl_queue:
"""
这儿crawl_queue用上了,就是我们__bool__函数的作用,为真则代表我们MongoDB队列里面还有数据
threads 或者 crawl_queue为真都代表我们还没下载完成,程序就会继续执行
"""
for thread in threads:
if not thread.is_alive(): ##is_alive是判断是否为空,不是空则在队列中删掉
threads.remove(thread)
while len(threads) < max_threads or crawl_queue.peek(): ##线程池中的线程少于max_threads 或者 crawl_qeue时
thread = threading.Thread(target=pageurl_crawler) ##创建线程
thread.setDaemon(True) ##设置守护线程
thread.start() ##启动线程
threads.append(thread) ##添加进线程队列
time.sleep(SLEEP_TIME)

def process_crawler():
process = []
num_cpus = multiprocessing.cpu_count()
print('将会启动进程数为:', num_cpus)
for i in range(num_cpus):
p = multiprocessing.Process(target=mzitu_crawler) ##创建进程
p.start() ##启动进程
process.append(p) ##添加进进程队列
for p in process:
p.join() ##等待进程队列里面的进程结束

if __name__ == "__main__":
process_crawler()

妹子图爬虫第三弹

前面两篇的教程教大家写了一个基础的爬虫程序,但是有问题啊,每次开始时都要重新下载,很难受。所以我们要解决这个问题,关键点在于要把我们爬过的页面记录下来,避免重复。在这里,原文作者使用 MongoDB (一个基于分布式文件存储的非关系型数据库) 来存储数据的,我也是不怎么明白什么是 非关系型数据库…不过这里有教程,大家可以看看。

首先是 MongoDB 的安装。例如把它安在 C 盘下 D:\software\MongoDB\Server ,之后需要创建两个目录:

D:\software\MongoDB\mongod.log(文件) 存储日志

D:\software\MongoDB\db 存储数据

然后以管理员身份打开命令行窗口,执行以下命令

"D:\software\MongoDB\Server\3.4\bin\mongod.exe" --config "D:\software\MongoDB\Server\3.4\mongod.cfg" --install

如图安装成功

用命令 net start MongoDB 来启动服务。

对了,还需要安装 MongoDB 的 python 模块 pip install PyMongo

现在我们在上一篇博文完成的代码中导入模块:

from pymongo import MongoClient

代码改造第一步,在类 mzitu 里添加一个函数:

1
2
3
4
5
6
7
def __init__(self):
client = MongoClient() ##与MongDB建立连接(这是默认连接本地MongDB数据库)
db = client['meinvxiezhenji'] ## 选择一个数据库
self.meizitu_collection = db['meizitu'] ##在meizixiezhenji这个数据库中,选择一个集合
self.title = '' ##用来保存页面主题
self.url = '' ##用来保存页面地址
self.img_urls = [] ##初始化一个 列表 用来保存图片地址

之后要改一下 all_url 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def all_url(self, url):
html = down.get(url, 3)
all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
self.title = title ##将主题保存到self.title中
print(u'开始保存:', title)
path = str(title).replace("?", '_')
self.mkdir(path)
os.chdir("D:\mzitu\\"+path)
href = a['href']
self.url = href ##将页面地址保存到self.url中
if self.meizitu_collection.find_one({'主题页面': href}): ##判断这个主题是否已经在数据库中、不在就运行else下的内容,在则忽略。
print(u'这个页面已经爬取过了')
else:
self.html(href)

接着改 html 函数:

1
2
3
4
5
6
7
8
def html(self, href):
html = down.get(href, 3)
max_span = BeautifulSoup(html.text, 'lxml').find_all('span')[10].get_text()
page_num = 0 ##这个当作计数器用 (用来判断图片是否下载完毕)
for page in range(1, int(max_span) + 1):
page_num = page_num + 1 ##每for循环一次就+1 (当page_num等于max_span的时候,就证明我们的在下载最后一张图片了)
page_url = href + '/' + str(page)
self.img(page_url, max_span, page_num) ##把上面我们我们需要的两个变量,传递给下一个函数。

改 img 函数…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def img(self, page_url, max_span, page_num): ##添加上面传递的参数
img_html = down.get(page_url, 3)
img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
self.img_urls.append(img_url) ##每一次 for page in range(1, int(max_span) + 1)获取到的图片地址都会添加到 img_urls这个初始化的列表
if int(max_span) == page_num: ##我们传递下来的两个参数用上了 当max_span和Page_num相等时,就是最后一张图片了,最后一次下载图片并保存到数据库中。
self.save(img_url)
post = { ##这是构造一个字典,里面有啥都是中文,很好理解吧!
'标题': self.title,
'主题页面': self.url,
'图片地址': self.img_urls,
'获取时间': datetime.datetime.now()
}
self.meizitu_collection.save(post) ##将post中的内容写入数据库。
print(u'插入数据库成功')
else: ##max_span 不等于 page_num执行这下面
self.save(img_url)

完整的代码在此

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
from bs4 import BeautifulSoup
import os
from Download import down ##导入模块变了一下
from pymongo import MongoClient
import datetime

class mzitu():

def __init__(self):
client = MongoClient() ##与MongDB建立连接(这是默认连接本地MongDB数据库)
db = client['meinvxiezhenji'] ## 选择一个数据库
self.meizitu_collection = db['meizitu'] ##在meizixiezhenji这个数据库中,选择一个集合
self.title = '' ##用来保存页面主题
self.url = '' ##用来保存页面地址
self.img_urls = [] ##初始化一个 列表 用来保存图片地址

def all_url(self, url):
html = down.get(url, 3)
all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
self.title = title ##将主题保存到self.title中
print(u'开始保存:', title)
path = str(title).replace("?", '_')
self.mkdir(path)
os.chdir("D:\mzitu\\"+path)
href = a['href']
self.url = href ##将页面地址保存到self.url中
if self.meizitu_collection.find_one({'主题页面': href}): ##判断这个主题是否已经在数据库中、不在就运行else下的内容,在则忽略。
print(u'这个页面已经爬取过了')
else:
self.html(href)

def html(self, href):
html = down.get(href, 3)
max_span = BeautifulSoup(html.text, 'lxml').find_all('span')[10].get_text()
page_num = 0 ##这个当作计数器用 (用来判断图片是否下载完毕)
for page in range(1, int(max_span) + 1):
page_num = page_num + 1 ##每for循环一次就+1 (当page_num等于max_span的时候,就证明我们的在下载最后一张图片了)
page_url = href + '/' + str(page)
self.img(page_url, max_span, page_num) ##把上面我们我们需要的两个变量,传递给下一个函数。

def img(self, page_url, max_span, page_num): ##添加上面传递的参数
img_html = down.get(page_url, 3)
img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
self.img_urls.append(img_url) ##每一次 for page in range(1, int(max_span) + 1)获取到的图片地址都会添加到 img_urls这个初始化的列表
if int(max_span) == page_num: ##我们传递下来的两个参数用上了 当max_span和Page_num相等时,就是最后一张图片了,最后一次下载图片并保存到数据库中。
self.save(img_url)
post = { ##这是构造一个字典,里面有啥都是中文,很好理解吧!
'标题': self.title,
'主题页面': self.url,
'图片地址': self.img_urls,
'获取时间': datetime.datetime.now()
}
self.meizitu_collection.save(post) ##将post中的内容写入数据库。
print(u'插入数据库成功')
else: ##max_span 不等于 page_num执行这下面
self.save(img_url)


def save(self, img_url):
name = img_url[-9:-4]
print(u'开始保存:', img_url)
img = down.get(img_url, 3)
f = open(name + '.jpg', 'ab')
f.write(img.content)
f.close()

def mkdir(self, path):
path = path.strip()
isExists = os.path.exists(os.path.join("D:\mzitu", path))
if not isExists:
print(u'建了一个名字叫做', path, u'的文件夹!')
os.makedirs(os.path.join("D:\mzitu", path))
return True
else:
print(u'名字叫做', path, u'的文件夹已经存在了!')
return False


Mzitu = mzitu() ##实例化
Mzitu.all_url('http://www.mzitu.com/all') ##给函数all_url传入参数 你可以当作启动爬虫(就是入口)

妹子图爬虫第二弹

上一篇文章中教大家去爬取妹子图,但是会出现很多问题,比如爬着爬着发现一直在报错,重新再来的话又得从第一张图开始下,太麻烦。通常是因为网站的反爬虫策略起了作用。

一般反爬虫策略有以下几种:

  • 后台对访问进行统计,如果单个IP访问超过阈值,予以封锁。
  • 后台对访问进行统计,如果单个session访问超过阈值,予以封锁。
  • 后台对访问进行统计,如果单个userAgent访问超过阈值,予以封锁。
  • 以上的组合。

针对上面的第一二条,来写个基本的下载模块。原理就是利用的不同的 User-Ahent 和 IP 来进行爬虫。

首先随便就可以在网上找到很多 User-Agent 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"

新建个下载的类然后改下代码

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
import requests
import re
import random

class download(object):
def __init__(self):
self.user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

def get(self, url):
UA = random.choice(self.user_agent_list) ##从self.user_agent_list中随机取出一个字符串(聪明的小哥儿一定发现了这是完整的User-Agent中:后面的一半段)
headers = {'User-Agent': UA} ##构造成一个完整的User-Agent (UA代表的是上面随机取出来的字符串哦)
response = requests.get(url, headers=headers) ##这样服务器就会以为我们是真的浏览器了
return response

下一步就是要找一些能用的 IP 了。网上有一些 IP 代理的网站,通常会发一些免费的代理 IP,比如 http://haoip.cc/tiqu.htm

下面的代码可以爬下来这些 IP。

1
2
3
4
5
6
7
8
iplist = [] ##初始化一个list用来存放我们获取到的IP
html = requests.get("http://haoip.cc/tiqu.htm")##不解释咯
iplistn = re.findall(r'r/>(.*?)<b', html.text, re.S) ##表示从html.text中获取所有r/><b中的内容,re.S的意思是包括匹配包括换行符,findall返回的是个list哦!
for ip in iplistn:
i = re.sub('\n', '', ip)##re.sub 是re模块替换的方法,这儿表示将\n替换为空
iplist.append(i.strip()) ##添加到我们上面初始化的list里面, i.strip()的意思是去掉字符串的空格哦!!
print(i.strip())
print(iplist)

然后一顿操作

Download.py

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
import requests
import re
import random
import time


class download():

def __init__(self):

self.iplist = [] ##初始化一个list用来存放我们获取到的IP
html = requests.get("http://haoip.cc/tiqu.htm") ##不解释咯
iplistn = re.findall(r'r/>(.*?)<b', html.text, re.S) ##表示从html.text中获取所有r/><b中的内容,re.S的意思是包括匹配包括换行符,findall返回的是个list哦!
for ip in iplistn:
i = re.sub('\n', '', ip) ##re.sub 是re模块替换的方法,这儿表示将\n替换为空
self.iplist.append(i.strip()) ##添加到我们上面初始化的list里面

self.user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

def get(self, url, timeout, proxy=None, num_retries=6): ##给函数一个默认参数proxy为空
UA = random.choice(self.user_agent_list) ##从self.user_agent_list中随机取出一个字符串
headers = {'User-Agent': UA} ##构造成一个完整的User-Agent (UA代表的是上面随机取出来的字符串哦)

if proxy == None: ##当代理为空时,不使用代理获取response(别忘了response啥哦!之前说过了!!)
try:
return requests.get(url, headers=headers, timeout=timeout)##这样服务器就会以为我们是真的浏览器了
except:##如过上面的代码执行报错则执行下面的代码

if num_retries > 0: ##num_retries是我们限定的重试次数
time.sleep(10) ##延迟十秒
print(u'获取网页出错,10S后将获取倒数第:', num_retries, u'次')
return self.get(url, timeout, num_retries-1) ##调用自身 并将次数减1
else:
print(u'开始使用代理')
time.sleep(10)
IP = ''.join(str(random.choice(self.iplist)).strip()) ##下面有解释哦
proxy = {'http': IP}
return self.get(url, timeout, proxy,) ##代理不为空的时候

else: ##当代理不为空
try:
IP = ''.join(str(random.choice(self.iplist)).strip()) ##将从self.iplist中获取的字符串处理成我们需要的格式(处理了些什么自己看哦,这是基础呢)
proxy = {'http': IP} ##构造成一个代理
return requests.get(url, headers=headers, proxies=proxy, timeout=timeout) ##使用代理获取response
except:

if num_retries > 0:
time.sleep(10)
IP = ''.join(str(random.choice(self.iplist)).strip())
proxy = {'http': IP}
print(u'正在更换代理,10S后将重新获取倒数第', num_retries, u'次')
print(u'当前代理是:', proxy)
return self.get(url, timeout, proxy, num_retries - 1)
else:
print(u'代理也不好使了!取消代理')
return self.get(url, 3)

request = download()

再改下 mzitu.py 中的代码

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
from bs4 import BeautifulSoup
import os
from Download import request ##导入模块变了一下
from pymongo import MongoClient

class mzitu():


def all_url(self, url):

html = request.get(url, 3)
all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
print(u'开始保存:', title)
path = str(title).replace("?", '_')
self.mkdir(path)
os.chdir("D:\mzitu\\"+path)
href = a['href']
self.html(href)

def html(self, href):
html = request.get(href, 3)
max_span = BeautifulSoup(html.text, 'lxml').find_all('span')[10].get_text()
for page in range(1, int(max_span) + 1):
page_url = href + '/' + str(page)
self.img(page_url)

def img(self, page_url):
img_html = request.get(page_url, 3)
img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
self.save(img_url)

def save(self, img_url):
name = img_url[-9:-4]
print(u'开始保存:', img_url)
img = request.get(img_url, 3)
f = open(name + '.jpg', 'ab')
f.write(img.content)
f.close()

def mkdir(self, path):
path = path.strip()
isExists = os.path.exists(os.path.join("D:\mzitu", path))
if not isExists:
print(u'建了一个名字叫做', path, u'的文件夹!')
os.makedirs(os.path.join("D:\mzitu", path))
return True
else:
print(u'名字叫做', path, u'的文件夹已经存在了!')
return False




Mzitu = mzitu() ##实例化
Mzitu.all_url('http://www.mzitu.com/all') ##给函数all_url传入参数 你可以当作启动爬虫(就是入口)

注意:两个 py 文件要放在一个文件夹下,文件夹中还要新建一个叫 init.py 的空文件

妹子图爬虫第一弹

本教程是根据 小白爬虫第一弹之抓取妹子图 编写

首先上边卧槽哥写的这个爬虫教程看过好几遍了,自己也跟着写了两三次了。开始觉得 哈,这么厉害,python 爬虫原来这么简单,因垂丝汀。但是后来发现“自己跟着写过”这个过程,其实能记住的知识很少。加上最近自己一直很迷茫,不知道该往哪个方面发展,不知道该学什么,在 python 和 java 中纠结,在 爬虫 还是 服务器 中纠结。前两周还看了两周的 用Python和Pygame写游戏,慢慢学着发现,教程是基于 python2 的,而我的电脑安装的环境是 python3.5 的,很多教程上需要的包 和一些方法都不能用,而且教程已经是六年前的了,不能说已经被放弃了吧,还是有些过时,python 执行效率又不高,只能写一些小游戏。所以说还是从基础开始,往深里学,用心学习。

想起了之前看到的一张图

自己听过的不如读过的,读过的不如讨论过的,讨论过的不如教给其他人的。所以我决定以后要把自己学过的知识,自己写出来,写在博客里。这样自己也印象比较深刻,要是我写的博文能帮到其他人那就更好了。

基础环境

首先,我承认,我看上面所说的那个教程的时候,完全是被要爬取的网站吸引了。毕竟xx是学习第一动力(/滑稽)。

下面先来看看完成这篇教程所需要的基础环境

  • Python。本教程是基于 python3 来写。windows用户可以安装 anaconda,这是一个Python的科学计算发行版本,作者打包好多好多的包。
  • Requests。网络请求包。
  • beautifulsoup。可以从 html 文件中提取数据,非常方便,不用再搞那些恶心的正则表达式了。
  • LXML。一个HTML解析包 用于辅助beautifulsoup解析网页。

安装这些模块的话可以通过命令行来安装

1
2
3
4
5
6
7
conda install requests
conda install beautifulsoup4
conda install lxml
或者
pip install requests
pip install beautifulsoup4
pip install lxml

代码

重点来了!!我们要爬的网站是 妹子图 , 开不开心,激不激动。

网站的 http://www.mzitu.com/all 页面有整个网站全部的数据,贼良心。我们就在这个页面开启我们的爬虫之旅。Just do it!

1
2
3
4
5
6
7
8
9
10
11
import requests
import os
from bs4 import BeautifulSoup

headers = {'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"} ## 浏览器请求头

all_url = 'http://www.mzitu.com/all'

start_html = requests.get(all_url, headers=headers)##使用 requests 来获取 all_url 的内容,并且把 headers 作为请求头

print(start_html.text) ##把爬取到的内容打印出来 !!!记住网页内容是 .text

执行上面的代码会在控制台输出 all_url 页面的 html 源码。

目前只是爬到了网页的 html 源文件,但是我们是要爬图的啊!!!我们要美女图啊!!!爬虫才刚开始,慢慢来。

在 chrome 中打开 http://www.mzitu.com/all,按下 F12 调出开发者调试工具。

如图:

点击调试窗口左上角的那个小箭头,这样浏览器会根据你鼠标选中的视图显示相应的 html 代码。

如图,所有的图片链接地址都在 <li>...</li>

随便点开一个 <li> 标签

会发现图片页面的地址在<a>标签的href属性中、主题在<a>标签中。所以我们只要爬取所有<li>标签里的内容就能爬取我们想要的地址了。

但是页面里还有好多不符合要求的<li>标签,所以要筛选一下。通过观察发现,所有的文章主题地址都在 <div class='all'>...</div> 标签里。就是 <div class='all'> 里的 <a>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
from bs4 import BeautifulSoup

headers = {'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"} ## 浏览器请求头

all_url = 'http://www.mzitu.com/all'

start_html = requests.get(all_url, headers=headers)##使用 requests 来获取 all_url 的内容,并且把 headers 作为请求头

all_soup = BeutifulSoup(start_html.text,'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)

all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
for a in all_a:
print(a)

运行结果如下:

然后就该提取我们想要的内容了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
from bs4 import BeautifulSoup

headers = {'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"} ## 浏览器请求头

all_url = 'http://www.mzitu.com/all'

start_html = requests.get(all_url, headers=headers)##使用 requests 来获取 all_url 的内容,并且把 headers 作为请求头

all_soup = BeutifulSoup(start_html.text,'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)

all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
for a in all_a:
title = a.get_text()
href = a['href]

结果:

然后我们在随便打开一个上面打印出来的链接地址,会发现页面中只有一张图啊,点击 下一页 会发现 url 一直在变,就是在第一张图的 url 后跟 ‘/‘ + 数字,就代表第几张图。所以只需拿到最后一张图的页码,然后访问第一张图到最后一张图所有的url就可以爬到全部的图了。

观察发现页码栏都在<div class='pagenavi'> 标签下,而最后一页的页码是 标签中倒数第二个 <span> 中的内容,所以这一波操作来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
from bs4 import BeautifulSoup

headers = {'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"} ## 浏览器请求头

all_url = 'http://www.mzitu.com/all'

start_html = requests.get(all_url, headers=headers)##使用 requests 来获取 all_url 的内容,并且把 headers 作为请求头

all_soup = BeutifulSoup(start_html.text,'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)

all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class 为 all 的div标签,然后查找所有的<a>标签。
for a in all_a:
title = a.get_text()
href = a['href]
html = requests.get(href,headers=headers)
html_soup = BeautifulSoup(html.text,'lxml)
max_span = html_soup.find('div',class_='pagenavi').find_all('span')[-2].get_text()##[-2]代表倒数第二个
for page in range(1, int(max_span)+1):
page_url = href + '/' + str(page)
print(page_url)

这时我们就得到了每张图所在的链接地址,距离爬到我们心心念念的图片地址还有最后一步。发现我们需要的地址在 <div class=”main-image”> 中的 <img> 标签的 src 属性中。

继续操作一波

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
from bs4 import BeautifulSoup

headers = {'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"} ## 浏览器请求头

all_url = 'http://www.mzitu.com/all'

start_html = requests.get(all_url, headers=headers)##使用 requests 来获取 all_url 的内容,并且把 headers 作为请求头

all_soup = BeutifulSoup(start_html.text,'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)

all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class 为 all 的div标签,然后查找所有的<a>标签。
for a in all_a:
title = a.get_text()
href = a['href]
html = requests.get(href,headers=headers)
html_soup = BeautifulSoup(html.text,'lxml)
max_span = html_soup.find('div',class_='pagenavi').find_all('span')[-2].get_text()##[-2]代表倒数第二个
for page in range(1, int(max_span)+1):
page_url = href + '/' + str(page)
page_html = requests.get(page_url,headers=headers)
page_soup = BeautifulSoup(page_html.text, 'lxml')
img_url = page_soup.find('div', class_='main-image').find('img')['src']
print(img_url)

hahahah,完美,这不就是我们想要的嘛。但是还要继续操作,我们只拿到了图片的 url ,还要把这些图片下载到本地才算“爬虫”啊。

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
import requests ##导入requests
from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
import os


headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
all_url = 'http://www.mzitu.com/all' ##开始的URL地址
start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
for a in all_a:
title = a.get_text() #取出a标签的文本
path = str(title).strip() ##去掉空格
os.makedirs(os.path.join("D:\mzitu", path)) ##创建一个存放套图的文件夹
os.chdir("D:\mzitu\\"+path) ##切换到上面创建的文件夹
href = a['href'] #取出a标签的href 属性
html = requests.get(href, headers=headers) ##上面说过了
html_Soup = BeautifulSoup(html.text, 'lxml') ##上面说过了
max_span = html_Soup.find('div', class_='pagenavi').find_all('span')[-2].get_text() ##查找所有的<span>标签获取第十个的<span>标签中的文本也就是最后一个页面了。
for page in range(1, int(max_span)+1): ##不知道为什么这么用的小哥儿去看看基础教程吧
page_url = href + '/' + str(page) ##同上
img_html = requests.get(page_url, headers=headers)
img_Soup = BeautifulSoup(img_html.text, 'lxml')
img_url = img_Soup.find('div', class_='main-image').find('img')['src'] ##这三行上面都说过啦不解释了哦
name = img_url[-9:-4] ##取URL 倒数第四至第九位 做图片的名字
img = requests.get(img_url, headers=headers)
f = open(name+'.jpg', 'ab')##写入多媒体文件必须要 b 这个参数!!必须要!!
f.write(img.content) ##多媒体文件要是用conctent哦!
f.close()

然后进行进一步的封装

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
import requests
from bs4 import BeautifulSoup
import os

class mzitu():

def all_url(self, url):
html = self.request(url)##调用request函数把套图地址传进去会返回给我们一个response
all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
print(u'开始保存:', title) ##加点提示不然太枯燥了
path = str(title).replace("?", '_') ##我注意到有个标题带有 ? 这个符号Windows系统是不能创建文件夹的所以要替换掉
self.mkdir(path) ##调用mkdir函数创建文件夹!这儿path代表的是标题title哦!!!!!不要糊涂了哦!
href = a['href']
self.html(href) ##调用html函数把href参数传递过去!href是啥还记的吧? 就是套图的地址哦!!不要迷糊了哦!

def html(self, href): ##这个函数是处理套图地址获得图片的页面地址
html = self.request(href)
max_span = BeautifulSoup(html.text, 'lxml').find('div', class='pagenavi').find_all('span')[-2].get_text()
for page in range(1, int(max_span) + 1):
page_url = href + '/' + str(page)
self.img(page_url) ##调用img函数

def img(self, page_url): ##这个函数处理图片页面地址获得图片的实际地址
img_html = self.request(page_url)
img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
self.save(img_url)

def save(self, img_url): ##这个函数保存图片
name = img_url[-9:-4]
img = self.request(img_url)
f = open(name + '.jpg', 'ab')
f.write(img.content)
f.close()

def mkdir(self, path): ##这个函数创建文件夹
path = path.strip()
isExists = os.path.exists(os.path.join("D:\mzitu", path))
if not isExists:
print(u'建了一个名字叫做', path, u'的文件夹!')
os.makedirs(os.path.join("D:\mzitu", path))
os.chdir(os.path.join("D:\mzitu", path)) ##切换到目录
return True
else:
print(u'名字叫做', path, u'的文件夹已经存在了!')
return False

def request(self, url): ##这个函数获取网页的response 然后返回
headers = {'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}
content = requests.get(url, headers=headers)
return content

Mzitu = mzitu() ##实例化
Mzitu.all_url('http://www.mzitu.com/all') ##给函数all_url传入参数 你可以当作启动爬虫(就是入口)

大功告成!

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×

keyboard_arrow_up 回到顶端