返回介绍

2.2 三种网页抓取方法

发布于 2024-02-05 23:37:18 字数 13205 浏览 0 评论 0 收藏 0

现在我们已经了解了该网页的结构,下面将要介绍三种抓取其中数据的方法。首先是正则表达式,然后是流行的BeautifulSoup 模块,最后是强大的lxml 模块。

2.2.1 正则表达式

如果你对正则表达式还不熟悉,或是需要一些提示时,可以查阅https://docs.python.org/2/howto/regex.html 获得完整介绍。

当我们使用正则表达式抓取面积数据时,首先需要尝试匹配<td> 元素中的内容,如下所示。

>>> import re
>>> url = 'http://example.webscraping.com/view/United
        -Kingdom-239'
>>> html = download(url)
>>> re.findall('<td class="w2p_fw">(.*?)</td>', html)
['<img src="/places/static/images/flags/gb.png" />',
    '244,820 square kilometres',
    '62,348,447',
    'GB',
    'United Kingdom',
    'London',
    '<a href="/continent/EU">EU</a>',
    '.uk',
    'GBP',
    'Pound',
    '44',
    '@# #@@|@## #@@|@@# #@@|@@## #@@|@#@ #@@|@@#@ #@@|GIR0AA',
    '^(([A-Z]\\d{2}[A-Z]{2})|([A-Z]\\d{3}[A-Z]{2})|([A-Z]{2}\\d{2}
        [A-Z]{2})|([A-Z]{2}\\d{3}[A-Z]{2})|([A-Z]\\d[A-Z]\\d[A-Z]{2})
          |([A-Z]{2}\\d[A-Z]\\d[A-Z]{2})|(GIR0AA))$',
    'en-GB,cy-GB,gd',
    '<div><a href="/iso/IE">IE </a></div>']

从上述结果中可以看出,多个国家属性都使用了<td class="w2p_fw"> 标签。要想分离出面积属性,我们可以只选择其中的第二个元素,如下所示。

>>> re.findall('<td class="w2p_fw">(.*?)</td>', html)[1]
'244,820 square kilometres'

虽然现在可以使用这个方案,但是如果网页发生变化,该方案很可能就会失效。比如表格发生了变化,去除了第二行中的国土面积数据。如果我们只在现在抓取数据,就可以忽略这种未来可能发生的变化。但是,如果我们希望未来还能再次抓取该数据,就需要给出更加健壮的解决方案,从而尽可能避免这种布局变化所带来的影响。想要该正则表达式更加健壮,我们可以将其父元素<tr> 也加入进来。由于该元素具有ID属性,所以应该是唯一的。

>>> re.findall('<tr id="places_area__row"><td
    class="w2p_fl"><label for="places_area"
        id="places_area__label">Area: </label></td><td
          class="w2p_fw">(.*?)</td>', html)
['244,820 square kilometres']

这个迭代版本看起来更好一些,但是网页更新还有很多其他方式,同样可以让该正则表达式无法满足。比如,将双引号变为单引号,<td> 标签之间添加多余的空格,或是变更area_label 等。下面是尝试支持这些可能性的改进版本。

>>> re.findall('<tr
  id="places_area__row">.*?<td\s*class=["\']w2p_fw["\']>(.*?)
    </td>', html)
['244,820 square kilometres']

虽然该正则表达式更容易适应未来变化,但又存在难以构造、可读性差的问题。此外,还有一些微小的布局变化也会使该正则表达式无法满足,比如在<td> 标签里添加title 属性。

从本例中可以看出,正则表达式为我们提供了抓取数据的快捷方式,但是该方法过于脆弱,容易在网页更新后出现问题。幸好,还有一些更好的解决方案,我们会在接下来的小节中继续介绍。

2.2.2 Beautiful Soup

Beautiful Soup 是一个非常流行的Python模块。该模块可以解析网页,并提供定位内容的便捷接口。如果你还没有安装该模块,可以使用下面的命令安装其最新版本:

pip install beautifulsoup4

使用Beautiful Soup的第一步是将已下载的HTML内容解析为soup文档。由于大多数网页都不具备良好的HTML格式,因此Beautiful Soup需要对其实际格式进行确定。例如,在下面这个简单网页的列表中,存在属性值两侧引号缺失和标签未闭合的问题。

        <ul class=country>
            <li>Area
            <li>Population
        </ul>

如果Population 列表项被解析为Area 列表项的子元素,而不是并列的两个列表项的话,我们在抓取时就会得到错误的结果。下面让我们看一下Beautiful Soup是如何处理的。

>>> from bs4 import BeautifulSoup
>>> broken_html = '<ul class=country><li>Area<li>Population</ul>'
>>> # parse the HTML
>>> soup = BeautifulSoup(broken_html, 'html.parser')
>>> fixed_html = soup.prettify()
>>> print fixed_html
<html>
    <body>
        <ul class="country">
            <li>Area</li>
            <li>Population</li>
        </ul>
    </body>
</html>

从上面的执行结果中可以看出,Beautiful Soup 能够正确解析缺失的引号并闭合标签,此外还添加了<html> 和<body> 标签使其成为完整的HTML文档。现在可以使用find() 和find_all() 方法来定位我们需要的元素了。

>>> ul = soup.find('ul', attrs={'class':'country'})
>>> ul.find('li') # returns just the first match
<li>Area</li>
>>> ul.find_all('li') # returns all matches
[<li>Area</li>, <li>Population</li>]

想要了解全部方法和参数,可以查阅BeautifulSoup的官方文档,其网址为:`http://www.crummy.com/software/BeautifulSoup/bs4/doc/。`

下面是使用该方法抽取示例国家面积数据的完整代码。

>>> from bs4 import BeautifulSoup
>>> url = 'http://example.webscraping.com/places/view/
        United-Kingdom-239'
>>> html = download(url)
>>> soup = BeautifulSoup(html)
>>> # locate the area row
>>> tr = soup.find(attrs={'id':'places_area__row'})
>>> td = tr.find(attrs={'class':'w2p_fw'}) # locate the area tag
>>> area = td.text # extract the text from this tag
>>> print area
244,820 square kilometres

这段代码虽然比正则表达式的代码更加复杂,但更容易构造和理解。而且,像多余的空格和标签属性这种布局上的小变化,我们也无须再担心了。

2.2.3 Lxml

Lxml 是基于libxml2 这一XML解析库的Python封装。该模块使用C语言编写,解析速度比Beautiful Soup更快,不过安装过程也更为复杂。最新的安装说明可以参考http://Lxml.de/installation.html 。

和Beautiful Soup一样,使用lxml 模块的第一步也是将有可能不合法的HTML解析为统一格式。下面是使用该模块 解析同一个不完整HTML的例子。

>>> import lxml.html
>>> broken_html = '<ul class=country><li>Area<li>Population</ul>'
>>> tree = lxml.html.fromstring(broken_html) # parse the HTML
>>> fixed_html = lxml.html.tostring(tree, pretty_print=True)
>>> print fixed_html
<ul class="country">
    <li>Area</li>
    <li>Population</li>
</ul>

同样地,lxml 也可以正确解析属性两侧缺失的引号,并闭合标签,不过该模块没有额外添加<html> 和<body> 标签。

解析完输入内容之后,进入选择元素的步骤,此时lxml 有几种不同的方法,比如XPath选择器和类似Beautiful Soup的find() 方法。不过,在本例和后续示例中,我们将会使用CSS选择器,因为它更加简洁,并且能够在第5章解析动态内容时得以复用。此外,一些拥有jQuery选择器相关经验的读者也会对其更加熟悉。

下面是使用lxml 的CSS选择器抽取面积数据的示例代码。

>>> tree = lxml.html.fromstring(html)
>>> td = tree.cssselect('tr#places_area__row > td.w2p_fw')[0]
>>> area = td.text_content()
>>> print area
244,820 square kilometres

CSS选择器的关键代码行已被加粗显示。该行代码首先会找到ID为places_area__row 的表格行元素,然后选择class为w2p_fw 的表格数据子标签。

CSS选择器

CSS选择器表示选择元素所使用的模式。下面是一些常用的选择器示例。

选择所有标签:*
选择<a>标签:a
选择所有class="link"的元素:.link
选择class="link"的<a>标签:a.link
选择id="home"的<a>标签:a#home
选择父元素为<a>标签的所有<span>子标签:a > span
选择<a>标签内部的所有<span>标签:a span
选择title属性为"Home"的所有<a>标签:a[title=Home]

W3C已提出CSS3规范,其网址为`http://www.w3.org/TR``/2011/REC-css3-selectors-20110929/`。

Lxml已经实现了大部分CSS3属性,其不支持的功能可以参见https://pythonhosted.org/cssselect/#supported-selectors 。

需要注意的是,lxml 在内部实现中,实际上是将CSS选择器转换为等价的XPath选择器。

2.2.4 性能对比

要想更好地对本章中介绍的三种抓取方法评估取舍,我们需要对其相对效率进行对比。一般情况下,爬虫会抽取网页中的多个字段。因此,为了让对比更加真实,我们将为本章中的每个爬虫都实现一个扩展版本,用于抽取国家网页中的每个可用数据。首先,我们需要回到Firebug中,检查国家页面其他特征的格式,如图2.4所示。

图2.4

从Firebug的显示中可以看出,表格中的每一行都拥有一个以places_ 起始且以__row 结束的ID。而在这些行中包含的国家数据,其格式都和上面的例子相同。下面是使用上述信息抽取所有可用国家数据的实现代码。

FIELDS = ('area', 'population', 'iso', 'country', 'capital',
  'continent', 'tld', 'currency_code', 'currency_name', 'phone',
    'postal_code_format', 'postal_code_regex', 'languages',
      'neighbours')

import re
def re_scraper(html):
    results = {}
    for field in FIELDS:
        results[field] = re.search('<tr id="places_%s__row">.*?<td
            class="w2p_fw">(.*?)</td>' % field, html).groups()[0]
    return results

from bs4 import BeautifulSoup
def bs_scraper(html):
    soup = BeautifulSoup(html, 'html.parser')
    results = {}
    for field in FIELDS:
        results[field] = soup.find('table').find('tr',
            id='places_%s__row' % field).find('td',
                class_='w2p_fw').text
    return results

import lxml.html
def lxml_scraper(html):
    tree = lxml.html.fromstring(html)
    results = {}
    for field in FIELDS:
        results[field] = tree.cssselect('table > tr#places_%s__row
            > td.w2p_fw' % field)[0].text_content()
    return results

抓取结果

现在,我们已经完成了所有爬虫的代码实现,接下来将通过如下代码片段,测试这三种方法的相对性能。

import time
NUM_ITERATIONS = 1000 # number of times to test each scraper
html = download('http://example.webscraping.com/places/view/
  United-Kingdom-239')
for name, scraper in [('Regular expressions', re_scraper),
    ('BeautifulSoup', bs_scraper),
    ('Lxml', lxml_scraper)]:
    # record start time of scrape
    start = time.time()
    for i in range(NUM_ITERATIONS):
        if scraper == re_scraper:
            re.purge()
        result = scraper(html)
        # check scraped result is as expected
        assert(result['area'] == '244,820 square kilometres')
    # record end time of scrape and output the total
    end = time.time()
    print '%s: %.2f seconds' % (name, end – start)

在这段代码中,每个爬虫都会执行1000次,每次执行都会检查抓取结果是否正确,然后打印总用时。这里使用的download 函数依然是上一章中定义的那个函数。请注意,我们在加粗的代码行中调用了re.purge() 方法。默认情况下,正则表达式模块会缓存搜索结果,为了与其他爬虫的对比更加公平,我们需要使用该方法清除缓存。

下面是在我的电脑中运行该脚本的结果。

$ python performance.py
Regular expressions: 5.50 seconds
BeautifulSoup: 42.84 seconds
Lxml: 7.06 seconds

由于硬件条件的区别,不同电脑的执行结果也会存在一定差异。不过,每种方法之间的相对差异应当是相当的。从结果中可以看出,在抓取我们的示例网页时,Beautiful Soup比其他两种方法慢了超过6倍之多。实际上这一结果是符合预期的,因为lxml 和正则表达式模块都是C语言编写的,而BeautifulSoup``则 是纯Python编写的。一个有趣的事实是,lxml 表现得和正则表达式差不多好。由于lxml 在搜索元素之前,必须将输入解析为内部格式,因此会产生额外的开销。而当抓取同一网页的多个特征时,这种初始化解析产生的开销就会降低,lxml 也就更具竞争力。这真是一个令人惊叹的模块!

2.2.5 结论

表2.1总结了每种抓取方法的优缺点。

表2.1

抓取方法

性能

使用难度

安装难度

正则表达式

困难

简单(内置模块)

Beautiful Soup

简单

简单(纯Python)

Lxml

简单

相对困难

如果你的爬虫瓶颈是下载网页,而不是抽取数据的话,那么使用较慢的方法(如Beautiful Soup)也不成问题。如果只需抓取少量数据,并且想要避免额外依赖的话,那么正则表达式可能更加适合。不过,通常情况下,lxml 是抓取数据的最好选择,这是因为该方法既快速又健壮,而正则表达式和Beautiful Soup只在某些特定场景下有用。

2.2.6 为链接爬虫添加抓取回调

前面我们已经了解了如何抓取国家数据,接下来我们需要将其集成到上一章的链接爬虫当中。要想复用这段爬虫代码抓取其他网站,我们需要添加一个callback 参数处理抓取行为。callback 是一个函数,在发生某个特定事件之后会调用该函数(在本例中,会在网页下载完成后调用)。该抓取callback 函数包含url 和html 两个参数,并且可以返回一个待爬取的URL列表。下面是其实现代码,可以看出在Python中实现该功能非常简单。

def link_crawler(..., scrape_callback=None):
    ...
    links = []
    if scrape_callback:
        links.extend(scrape_callback(url, html) or [])
        ...

在上面的代码片段中,我们加粗显示了新增加的抓取callback 函数代码。如果想要获取该版本链接爬虫的完整代码,可以访问https://bitbucket. org/wswp/code/src/tip/chapter02/link_crawler.py 。

现在,我们只需对传入的scrape_callback 函数定制化处理,就能使用该爬虫抓取其他网站了。下面对lxml 抓取示例的代码进行了修改,使其能够在callback 函数中使用。

def scrape_callback(url, html):
    if re.search('/view/', url):
        tree = lxml.html.fromstring(html)
        row = [tree.cssselect('table > tr#places_%s__row >
            td.w2p_fw' % field)[0].text_content() for field in
                FIELDS]
        print url, row

上面这个callback 函数会去抓取国家数据,然后将其显示出来。不过通常情况下,在抓取网站时,我们更希望能够复用这些数据,因此下面我们对其功能进行扩展,把得到的结果数据保存到CSV表格中,其代码如下所示。

import csv
class ScrapeCallback:
    def __init__(self):
        self.writer = csv.writer(open('countries.csv', 'w'))
        self.fields = ('area', 'population', 'iso', 'country',
            'capital', 'continent', 'tld', 'currency_code',
                'currency_name', 'phone', 'postal_code_format',
                    'postal_code_regex', 'languages',
                        'neighbours')
        self.writer.writerow(self.fields)

    def __call__(self, url, html):
        if re.search('/view/', url):
            tree = lxml.html.fromstring(html)
            row = []
            for field in self.fields:
                row.append(tree.cssselect('table >
                    tr#places_{}__row >
                        td.w2p_fw'.format(field))
                            [0].text_content())
            self.writer.writerow(row)

为了实现该callback ,我们使用了回调类,而不再是回调函数,以便保持csv 中writer 属性的状态。csv 的writer 属性在构造方法中进行了实例化处理,然后在__call__ 方法中执行了多次写操作。请注意,__call__ 是一个特殊方法,在对象作为函数被调用时会调用该方法,这也是链接爬虫中cache_callback 的调用方法。也就是说,scrape_callback(url, html) 和调用scrape_callback.__call__(url, html) 是等价的。如果想要了解更多有关Python特殊类方法的知识,可以参考https://docs.python.org/2/reference/datamodel.html#special-method-names 。

下面是向链接爬虫传入回调的代码写法。

link_crawler('http://example.webscraping.com/', '/(index|view)',
    max_depth=-1, scrape_callback=ScrapeCallback())

现在,当我们运行这个使用了callback 的爬虫时,程序就会将结果写入一个CSV文件中,我们可以使用类似Excel或者LibreOffice的应用查看该文件,如图2.5所示。

图2.5

成功了!我们完成了第一个可以工作的数据抓取爬虫。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文