关于爬虫中常见的两个网页解析工具的分析 —— lxml / xpath 与 bs4 / BeautifulSoup

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于爬虫中常见的两个网页解析工具的分析 —— lxml / xpath 与 bs4 / BeautifulSoup相关的知识,希望对你有一定的参考价值。

  读者可能会奇怪我标题怎么理成这个鬼样子,主要是单单写 lxml 与 bs4 这两个 py 模块名可能并不能一下引起大众的注意,一般讲到网页解析技术,提到的关键词更多的是 BeautifulSoup 和 xpath ,而它们各自所在的模块(python 中是叫做模块,但其他平台下更多地是称作库),很少被拿到明面上来谈论。下面我将从效率、复杂度等多个角度来对比 xpath 与 beautifulsoup 的区别。

效率

  从效率上来讲,xpath 确实比 BeautifulSoup 高效得多,每次分步调试时,soup 对象的生成有很明显的延迟,而 lxml.etree.html(html) 方式则在 step over 的一瞬间便构建成功了一个可执行 xpath 操作的对象,速度惊人。原理上来讲,bs4 是用 python 写的,lxml 是 c 语言实现的,而且 BeautifulSoup 是基于 DOM 的,会载入整个文档,解析整个DOM树,因此时间和内存开销都会大很多。而lxml只会进行局部遍历。
 

使用复杂度

  从使用复杂度来讲,beautifulsoup 的 find 方法要比 xpath 简单,后者不仅要求通晓 xpath 语法,而且 xpath 方法的返回对象始终是一个 list,这使得对于页面中一些唯一元素的处理有些尴尬,比如根据 id 获取页面某一标签,下面我用两种方式实现一个获取网页导航栏的方法 (注释部分为 bs4 的实现):
    def get_nav(self,response):
        # soup = BeautifulSoup(response.body_as_unicode(), ‘lxml‘)
        # nav_list = soup.find(‘ul‘, id=‘nav‘).find_all(‘li‘)
        model = etree.HTML(response.body_as_unicode())
        nav_list = model.xpath(//ul[@id="nav"]/li)
        for nav in nav_list[1:]:
            # href = nav.find(‘a‘).get(‘href‘)
            href = nav.xpath(./a/@href)[0]
yield Request(href, callback=self.get_url)

  可以看到 xpath 除了其特殊的语法看上去有些别扭(跟正则表达式似的)以外,它在代码简洁度上还是可观的,只是所有 xpath 方法的返回结果都是一个 list ,如果匹配目标是单个元素,对于无脑下标取0的操作,强迫症患者可能有些难受。相比之下,BeautifulSoup 这一长串的 find 与 find_all 方法显得有些呆板,如果碰到搜索路线比较曲折的,比如:

# href = article.find(‘div‘, class_=‘txt‘).find(‘p‘, class_=‘tit blue‘).find(‘span‘).find(‘em‘).find(‘a‘).get(‘href‘)
href = article.xpath(./div[@class="txt"]//p[@class="tit blue"]/span/em/a/@href)[0]

  这种情况下,BeautifulSoup 的写法就显得有些让人反胃了,当然一般情况下不会出现这么长的路径定位。

 

功能缺陷总结——BeautifulSoup

   BeautifulSoup 在使用上的一个短板,就是在嵌套列表中去匹配元素的时候会显得很无力,下面是一个例子(具体网页结构可根据 index_page 在浏览器打开进行审查):

class RankSpider(spider):
    name = PCauto_rank
    index_page = http://price.pcauto.com.cn/top/hot/s1-t1.html
    api_url = http://price.pcauto.com.cn%s

    def start_requests(self):
        yield Request(self.index_page, callback=self.get_left_nav)

    # 测试 BeautifulSoup 是否能连续使用两个 find_all 方法
    def get_left_nav(self,response):
        # model = etree.HTML(response.body_as_unicode())
        # nav_list = model.xpath(‘//div[@id="leftNav"]/ul[@class="pb200"]/li//a[@class="dd "]‘)
        soup = BeautifulSoup(response.body_as_unicode(), lxml)
        nav_list = soup.find(div, id=leftNav).find(ul, class_=pb200).find_all(li).find_all(a, class_=dd)
        for sub_nav in nav_list:
            href = self.api_url % sub_nav.xpath(./@href)[0]
            yield Request(href, callback=self.get_url)

    def get_url(self):
        pass

   使用注释部分的 xpath 写法没什么问题,可实现准确定位,但用到 BeautifulSoup 去实现相应逻辑的时候,就要连续使用两个 find_all 方法 ,显然这种写法不符合规范,运行的时候会报 AttributeError: ‘ResultSet‘ object has no attribute ‘find_all‘ 错误,这时候我们要实现这种匹配,只能先去遍历各个 li ,然后调 find_all 方法找到 li 下的各个 a 标签,实在繁琐,所以这种场景用 xpath 来解决会省下不少麻烦。

  当然这里我只是单单为了诠释这么个问题才在故意在拿目标 url 时分这么多级的,实际开发中我这里用的是:

        # nav_list = model.xpath(‘//div[@id="leftNav"]///a[@class="dd "]‘)
        nav_list = soup.find(div, id=leftNav).find_all(a, class_=dd)

  但如果说我们的目标不是所有的 li 下面的 a 标签,而是部分 class="*" 的 li 下面的 a 标签,这时候我们就只能选择使用 xpath 来达到目的,当然如果你喜欢写遍历,觉得这样写出来逻辑展示更清晰,那你可以跳过这一节。

 

功能缺陷总结——xpath

  xpath 的类选择器在做公共类名选择时有短板,也勉强把它算作功能缺陷吧,比如:     
 model = etree.HTML(response.body_as_unicode())
model.xpath(//div[@class="box box-2 box-4"])
  无法定位 html 中 class 为 box box-2 box-4 mt25 与 box box-2 box-4 mt17 的两个 div,必须分别以: 
model.xpath(//div[@class="box box-2 box-4 mt25"]) 
model.xpath(//div[@class="box box-2 box-4 mt17"])

  来匹配目标,这可能要归结于 xpath 在设计的时候本身就是以类名的完全匹配来确定目标的,哪怕多一个空格:

  页面中一个 a 标签是这样写的:  <a href="/top/hot/s1-t1.html" class="dd ">5万以下</a> 用 xpath 去选择,写作:

      model.xpath(‘//a[@class="dd"]‘)

  死活匹配不到(当时真的是蛮懵逼的),必须要在后面加空格,但在通过 js 控制台 a.dd 这个类选择器又可以定位到目标,而且 BeautifulSoup 调 find_all(‘a‘, class_=‘dd‘) 也是没有问题的,这种应用场景下的 xpath 就略显死板。

 

文本获取

  

        # place = soup.find(‘div‘,class_="guide")
        place = model.xpath(//div[@class="guide"])
        # nav and aiticle
        if place:
            # mark = place.find(‘span‘,class_="mark")
            mark = place[0].xpath(./span[@class="mark"])
            if mark:
                # text = mark.get_text().strip().replace(‘\n‘,‘‘).replace(‘\r‘,‘‘)
                # text = mark[0].text.strip().replace(‘\n‘,‘‘).replace(‘\r‘,‘‘)  # false
                text = mark[0].xpath(string())
                result[address] = text

 

 

 


以上是关于关于爬虫中常见的两个网页解析工具的分析 —— lxml / xpath 与 bs4 / BeautifulSoup的主要内容,如果未能解决你的问题,请参考以下文章

爬虫网页分析解析辅助工具 Xpath-helper

Python编程网页爬虫工具集介绍

Python爬虫解析网页的4种方式 值得收藏

【Python爬虫】分析网页真实请求

爬虫之数据解析,网页源码数据分析

爬取动态网页:Selenium