美文网首页Python爬虫Python专辑Data Mining & Data Analysis
Python爬虫小白入门(三)BeautifulSoup库

Python爬虫小白入门(三)BeautifulSoup库

作者: 阿里波特 | 来源:发表于2016-12-27 17:14 被阅读1282次

    一、前言


    上一篇演示了如何使用requests模块向网站发送http请求,获取到网页的HTML数据。这篇来演示如何使用BeautifulSoup模块来从HTML文本中提取我们想要的数据。

    update on 2016-12-28:之前忘记给BeautifulSoup的官网了,今天补上,顺便再补点BeautifulSoup的用法。

    update on 2017-08-16:很多网友留言说Unsplash网站改版了,很多内容是动态加载的。所以建议动态加载的内容使用PhantomJS而不是Request库进行请求,如果使用PhantomJS请看我的下一篇博客,如果是定位html文档使用的class等名字更改的话,建议大家根据更改后的内容进行定位,学爬虫重要的是爬取数据的逻辑,逻辑掌握了网站怎么变都不重要啦。

    二、运行环境


    我的运行环境如下:

    • 系统版本
      Windows10。

    • Python版本
      Python3.5,推荐使用Anaconda 这个科学计算版本,主要是因为它自带一个包管理工具,可以解决有些包安装错误的问题。去Anaconda官网,选择Python3.5版本,然后下载安装。

    • IDE
      我使用的是PyCharm,是专门为Python开发的IDE。这是JetBrians的产品,点我下载

    三、模块安装


    BeautifulSoup 有多个版本,我们使用BeautifulSoup4。详细使用看BeautifuSoup4官方文档
    使用管理员权限打开cmd命令窗口,在窗口中输入下面的命令即可安装:
    conda install beautifulsoup4

    直接使用Python3.5 没有使用Anaconda版本的童鞋使用下面命令安装:
    pip install beautifulsoup4

    然后我们安装lxml,这是一个解析器,BeautifulSoup可以使用它来解析HTML,然后提取内容。

    Anaconda 使用下面命令安装lxml:
    conda install lxml

    使用Python3.5 的童鞋们直接使用pip安装会报错(所以才推荐使用Anaconda版本),安装教程看这里

    如果不安装lxml,则BeautifulSoup会使用Python内置的解析器对文档进行解析。之所以使用lxml,是因为它速度快。

    文档解析器对照表如下:

    解析器 使用方法 优势 劣势
    Python标准库 BeautifulSoup(markup,"html.parser") 1. Python的内置标准库
    2. 执行速度适
    3. 中文档容错能力强
    Python 2.7.3 or 3.2.2)前 的版本中文档容错能力差
    lxml HTML 解析器 BeautifulSoup(markup,"lxml") 1. 速度快
    2. 文档容错能力强
    需要安装C语言库
    lxml XML 解析器 BeautifulSoup(markup,["lxml-xml"])
    BeautifulSoup(markup,"xml")
    1. 速度快
    2. 唯一支持XML的解析器
    需要安装C语言库
    html5lib BeautifulSoup(markup,"html5lib") 1. 最好的容错性
    2. 以浏览器的方式解析文档
    3. 生成HTML5格式的文档
    速度慢,不依赖外部扩展

    四、BeautifulSoup 库的使用


    网上找到的几个官方文档:BeautifulSoup4.4.0中文官方文档BeautifulSoup4.2.0中文官方文档。不同版本的用法差不多,几个常用的语法都一样。

    首先来看BeautifulSoup的对象种类,在使用的过程中就会了解你获取到的东西接下来应该如何操作。

    4.1 BeautifulSoup对象的类型

    Beautiful Soup将复杂HTML文档转换成一个复杂的树形结构,每个节点都是Python对象。所有对象可以归纳为4种类型: Tag , NavigableString , BeautifulSoup , Comment 。下面我们分别看看这四种类型都是什么东西。

    4.1.1 Tag

    这个就跟HTML或者XML(还能解析XML?是的,能!)中的标签是一样一样的。我们使用find()方法返回的类型就是这个(插一句:使用find-all()返回的是多个该对象的集合,是可以用for循环遍历的。)。返回标签之后,还可以对提取标签中的信息。

    提取标签的名字:

    tag.name

    提取标签的属性:

    tag['attribute']
    我们用一个例子来了解这个类型:

    from bs4 import BeautifulSoup
    
    html_doc = """
    <html><head><title>The Dormouse's story</title></head>
    <body>
    <p class="title"><b>The Dormouse's story</b></p>
    <p class="story">Once upon a time there were three little sisters; and their names were
    <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
    <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
    <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
    and they lived at the bottom of a well.</p>
    
    <p class="story">...</p>
    """
    soup = BeautifulSoup(html_doc, 'lxml')  #声明BeautifulSoup对象
    find = soup.find('p')  #使用find方法查到第一个p标签
    print("find's return type is ", type(find))  #输出返回值类型
    print("find's content is", find)  #输出find获取的值
    print("find's Tag Name is ", find.name)  #输出标签的名字
    print("find's Attribute(class) is ", find['class'])  #输出标签的class属性值
    

    4.1.2 NavigableString

    NavigableString就是标签中的文本内容(不包含标签)。获取方式如下:
    tag.string
    还是以上面那个例子,加上下面这行,然后执行:
    print('NavigableString is:', find.string)

    4.1.3 BeautifulSoup

    BeautifulSoup对象表示一个文档的全部内容。支持遍历文档树和搜索文档树。

    4.1.4 Comment

    这个对象其实就是HTML和XML中的注释。

    markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
    soup = BeautifulSoup(markup)
    comment = soup.b.string
    type(comment)
    # <class 'bs4.element.Comment'>
    

    有些时候,我们并不想获取HTML中的注释内容,所以用这个类型来判断是否是注释。

    if type(SomeString) == bs4.element.Comment:
        print('该字符是注释')
    else:
        print('该字符不是注释')
    

    4.2 BeautifulSoup遍历方法

    4.2.1 节点和标签名

    可以使用子节点、父节点、 及标签名的方式遍历:

    soup.head #查找head标签
    soup.p #查找第一个p标签
    
    #对标签的直接子节点进行循环
    for child in title_tag.children:
        print(child)
    
    soup.parent #父节点
    
    # 所有父节点
    for parent in link.parents:
        if parent is None:
            print(parent)
        else:
            print(parent.name)
    
    # 兄弟节点
    sibling_soup.b.next_sibling #后面的兄弟节点
    sibling_soup.c.previous_sibling #前面的兄弟节点
    
    #所有兄弟节点
    for sibling in soup.a.next_siblings:
        print(repr(sibling))
    
    for sibling in soup.find(id="link3").previous_siblings:
        print(repr(sibling))
    

    4.2.2 搜索文档树

    最常用的当然是find()和find_all()啦,当然还有其他的。比如find_parent() 和 find_parents()、 find_next_sibling() 和 find_next_siblings() 、find_all_next() 和 find_next()、find_all_previous() 和 find_previous() 等等。
    我们就看几个常用的,其余的如果用到就去看官方文档哦。

    • find_all()
      搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件。返回值类型是bs4.element.ResultSet。
      完整的语法:
      find_all( name , attrs , recursive , string , **kwargs )
      这里有几个例子
    soup.find_all("title")
    # [<title>The Dormouse's story</title>]
    #
    soup.find_all("p", "title")
    # [<p class="title"><b>The Dormouse's story</b></p>]
    # 
    soup.find_all("a")
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
    #
    soup.find_all(id="link2")
    # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
    #
    import re
    soup.find(string=re.compile("sisters"))
    # u'Once upon a time there were three little sisters; and their names were\n'
    

    name 参数:可以查找所有名字为 name 的tag。
    attr 参数:就是tag里的属性。
    string 参数:搜索文档中字符串的内容。
    recursive 参数: 调用tag的 find_all() 方法时,Beautiful Soup会检索当前tag的所有子孙节点。如果只想搜索tag的直接子节点,可以使用参数 recursive=False 。

    • find()
      与find_all()类似,只不过只返回找到的第一个值。返回值类型是bs4.element.Tag。
      完整语法:
      find( name , attrs , recursive , string , **kwargs )
      看例子:
    soup.find('title')
    # <title>The Dormouse's story</title>
    #
    soup.find("head").find("title")
    # <title>The Dormouse's story</title>
    

    基本功已经练完,开始实战!

    五、继续上一篇实例


    继续上一篇的网站Unsplash,我们在首页选中图片,查看html代码。发现所有的图片都在a标签里,并且class都是cV68d,如下图。

    通过仔细观察,发现图片的链接在style中的background-image中有个url。这个url就包含了图片的地址,url后面跟了一堆参数,可以看到其中有&w=XXX&h=XXX,这个是宽度和高度参数。我们把高度和宽度的参数去掉,就能获取到大图。下面,我们先获取到所有的含有图片的a标签,然后在循环获取a标签中的style内容。

    其实在图片的右下方有一个下载按钮,按钮的标签中有一个下载链接,但是该链接并不能直接请求到图片,需要跳转几次,通过获取表头里的Location才能获取到真正的图片地址。后续我再以这个角度获取图片写一篇博文,咱们现根据能直接获取到的url稍做处理来获取图片。小伙伴儿们也可能会发现其他的方式来获取图片的url,都是可以的,尽情的尝试吧!

    import requests #导入requests 模块
    from bs4 import BeautifulSoup  #导入BeautifulSoup 模块
    
    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36'}  #给请求指定一个请求头来模拟chrome浏览器
    web_url = 'https://unsplash.com'r = requests.get(web_url, headers=headers) #像目标url地址发送get请求,返回一个response对象
    all_a = BeautifulSoup(r.text, 'lxml').find_all('a', class_='cV68d')  #获取网页中的class为cV68d的所有a标签
    for a in all_a:
      print(a['style']) #循环获取a标签中的style
    

    这里的find_all('a', class_='cV68d') 是找到所有class为cV68d的a标签,返回的是一个list,所以可以用for循环获取每个a标签。
    还有,get请求使用了headers参数,这个是用来模拟浏览器的。如何知道‘User-Agent’是什么呢?
    在你的Chrome浏览器中,按F12,然后刷新网页,看下图就可以找到啦。

    OK,我们来执行以下上面的代码,结果如下:


    接下来的任务是在一行的文本中取到图片的url。仔细看每一行的字符串,两个双引号之间的内容就是图片的url了,所以我们Python的切片功能来截取这中间的内容。

    改写for循环中的内容:

    for a in all_a: 
        img_str = a['style'] #a标签中完整的style字符串
        print(img_str[img_str.index('"')+1 : img_str.index('"',img_str[img_str.index('"')+1)]) #使用Python的切片功能截取双引号之间的内容
    

    获取到url后还要把宽度和高度的参数去掉。

            for a in all_a:
                img_str = a['style'] #a标签中完整的style字符串
                print('a标签的style内容是:', img_str)
                first_pos = img_str.index('"') + 1
                second_pos = img_str.index('"',first_pos)
                img_url = img_str[first_pos: second_pos] #使用Python的切片功能截取双引号之间的内容
                width_pos = img_url.index('&w=')
                height_pos = img_url.index('&q=')
                width_height_str = img_url[width_pos : height_pos]
                print('高度和宽度数据字符串是:', width_height_str)
                img_url_final = img_url.replace(width_height_str, '')
                print('截取后的图片的url是:', img_url_final)
    

    有了这些图片的url,就可以通过继续发请求的方式获取图片啦。接下来我们先来封装一下发请求的代码。
    先创建一个类:

    class BeautifulPicture(): 
       def __init__(self):  #类的初始化操作
            self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'}  #给请求指定一个请求头来模拟chrome浏览器
            self.web_url = 'https://unsplash.com' #要访问的网页地址
            self.folder_path = 'D:\BeautifulPicture'  #设置图片要存放的文件目录
    

    然后封装request请求:

        def request(self, url):  #返回网页的response
            r = requests.get(url)  # 像目标url地址发送get请求,返回一个response对象
            return r
    

    我们在文件目录下保存图片的话,要先创建文件目录。所以再添加一个创建目录的方法:
    要先引入os库哦。
    import os
    然后是方法定义:

        def mkdir(self, path):  ##这个函数创建文件夹
            path = path.strip()
            isExists = os.path.exists(path)
            if not isExists:
                print('创建名字叫做', path, '的文件夹')
                os.makedirs(path)
                print('创建成功!')
            else:
                print(path, '文件夹已经存在了,不再创建')
    

    再然后是保存图片啦。

        def save_img(self, url, name): ##保存图片
            print('开始保存图片...')
            img = self.request(url)
            time.sleep(5)
            file_name = name + '.jpg'
            print('开始保存文件')
            f = open(file_name, 'ab')
            f.write(img.content)
            print(file_name,'文件保存成功!')
            f.close()
    

    工具方法都已经准备完毕,开始我们的逻辑部分:

        def get_pic(self):
            print('开始网页get请求')
            r = self.request(self.web_url)
            print('开始获取所有a标签')
            all_a = BeautifulSoup(r.text, 'lxml').find_all('a', class_='cV68d')  #获取网页中的class为cV68d的所有a标签
            print('开始创建文件夹')
            self.mkdir(self.folder_path)  #创建文件夹
            print('开始切换文件夹')
            os.chdir(self.folder_path)   #切换路径至上面创建的文件夹
            i = 1 #后面用来给图片命名
            for a in all_a:
                img_str = a['style'] #a标签中完整的style字符串
                print('a标签的style内容是:', img_str)
                first_pos = img_str.index('"') + 1
                second_pos = img_str.index('"',first_pos)
                img_url = img_str[first_pos: second_pos] #使用Python的切片功能截取双引号之间的内容
                width_pos = img_url.index('&w=')
                height_pos = img_url.index('&q=')
                width_height_str = img_url[width_pos : height_pos]
                print('高度和宽度数据字符串是:', width_height_str)
                img_url_final = img_url.replace(width_height_str, '')
                print('截取后的图片的url是:', img_url_final)
                self.save_img(img_url_final, str(i))
                i += 1
    

    最后就是执行啦:

    beauty = BeautifulPicture()  #创建一个类的实例
    beauty.get_pic()  #执行类中的方法
    

    最后来一个完整的代码,对中间的一些部分进行了封装和改动,并添加了每部分的注释,一看就明白了。有哪块有疑惑的可以留言~~

    import requests #导入requests 模块
    from bs4 import BeautifulSoup  #导入BeautifulSoup 模块
    import os  #导入os模块
    
    class BeautifulPicture():
    
        def __init__(self):  #类的初始化操作
            self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1'}  #给请求指定一个请求头来模拟chrome浏览器
            self.web_url = 'https://unsplash.com'  #要访问的网页地址
            self.folder_path = 'D:\BeautifulPicture'  #设置图片要存放的文件目录
    
        def get_pic(self):
            print('开始网页get请求')
            r = self.request(self.web_url)
            print('开始获取所有a标签')
            all_a = BeautifulSoup(r.text, 'lxml').find_all('a', class_='cV68d')  #获取网页中的class为cV68d的所有a标签
            print('开始创建文件夹')
            self.mkdir(self.folder_path)  #创建文件夹
            print('开始切换文件夹')
            os.chdir(self.folder_path)   #切换路径至上面创建的文件夹
    
            for a in all_a: #循环每个标签,获取标签中图片的url并且进行网络请求,最后保存图片
                img_str = a['style'] #a标签中完整的style字符串
                print('a标签的style内容是:', img_str)
                first_pos = img_str.index('"') + 1
                second_pos = img_str.index('"',first_pos)
                img_url = img_str[first_pos: second_pos] #使用Python的切片功能截取双引号之间的内容
                #获取高度和宽度的字符在字符串中的位置
                width_pos = img_url.index('&w=')
                height_pos = img_url.index('&q=')
                width_height_str = img_url[width_pos : height_pos] #使用切片功能截取高度和宽度参数,后面用来将该参数替换掉
                print('高度和宽度数据字符串是:', width_height_str)
                img_url_final = img_url.replace(width_height_str, '')  #把高度和宽度的字符串替换成空字符
                print('截取后的图片的url是:', img_url_final)
                #截取url中参数前面、网址后面的字符串为图片名
                name_start_pos = img_url.index('photo')
                name_end_pos = img_url.index('?')
                img_name = img_url[name_start_pos : name_end_pos]
                self.save_img(img_url_final, img_name) #调用save_img方法来保存图片
    
        def save_img(self, url, name): ##保存图片
            print('开始请求图片地址,过程会有点长...')
            img = self.request(url)
            file_name = name + '.jpg'
            print('开始保存图片')
            f = open(file_name, 'ab')
            f.write(img.content)
            print(file_name,'图片保存成功!')
            f.close()
    
        def request(self, url):  #返回网页的response
            r = requests.get(url, headers=self.headers)  # 像目标url地址发送get请求,返回一个response对象。有没有headers参数都可以。
            return r
    
        def mkdir(self, path):  ##这个函数创建文件夹
            path = path.strip()
            isExists = os.path.exists(path)
            if not isExists:
                print('创建名字叫做', path, '的文件夹')
                os.makedirs(path)
                print('创建成功!')
            else:
                print(path, '文件夹已经存在了,不再创建')
    
    beauty = BeautifulPicture()  #创建类的实例
    beauty.get_pic()  #执行类中的方法
    

    执行的过程中可能会有点慢,这是因为图片本身比较大!如果仅仅是为了测试爬虫,则可以不把图片的宽度和高度替换掉,图片就没那么大啦,运行过程会快很多。

    六、后语


    伙伴儿们是不是发现,我们只获取到了10张图片,并没有把网站所有照片都下载下来。

    这是因为咱们爬取的网站是下拉刷新的,下拉一次,刷新10张照片。那么,该如何爬取这种下拉刷新的网页呢?请看下一篇喽。

    相关文章

      网友评论

      • e1e1c1a56340:请问使用find_all方法得到了一个类型是<class 'bs4.element.ResultSet'>的对象,他是不是就没有上面的四种对象类型了啊?比如从find_all得到的对象的基础上得到当前标签的string应该怎么弄呢?先谢谢了:blush:
        e1e1c1a56340:奥,我知道了,吼吼
        >>> type(th_all)
        <class 'bs4.element.ResultSet'>
        >>> for th in th_all:
        type(th)


        <class 'bs4.element.Tag'>
        <class 'bs4.element.Tag'>
        <class 'bs4.element.Tag'>
        <class 'bs4.element.Tag'>
      • Emilyhhh:首先谢谢楼主的分享 讲解的非常仔细 另外 请问楼主那个动态图片是怎么做的呢
        Emilyhhh:@阿里波特 谢谢你:relaxed:
        阿里波特:@Emily_d4b6 截图软件GifCam
      • 抬头挺胸才算活着:楼主,上面这句语句之后all_a = BeautifulSoup(r.text, 'lxml').find_all('a', class_='_1pn7R'),没有返回任何值,好像是网站发生了变化,烦请楼主帮忙指教下,有几个人在你博客园那里也出现了这样子的问他
        阿里波特:@李悦城 好的,谢谢反馈,最近特别忙,一直没时间再写东西。。。
      • 304891020da0:写的真好!期待下一篇!
      • 9ca275fadd95: all_a = BeautifulSoup(r.text, 'lxml').find_all('a', class_='cV68d') 这一句具体什么意思呢,还有我的报错是:ValueError: Couldn't find a tree builder with the features you requested: lxml. Do you need to install a parser library? :joy: 请问怎么解决呢
        阿里波特:@nimeili 木有安装lxml

      本文标题:Python爬虫小白入门(三)BeautifulSoup库

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