美文网首页
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