美文网首页
Python学习数据抓取

Python学习数据抓取

作者: 海人为记 | 来源:发表于2017-02-12 18:59 被阅读128次

在上一章中,我们构建了一个爬虫,可以通过跟踪链接的方式下载我们所需的网页。虽然这个例子很有意思,却不够实用,因为爬虫在下载网页之后又将结果丢弃了。现在,我们需要让这个爬虫从每个网页中抽取一些数据,然后实现某些事情,这种做法也被称为抓取(scraping)。
首先,我们会介绍了一个叫做Firebug Lite的浏览器扩展,用于检查网页内容,如果你有一些网络开发背景的话,可能已经对该扩展十分熟悉了。然后,我们会介绍了三种抽取网页数据的方法,分别是正则表达式/Beautiful Soup和lxml。最后,我们将对比这三种数据抓取方法。

分析网页

想要了解一个网页的结果如何,可以使用查看源代码的方法。在大多数浏览器中,都可以在页面上右键单击选择View page source选项,获取网页的源代码。
我们可以在HTML的代码中找到我们感兴趣的数据。

三种网页抓取方法

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

正则表达式

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

import urllib.request
import re
def scrape(html):
    html = html.decode('utf-8')
    area = re.findall('<tr id="places_area__row">.*?<td\s*class=["\']w2p_fw["\']>(.*?)</td>', html)[0]
    return areahtml.decode('utf-8')

if __name__ == '__main__':
    html = urllib.request.urlopen('http://example.webscraping.com/view/United-Kingdom-239').read()
    print(scrape(html))

获得结果244,820 square kilometres
正则表达式为我们提供了抓取数据的快捷方式,但是该方法过去脆弱,容易在网页更新后出现问题。

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', sttrs=('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>]

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

import urllib.request
from bs4 import BeautifulSoup

def scrape(html):
    soup = BeautifulSoup(html, "lxml")
    tr = soup.find(attrs={'id': 'places_area__row'}) # 找到区域行
    # 'class'是一个特殊的Python属性,所以使用‘class_’替换
    td = tr.find(attrs={'class':'w2p_fw'}) # 找到区域标签
    area = td.text # 从该标签提取区域内容
    return area

if __name__ == '__main__':
    html = urllib.request.urlopen('http://example.webscraping.com/view/United-Kingdom-239').read()
    print(scrape(html))

输出为244,820 square kilometres

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选择器,因为它更加简洁,并且能够在解析动态内容时得以复用。此外,一些拥有jQuery选择器相关经验的读者也会对其更加熟悉。
下面是使用lxml的CSS选择器抽取面积数据的示例代码。

import urllib.request
import lxml.html

def scrape(html):
    html = html.decode('utf-8')
    tree = lxml.html.fromstring(html)
    td = tree.cssselect('tr#places_area__row > td.w2p_fw')[0]
    area = td.text_content()
    return area

if __name__ == '__main__':
    html = urllib.request.urlopen('http://example.webscraping.com/view/United-Kingdom-239').read()
    print(scrape(html))

CSS选择器首选会找到ID为places_area__row的表格行元素,然后选择class为w2p_fw的表格数据子标签。
需要注意的是,lxml在内部实现中,实际上是将CSS选择器转换为等价的XPath选择器。

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

为链接爬虫添加抓取回调

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

def link_crawler(seed_url, link_regex=None, delay=5, max_depth=-1, 
        max_urls=-1, headers=None, user_agent='wswp', proxy=None, 
        num_retries=1, scrape_callback=None):
    """从指定的种子网址按照link_regex匹配的链接进行抓取"""
    crawal_queue = queue.deque([seed_url]) # 仍然需要抓取的网址队列
    seen = {seed_url: 0} # 已经看到的网址以及深度
    num_urls = 0 # 跟踪已下载了多少个URL
    rp = get_robots(seed_url)
    throttle = Throttle(delay)
    headers = headers or {}
    if user_agent:
        headers['User-agent'] = user_agent

    while crawal_queue:
        url = crawal_queue.pop()
        depth = seen[url]
        # 检查网址传递的robots.txt限制
        if rp.can_fetch(user_agent, url):
            throttle.wait(url)
            html = download(url, headers, proxy=proxy, num_retries=num_retries)
            links = []
            if scrape_callback:
                links.extend(scrape_callback(url, html) or [])

            if depth != max_depth:
                # 仍然可以进一步爬行
                if link_regex:
                    # 过滤符合我们的正则表达式的链接
                    links.extend(link for link in get_links(html) if re.match(link_regex, link))

                for link in links:
                    link = normalize(seed_url, link)
                    # 检查是否已经抓取这个链接
                    if link not in seen:
                        seen[link] = depth + 1
                        # 检查链接在同一域内
                        if same_domain(seed_url, link):
                            # 成功! 添加这个新链接到队列里
                            crawal_queue.append(link)

            # 检查是否已达到下载的最大值
            num_urls += 1
            if num_urls == max_urls:
                break
        else:
            print("Blocked by robots.txt:", url) # 链接已被robots.txt封锁

在上面的代码片段中,我们增加了抓取callback函数代码。
现在,我们只需要传入的scrap_callback函数定制化处理,就能使用该爬虫抓取其他网站了。下面对lxml抓取示例的代码进行了修改,使其能够在callback函数中使用,并且我们将得到的结果保存到CSV表格中。

import csv
import re
import urllib.parse
import lxml.html
from link_crawler import link_crawler

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)

if __name__ == '__main__':
    link_crawler('http://example.webscraping.com/', '/(index|view)', scrape_callback=ScrapeCallback())

为了实现该callback,我们使用了会掉泪,而不在是回调函数,以便保持csv中writer属性的状态。csv的writer属性在构造方法中进行了实例化处理,然后在__call__方法中执行了多次写操作。请注意:__call__是一个特殊方法,在对象作为函数被调用时会调用该方法,这也是链接爬虫中的cache_callback的调用方法。也即是说,scrape_callback(url, html)和调用scrape_callback.__call__(url html)是等价的。
现在,当我们运行这个使用了callback的爬虫时,程序就会将结果写入一个CSV文件中,我们可以使用类似Excel或者LibreOffice的应用查看该文件。

相关文章

网友评论

      本文标题:Python学习数据抓取

      本文链接:https://www.haomeiwen.com/subject/xwkgittx.html