如何抓取Web页面使用Scrapy和Python 3

介绍

网络抓取,通常称为网页抓取或网络抓取,或“编程遍历一系列网页和提取数据”是用于在网络上处理数据的强大工具。 使用网络scraper,您可以挖掘一组产品的数据,获取大量的文本或定量数据来玩,从没有官方API的网站获取数据,或者满足您个人的个人好奇心。 在本教程中,您将了解当探索一个有趣的数据集时,抓取和加载过程的基本原理。我们将使用 BrickSet ,社区运行的网站,其中包含有关LEGO集的信息。在本教程结束时,您将有一个功能齐全的Python网络抓取工具,遍历Brickset上的一系列页面,并从每个页面提取关于LEGO站的数据,将数据显示到屏幕上。 scraper将很容易扩展,所以你可以修改它,并使用它作为一个基础,你自己的项目从网上抓取数据。

先决条件

要完成本教程,你需要一个地方发展环境,为Python 3.您可以按照 如何安装和设置为Python 3本地编程环境来配置你需要的一切。

第1步 - 创建基本scraper

scraper两步过程:
  1. 您可以系统地查找和下载网页。
  2. 你把这些网页和从中提取信息。
这两个步骤可以以许多种方式在许多语言中实现。 您可以使用您的编程语言提供的模块或库来从头构建一个scraper,但是随着scraper越来越复杂,您必须处理一些潜在的难题。例如,您需要处理并发性,以便一次可以抓取多个页面。您可能想要找出如何将您的抓取的数据转换为不同的格式,如CSV,XML或JSON。有时,您需要处理需要特定设置和访问模式的网站。 如果你在现有的图书馆之上构建你的scraper,为你处理这些问题,你会有更好的运气。在本教程中,我们将使用Python和 Scrapy建立我们的scraper。 Scrapy是最流行和功能强大的Python抓取库之一;它需要一个“包括电池”的方法来scraper,这意味着它处理了很多所有scraper所需的常用功能,所以开发人员不必每次都重新发明轮子。它使scraper一个快速和有趣的过程! Scrapy,最喜欢的Python包,是PyPI上(也称为 pip )。 PyPI,Python包索引,是一个社区拥有的所有已发布的Python软件的存储库。 如果你有一个Python安装像在本教程的前提条件所述,您已经 pip安装在机器上,这样你就可以用下面的命令来安装Scrapy:
pip install scrapy
如果您遇到任何问题与安装,或者要安装Scrapy不使用 pip ,检查出的 官方安装文档 。 安装Scrapy后,我们为我们的项目创建一个新文件夹。你可以在终端通过运行:
mkdir brickset-scraper
现在,导航到刚刚创建的新目录:
cd brickset-scraper
然后为我们的scraper称为一个新的Python文件 scraper.py 。 我们将把所有的代码放在这个教程的文件中。 您可以创建与终端这个文件 touch命令,如下所示:
touch scraper.py
或者,您可以使用文本编辑器或图形文件管理器创建文件。 我们将从制作一个使用Scrapy作为其基础的非常基本的scraper开始。为了做到这一点,我们将创建一个子类的Python类 scrapy.Spider ,由Scrapy提供一个基本的爬虫类。这个类将有两个必需的属性:
  • name -只为爬虫的名称。
  • start_urls -你开始从抓取的URL列表。我们将从一个URL开始。
打开 scrapy.py在文本编辑器文件,并添加以下代码来创建基本的爬虫:
scraper.py
import scrapy


class BrickSetSpider(scrapy.Spider):
    name = "brickset_spider"
    start_urls = ['http://brickset.com/sets/year-2016']
让我们逐行细分一下: 首先,我们导入 scrapy这样我们就可以使用该软件包提供的类。 接下来,我们就对 Spider通过Scrapy提供的类,使 子类在它外面叫 BrickSetSpider 。 将子类视为其父类的更特殊形式。 该 Spider子类有方法和定义如何跟踪的网址和提取数据从它找到的网页的行为,但它不知道去哪里找还是要看哪些数据。通过子类化,我们可以给它那个信息。 然后,我们给爬虫的名字 brickset_spider 。 最后,我们给我们的scraper单个URL从开始: http://brickset.com/sets/year-2016 。如果您在浏览器中打开该网址,则会转到搜索结果页,显示包含LEGO集的许多网页中的第一页。 现在让我们测试一下scraper。通常,您可以通过运行像一个命令来运行Python文件 python path/to/file.py 。 然而,Scrapy带有 其自己的命令行界面来简化开始scraper的过程。使用以下命令启动您的scraper:
scrapy runspider scraper.py
你会看到这样的:
Output2016-09-22 23:37:45 [scrapy] INFO: Scrapy 1.1.2 started (bot: scrapybot)
2016-09-22 23:37:45 [scrapy] INFO: Overridden settings: {}
2016-09-22 23:37:45 [scrapy] INFO: Enabled extensions:
['scrapy.extensions.logstats.LogStats',
 'scrapy.extensions.telnet.TelnetConsole',
 'scrapy.extensions.corestats.CoreStats']
2016-09-22 23:37:45 [scrapy] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
 ...
 'scrapy.downloadermiddlewares.stats.DownloaderStats']
2016-09-22 23:37:45 [scrapy] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
 ...
 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2016-09-22 23:37:45 [scrapy] INFO: Enabled item pipelines:
[]
2016-09-22 23:37:45 [scrapy] INFO: Spider opened
2016-09-22 23:37:45 [scrapy] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-09-22 23:37:45 [scrapy] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-09-22 23:37:47 [scrapy] DEBUG: Crawled (200) <GET http://brickset.com/sets/year-2016> (referer: None)
2016-09-22 23:37:47 [scrapy] INFO: Closing spider (finished)
2016-09-22 23:37:47 [scrapy] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 224,
 'downloader/request_count': 1,
 ...
 'scheduler/enqueued/memory': 1,
 'start_time': datetime.datetime(2016, 9, 23, 6, 37, 45, 995167)}
2016-09-22 23:37:47 [scrapy] INFO: Spider closed (finished)
这是很多的输出,所以让我们把它分解说明。
  • scraper初始化并加载了处理从URL读取数据所需的其他组件和扩展。
  • 它曾经我们在提供的URL start_urls列表,并抓住了HTML,就像你的网页浏览器会怎么做。
  • 它通过了HTML的parse方法,它默认情况下不执行任何操作。 因为我们从来没有写过自己的parse方法,爬虫刚刚完成且没有做任何工作。
现在让我们从页面中提取一些数据。

第2步 - 从页面提取数据

我们创建了一个非常基本的程序,下拉一个页面,但它不做任何scraper或spidering。让我们给它一些数据提取。 如果你看一下 我们要scraper的页面 ,你会看到它的结构如下:
  • 每个页面上都有一个标题。
  • 有一些顶级搜索数据,包括匹配的数量,我们正在搜索的,以及网站的面包屑。
  • 然后有集合本身,显示在什么看起来像一个表或有序列表。每个集合具有相似的格式。
当编写一个scraper时,最好看一下HTML文件的源代码并熟悉结构。所以这里是,为了可读性删除了一些东西:
brickset.com/sets/year-2016<body>
  <section class="setlist">
    <article class='set'>
      <a class="highslide plain mainimg" href=
      "https://www.youcl.com/uploads/sets/images/10251-1.jpg?201510121127"
      onclick="return hs.expand(this)"><img src=
      "https://www.youcl.com/uploads/sets/small/10251-1.jpg?201510121127"
      title="10251-1: Brick Bank"></a>
      <div class="highslide-caption">
        <h1><a href='/sets/10251-1/Brick-Bank'>Brick Bank</a></h1>
        <div class='tags floatleft'>
          <a href='/sets/10251-1/Brick-Bank'>10251-1</a> <a href=
          '/sets/theme-Advanced-Models'>Advanced Models</a> <a class=
          'subtheme' href=
          '/sets/theme-Advanced-Models/subtheme-Modular-Buildings'>Modular
          Buildings</a> <a class='year' href=
          '/sets/theme-Advanced-Models/year-2016'>2016</a>
        </div>
        <div class='floatright'>
          &copy;2016 LEGO Group
        </div>
        <div class="pn">
          <a href="#" onclick="return hs.previous(this)" title=
          "Previous (left arrow key)">&#171; Previous</a> <a href="#"
          onclick="return hs.next(this)" title=
          "Next (right arrow key)">Next &#187;</a>
        </div>
      </div>
      ...
    </article>
    <article class='set'>

      ...

    </article>
</section>
</body>
抓取此页面有两个步骤:
  1. 首先,通过寻找具有我们想要的数据的页面部分来抓取每个LEGO集合。
  2. 然后,对于每个集合,抓取我们想要的数据,通过从HTML标签中拉出数据。
scrapy根据您提供的 选择待价而沽的数据。 选择器模式,我们可以用它来找到一个页面上的一个或多个元素,所以我们可以再与元素中的数据。 scrapy既支持CSS选择器和 XPath的选择器。 我们现在将使用CSS选择器,因为CSS是更容易的选择,并且完美适合查找页面上的所有集合。如果你看的HTML页面,你会看到每一套与类指定的 set 。 由于我们正在寻找一个类,我们会使用 .set我们的CSS选择器。 所有我们要做的是,选择通入 response对象,像这样:
scraper.py
class BrickSetSpider(scrapy.Spider):
    name = "brickset_spider"
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):
            pass
此代码抓取页面上的所有集合,并循环遍历它们以提取数据。现在让我们从这些集合中提取数据,以便我们可以显示它们。 再看看[来源](查看源代码:brickset.com/sets/year-2016)我们分析页面的告诉我们,每一集的名称存储在一个范围内 a内部标签 h1标签为每套:
brickset.com/sets/year-2016<h1><a href='/sets/10251-1/Brick-Bank'>Brick Bank</a></h1>
brickset我们遍历对象都有自己 css方法,所以我们可以在一个选择器传递给定位子元素。修改您的代码如下,找到集合的名称并显示它:
scraper.py
class BrickSetSpider(scrapy.Spider):
    name = "brickset_spider"
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 a ::text'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
            }
:后尾随逗号extract_first()不是一个错字。 我们将很快添加更多的这部分,所以我们留下了逗号,以便以后更容易添加。 你会注意到在这段代码中有两件事:
  • 我们追加::text给我们的名称选择。 这是一个CSS伪选择器 ,在取里面的文字a标签,而不是标签本身。
  • 我们称extract_first()返回的对象上brickset.css(NAME_SELECTOR)因为我们只想说选择相匹配的第一个元素。这给了我们一个字符串,而不是一个元素列表。
保存文件并再次运行scraper:
scrapy runspider scraper.py
这次您将看到输出中出现的集合的名称:
Output...
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Brick Bank'}
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Volkswagen Beetle'}
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Big Ben'}
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Winter Holiday Train'}
...

让我们继续通过增加新的选择图像,片,和数字,或 minifigs附带一套关于这个不断扩大范围。 再看一下特定集合的HTML:
brickset.com/sets/year-2016<article class="set">
  <a class="highslide plain mainimg" href="https://www.youcl.com/uploads/sets/images/10251-1.jpg?201510121127" onclick="return hs.expand(this)">
    <img src="https://www.youcl.com/uploads/sets/small/10251-1.jpg?201510121127" title="10251-1: Brick Bank"></a>
  ...
  <div class="meta">
    <h1><a href="/sets/10251-1/Brick-Bank"><span>10251:</span> Brick Bank</a> </h1>
    ...
    <div class="col">
      <dl>
        <dt>Pieces</dt>
        <dd><a class="plain" href="/inventories/10251-1">2380</a></dd>
        <dt>Minifigs</dt>
        <dd><a class="plain" href="/minifigs/inset-10251-1">5</a></dd>
        ...
      </dl>
    </div>
    ...
  </div>
</article>
我们可以通过检查这段代码看到一些东西:
  • 对于该组的图像被存储在所述src一个的属性img标签的内侧a在该组的开始代码。我们可以使用另一个CSS选择器来获取这个值,就像我们抓取每个集合的名称一样。
  • 获得件数是有点棘手。有一个dt包含文本标签的Pieces ,然后dd跟随它包含片的实际数量的标签。 我们将使用的XPath ,用于遍历XML查询语言,要抓住这个,因为它太复杂,使用CSS选择器来表示。
  • 获得集合中的迷你牌的数量类似于获得件数。有一个dt包含文本标签Minifigs ,其次是dd之后,随着数字标签。
所以,让我们修改scraper得到这个新的信息:
scraper.py
class BrickSetSpider(scrapy.Spider):
    name = 'brick_spider'
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 a ::text'
            PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()'
            MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()'
            IMAGE_SELECTOR = 'img ::attr(src)'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
                'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(),
                'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(),
                'image': brickset.css(IMAGE_SELECTOR).extract_first(),
            }
保存您的更改并再次运行scraper:
scrapy runspider scraper.py
现在您将看到程序输出中的新数据:
Output2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': '5', 'pieces': '2380', 'name': 'Brick Bank', 'image': 'https://www.youcl.com/uploads/sets/small/10251-1.jpg?201510121127'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': '1167', 'name': 'Volkswagen Beetle', 'image': 'https://www.youcl.com/uploads/sets/small/10252-1.jpg?201606140214'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': '4163', 'name': 'Big Ben', 'image': 'https://www.youcl.com/uploads/sets/small/10253-1.jpg?201605190256'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': None, 'name': 'Winter Holiday Train', 'image': 'https://www.youcl.com/uploads/sets/small/10254-1.jpg?201608110306'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': None, 'name': 'XL Creative Brick Box', 'image': '/assets/images/misc/blankbox.gif'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': '583', 'name': 'Creative Building Set', 'image': 'https://www.youcl.com/uploads/sets/small/10702-1.jpg?201511230710'}
现在让我们把这个scraper变成一个爬虫,跟随链接。

第3步 - 抓取多个页面

我们已成功从初始网页中提取数据,但我们还没有进展到看到其他结果。爬虫的整个点是检测和遍历到其他页面的链接,并从这些页面抓取数据。 你会发现,每一页的顶部和底部有一个小右克拉( > )链接到结果的下一个页面。这里是HTML的:
brickset.com/sets/year-2016<ul class="pagelength">

  ...

  <li class="next">
    <a href="http://brickset.com/sets/year-2017/page-2">&#8250;</a>
  </li>
  <li class="last">
    <a href="http://brickset.com/sets/year-2016/page-32">&#187;</a>
  </li>
</ul>
正如你所看到的,有一个 li与类的标签 next ,而标签内,有一个 a一个链接到下一个页面的标签。我们所要做的就是告诉scraper跟踪那个链接(如果存在)。 修改代码如下:
scraper.py
class BrickSetSpider(scrapy.Spider):
    name = 'brick_spider'
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 a ::text'
            PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()'
            MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()'
            IMAGE_SELECTOR = 'img ::attr(src)'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
                'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(),
                'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(),
                'image': brickset.css(IMAGE_SELECTOR).extract_first(),
            }

        NEXT_PAGE_SELECTOR = '.next a ::attr(href)'
        next_page = response.css(NEXT_PAGE_SELECTOR).extract_first()
        if next_page:
            yield scrapy.Request(
                response.urljoin(next_page),
                callback=self.parse
            )
首先,我们为“下一页”链接定义一个选择器,提取第一个匹配,并检查它是否存在。该 scrapy.Request是我们回报说:“嘿,抓取此网页”,和值 callback=self.parse说:“一旦你已经得到从这个页面的HTML,它传回给此方法,所以我们可以分析它,提取数据,并找到下一页。 这意味着,一旦我们转到下一页,我们将寻找一个指向下一页的链接,在该页上,我们将寻找指向下一页的链接,依此类推,直到我们找不到下一页的链接。这是网络抓取的关键部分:查找和跟踪链接。在这个例子中,它非常线性;一个网页上有一个指向下一页的链接,直到我们点击最后一个网页,但您可以点击链接到标记,其他搜索结果或任何其他网址。 现在,如果你保存你的代码并再次运行spider,你会看到它不只是停止一次迭代的第一页的集合。它继续经历23页的所有779场比赛!在宏伟的方案中,它不是一大块数据,但现在你知道自动找到新页面的过程。 这里是我们完成的本教程的代码,使用特定于Python的突出显示:
scraper.py
import scrapy


class BrickSetSpider(scrapy.Spider):
    name = 'brick_spider'
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 a ::text'
            PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()'
            MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()'
            IMAGE_SELECTOR = 'img ::attr(src)'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
                'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(),
                'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(),
                'image': brickset.css(IMAGE_SELECTOR).extract_first(),
            }

        NEXT_PAGE_SELECTOR = '.next a ::attr(href)'
        next_page = response.css(NEXT_PAGE_SELECTOR).extract_first()
        if next_page:
            yield scrapy.Request(
                response.urljoin(next_page),
                callback=self.parse
            )

结论

在本教程中,您构建了一个功能全面的爬虫,它从少于30行代码的网页中提取数据。这是一个伟大的开始,但有很多有趣的事情,你可以做这个爬虫。这里有一些方法可以扩展你写的代码。他们会给你一些练习scraper数据。
  1. 现在我们只分析结果从2016年,你可能已经从猜测2016的一部分http://brickset.com/sets/year-2016 -你将如何抓取其他年份的结果吗?
  2. 大多数套装都有零售价格。如何从该单元格中提取数据?你将如何获得原始数出它的提示 :你会发现在数据dt就像件,minifigs的数量。
  3. 大多数结果具有指定关于集合或其上下文的语义数据的标签。考虑到单个集合有多个标签,我们如何抓取这些?
这应该足以让你思考和试验。如果您需要Scrapy的更多信息,请 Scrapy的官方文档
赞(52) 打赏
未经允许不得转载:优客志 » 系统运维
分享到:

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏