使用lxml和bs4进行爬虫

前言

前面其实是学Java的过程中学到正则表达式的,然后结合之前学的IO流来练习一下使用Java进行爬虫。而且我们使用的爬虫的工具还是python爬虫三大工具中最难的——正则表达式。这就让我有点好奇还有两个爬虫的工具是怎么用的了。这里就使用上次的爬虫的例子来简单的介绍一下吧。

使用lxml

其实这个东西应该是叫XPath吧,其实我也不是很清楚,不过用起来还是蛮简单的,尤其是学过了一点点前端之后。使用lxml和bs4真的是如鱼得水,好用得不得了。

之前我们使用python进行爬虫的时候使用的是urllib.request这个库,不过现在requests应该是最适合的,因为requests库的api非常的人性化而且也非常的好用。不过问题就是这个库是需要我们去下载的,原生的python是没有这个库的。我们可以使用pip install requests或者python -m pip install requests来安装这个库。lxml也是如此,我们可以使用上面相同的方式来获取到这个库。因为只是简单的介绍,所以我也不会有过多的说明,所以说直接上代码吧。

首先看一下requests的使用。

HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36'
}

def getUrlText(url):
    response = requests.get(url, headers=HEADERS)
    text = response.content.decode("utf-8")
    return text  

我们再重新来看一下urllib.request的使用

def getUrlContent(url):
    headers = {
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36'
    }

    req = request.Request(url, headers = headers)
    resp = request.urlopen(req)
    urlContent = resp.read().decode('utf-8')
    return urlContent

可以看到requests库的确是方便多了。

requests.get值得就是向目标url发出get请求,相同的requests.post就是发出post请求,这一点上比urllib中的容易理解。对于相应后的返回值,有两个方法是需要注意一下的。response.content返回的是字节码,我们可以通过观察网站的charset手动调用decode()方法进行解码。还有一个方法是response.text返回是字符串,是python通过猜测给我们解的码,所以说这个方法很有可能会出现乱码(真的是一个弟弟方法),所以说我个人还是习惯于使用response.content

既然目标网站的源代码我们已经搞过来了,下面一步就是来提取有用的信息了,之前使用的是正则表达式,这里使用lxml。

def getImgSrc(text):
    html = etree.HTML(text)
    div = html.xpath("//div[@class='content_nr']")[0]
    srcs = div.xpath(".//img/@src")
    return srcs

就简单的几行代码。第二行代码使用etree来加载这个源代码。(其实是lxml.etree,只不过我们使用了from lxml import etree而已),然后返回了一个对象。我们可以对这个对象使用xpath语法。

下面就简单的说说xpath语法。

表达式 描述
nodename 选取此节点的所有子节点。
/ 从根节点选取。
// 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
. 选取当前节点。
.. 选取当前节点的父节点。
@ 选取属性。
路径表达式 结果
/bookstore/book[1] 选取属于 bookstore 子元素的第一个 book 元素。
/bookstore/book[last()] 选取属于 bookstore 子元素的最后一个 book 元素。
/bookstore/book[last()-1] 选取属于 bookstore 子元素的倒数第二个 book 元素。
/bookstore/book[position()<3] 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。
//title[@lang] 选取所有拥有名为 lang 的属性的 title 元素。
//title[@lang=’eng’] 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。
/bookstore/book[price>35.00] 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。
/bookstore/book[price>35.00]/title 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。
通配符 描述
* 匹配任何元素节点。
@* 匹配任何属性节点。
node() 匹配任何类型的节点。

通过在路径表达式中使用“|”运算符,可以选取若干个路径。

上面就是XPath的一些简单的语法。下面就看着这些语法来看看下面的两行代码。

div = html.xpath("//div[@class='content_nr']")[0]

//div[@class='content_nr']是选取class属性为content_nr的div节点,虽然这个节点在网站上只有一个,不过依旧还是返回一个列表。我们也可以在XPath选取的时候就选择第一个,不过要注意的是,这个位置竟然是从1开始的。不过这里我们是不可以写到里面去的,因为里面我们已经加上了一个class 的限定条件了,不可以在后面继续使用[]表示第几个了。

srcs = div.xpath(".//img/@src")

我们已经得到了存放许多图片链接的div,其中图片的链接都是在div里面的img标签的src属性下。

这是我们使用的是.//img而不是//img,这是因为此时指定XPath的已经不是html了而是我们选取的div,如果还是使用//img,那么还是会选取到网页中的所有img标签。后面的/@src指的就是选取img标签的src属性,返回值也是一个列表,是一个src里面地址字符串的列表,这就很舒服了~~

下面下载图片的事情就简单了。

def urlRetrieve(url, destFilename):
    with open(destFilename, 'wb') as f:
        f.write(requests.get(url, headers=HEADERS).content)

def downloadImgs(srcs, destDir):
    if not os.path.exists(destDir):
        os.makedirs(destDir)
    if not os.path.isdir(destDir):
        print('Error!! You must provide a Dir')
        exit()
    for i, src in enumerate(srcs):
        filename = os.path.join(destDir, str(i)+'.'+src.split('.')[-1])
        urlRetrieve(src, filename)

模仿urllib库,我写了一个urlRetrieve函数(原函数是urllib.urlretrieve)。所谓下载图片就是将content字节流通过二进制的形式写入到文件当中就行了。

最后再把这些函数串起来就完事了。

def spider(url, destDir):
    text = getUrlText(url)
    srcs = getImgSrc(text)
    downloadImgs(srcs, destDir)

这样爬取单页的操作就完成了。其他的操作都是类似的,这里就不再研究了,只是简单介绍一下而已啦。

使用bs4

bs4也是一个扩展库,需要我们使用pip install bs4或者python -m pip install bs4来安装,不过如果你安装了anaconda的话,上面的这些库你都不需要安装。直接自带的!~

bs4的使用和上面的lxml其实是差不多了。至于代码除了getImgSrc之外其余的代码都是相同的。所以说我们看一下getImgSrc()函数就完事了。

def getImgSrc(text):
    soup = BeautifulSoup(text, 'lxml')
    div = soup.find_all('div', class_ ='content_nr')[0]
    imgs = div.find_all('img')
    srcs = [img['src'] for img in imgs]
    return srcs

在代码的开始我们使用from bs4 import BeautifulSoup导入这个库。

上面的第二行就是使用lxml解析器将网页的源代码解析成一个。。emm美味的汤??算了,不管了,上面就是将源代码中的html代码整合成为一种树结构,便于后面的使用。

div = soup.find_all('div', class_='content_nr')[0]

就是寻找所有class属性为content_nr的标签,返回一个列表。至于为什么是class_是因为class在python中是保留字(类名),不可以使用。不过我们也可以使用div = soup.find_all('div', attrs = {'class':'content_nr'})这种方式。这时候我们就可以使用class了,因为这里的class是一个字符串。

imgs = div.find_all('img')

找到div所有的img标签

srcs = [img['src'] for img in imgs]

img['src']是获取img标签的src里面的值,使用上面的那种方式,可以将img标签列表转为一个src列表。其实获取属性还可以使用img.attrs['src']的方式,不过这个也是有点儿没事找事了。既然可以少写一点,为什么不少写一点呢?

其实除了find_all()函数之外,BeautifulSoup还提供了CSS选择器的方式。上面的函数的代码还可以这样写。

def getImgSrc(text):
    soup = BeautifulSoup(text, 'lxml')
    div = soup.select('div.content_nr')[0]
    imgs = soup.select('img')
    srcs = [img['src'] for img in imgs]
    return srcs

使用CSS选择器也是一个蛮不错的方式。

还有一点需要注意的是,如果网页的代码非常不规范,使用lxml解析器会报错的话,我们可以使用容错性更高的解析器。不过效率肯定是更低了。

soup = BeautifulSoup(text, 'html5lib')。使用这也是需要安装html5lib这个库的,正如使用lxml解析器也需要安装lxml库。

下面给出完整的代码。

import requests
import os
from bs4 import BeautifulSoup

HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36'
}

def urlRetrieve(url, destFilename):
    with open(destFilename, 'wb') as f:
        f.write(requests.get(url, headers=HEADERS).content)

def getUrlText(url):
    response = requests.get(url, headers=HEADERS)
    text = response.content.decode("utf-8")
    return text    

def getImgSrc(text):
    soup = BeautifulSoup(text, 'lxml')
    # div = soup.find_all('div', class_ ='content_nr')[0]
    # div = soup.select('div.content_nr')[0]
    div = soup._find_all('div', attrs = {'class':'content_nr'})[0]
    # imgs = div.find_all('img')
    imgs = div.select('img')
    srcs = [img['src'] for img in imgs]
    return srcs

def downloadImgs(srcs, destDir):
    if not os.path.exists(destDir):
        os.makedirs(destDir)
    if not os.path.isdir(destDir):
        print('Error!! You must provide a Dir')
        exit()
    for i, src in enumerate(srcs):
        filename = os.path.join(destDir, str(i)+'.'+src.split('.')[-1])
        urlRetrieve(src, filename)

def spider(url, destDir):
    text = getUrlText(url)
    srcs = getImgSrc(text)
    downloadImgs(srcs, destDir)

if __name__ == '__main__':
    url = 'http://moe.005.tv/78680.html'
    destDir = 'testBs4'
    spider(url, destDir)

总结

对于介绍的三种的数据提取的库,基本上可以这么说。

方式 速度 难度
Regex 最快 最难
lxml/XPath 一般 一般
BeautifulSoup 最慢 最易

不过也不出乎我的意料,哪有什么简单又快的呢?就像python一样,你觉得使用起来难度最小,速度肯定也是较慢的。


一枚小菜鸡