zoukankan      html  css  js  c++  java
  • 利用爬虫获取豆瓣上可能喜欢的书籍

    利用爬虫获取豆瓣上可能喜欢的书籍

    标签: 爬虫 Python


    1.目标

    博主比較喜欢看书,购物车里面会放很多书,然后等打折的时候开个大招。

    然而会遇到一个问题,就是不知道什么书是好书,不知道一本书究竟好不好,所以经常会去豆瓣读书看看有什么好书推荐,只是这样效率比較低。近期学习了爬虫的基础知识。有点手痒,故写一个爬取豆瓣推荐书籍的爬虫,和大家分享一下。

    我们给爬虫设置一个起始url,然后爬取豆瓣在该url推荐的书籍及推荐书籍的推荐书籍……直到达到预设的爬取次数或者某个终止条件。

    因为篇幅有限。不可能解说太多的基础知识,假设大家认为理解有困难的话,能够看看慕课网Python开发简单爬虫的视频。这个视频非常的赞。

    2.爬虫框架

    爬虫一共同拥有5个模块:调度器,url管理器,html下载器,html解析器和html输出器。

    爬虫调度器通过调度其它的模块完毕任务,上面推荐的视频中有一张非常棒的图说明了爬虫通过调度器执行的流程:

    当中的应用模块相应的是输出器。解释一下执行流程:

    (1) 调度器查询是否有未爬取的url
    (2) 假设“无”则跳转至(8)。假设“有”则获取一个url
    (3) 下载器依据获取的url下载html数据
    (4) 解析器解析下载的html数据,获得新的url和有价值数据
    (5) 调度器将获得的url传递给url管理器
    (6) 调度器将获得的有价值数据传递给输出器
    (7) 跳转至(1)
    (8) 将输出器中的有价值数据全部输出

    3.爬虫实现

    3.1 url管理器实现

    url管理器对未爬取和已爬取的url进行管理。记录未爬取的url是为了对新的网页进行爬取,记录已爬取的url是为了防止爬取已经爬取过的网页。

    url管理器中有2个集合,分别记录未爬取和已爬取的url。
    url管理器中有4种方法,详见代码凝视:

    #file: url_manager.py
    
    class UrlManager(object):
        def __init__(self):
            self.new_urls = set() #未爬取url集合
            self.old_urls = set() #已爬取url集合
    
        #加入新的单个url。仅仅加入不在新旧集合中的url
        def add_new_url(self, url):
            if url is None:
                return
            if url not in self.new_urls and url not in self.old_urls:
                self.new_urls.add(url)
    
        #加入新的一堆url。调用add_new_url加入
        def add_new_urls(self, urls):
            if urls is None or len(urls) == 0:
                return
            for url in urls:
                self.add_new_url(url)
    
        #是否还有未爬取的url    
        def has_new_url(self):
            return len(self.new_urls) != 0
    
        #获取一个新的url,将该url从未爬取集合删除,加入到已爬取集合中    
        def get_new_url(self):
            new_url = self.new_urls.pop()
            self.old_urls.add(new_url)
            return new_url

    3.2 html下载器实现

    html下载器依据传入的url下载网页的html数据。

    下载器须要用到urllib2库。这个库是Python编写爬虫时经常使用的库。具有依据给定的url获取html数据。伪装成浏览器訪问网页,设置代理等功能。因为我们获取的是豆瓣的推荐书籍,不须要登录,所以仅仅使用依据url获取html数据的功能就可以。

    须要注意的是,豆瓣是个非常不错的站点,所以可能有非常多的爬虫在爬取豆瓣。因此豆瓣也有非常多的反爬虫机制。最直接的反爬虫机制就是禁制程序訪问豆瓣,因此我们的爬虫要伪装成浏览器进行页面爬取

    #file: html_downloader.py
    
    import urllib2
    
    class HtmlDownloader(object):
        def download(self, url):
            if url is None:
                return None
            try:
                request = urllib2.Request(url)
                request.add_header('user-agent', 'Mozilla/5.0') #加入头信息,伪装成Mozilla浏览器
                response = urllib2.urlopen(request) #訪问这个url
            except urllib2.URLError, e: #假设出错则打印错误代码和信息
                if hasattr(e,"code"):
                    print e.code #错误代码,如403
                if hasattr(e,"reason"):
                    print e.reason #错误信息,如Forbidden
            if response.getcode() != 200: #200表示訪问成功
                return None
            return response.read() #返回该url的html数据

    3.3 html解析器实现

    解析器解析下载的html数据。获得新的url和有价值数据,该模块是爬虫最麻烦的模块。

    解析器须要用到BeautifulSoupre库。
    BeautifulSoup是用Python写的一个HTML/XML的解析器。能够非常方便的从HTML/XML字符串中提取信息。


    re是Python默认的正則表達式模块。提供正則表達式相关的操作。

    3.3.1 parser()方法实现

    解析器对外部仅仅提供一个方法parser。该方法调用内部的两个方法实现解析功能:

    #file: html_parser.py
    
    from bs4 import BeautifulSoup
    import re
    
    class HtmlParser(object):
        def parse(self, page_url, html_cont):
            if page_url is None or html_cont is None:
                return
            soup = BeautifulSoup(html_cont, 'html.parser', from_encoding='utf-8') #创建一个beautifulsoup对象
            new_urls = self._get_new_urls(soup) #调用内部方法提取url
            new_data = self._get_new_data(page_url, soup) #调用内部方法提取有价值数据
    
            return new_urls, new_data

    3.3.2 _get_new_urls()方法实现

    内部方法_get_new_urls()从传递的beautifulsoup对象中提取url信息,那么究竟提取的哪个部分的url?我们以豆瓣《代码大全》页面为样例进行解说。

    打开该页面。在“喜欢读‘代码大全(第2版)’的人也喜欢”处(即1处)点击鼠标右键,审查元素,这时会在浏览器下方弹出网页代码,只是我们要的不是这个标题,将鼠标移动到其父结点处(即2处),会发现推荐的书籍都被蓝色覆盖了,即<div id="db-rec-section" class="block5 subject_show knnlike">包括的url都是我们要提取的url。



    在设计模式处点击鼠标右键,审查元素。能够看到《设计模式》的url为https://book.douban.com/subject/1052241/,使用相同的方法能够查看到其它书籍的url,这些url的前缀都是一样的,不同仅仅是最后的数字不一样。且这些数字或为7位,或为8位,因此推荐书籍url的正則表達式能够写为"https://book.douban.com/subject/d+/$"

    内部方法_get_new_urls()的实现代码例如以下:

    #file: html_parser.py
    
    def _get_new_urls(self, soup):
            new_urls = set()
            #相同喜欢区域:<div id="db-rec-section" class="block5 subject_show knnlike">
            recommend = soup.find('div', class_='block5 subject_show knnlike') #先找到推荐书籍的区域
            #<a href="https://book.douban.com/subject/11614538/" class="">程序猿的职业素质</a>
            links = recommend.find_all('a', href=re.compile(r"https://book.douban.com/subject/d+/$")) #在推荐区域中寻找全部含有豆瓣书籍url的结点
            for link in links: 
                new_url = link['href'] #从结点中提取超链接,即url
                new_urls.add(new_url)
            return new_urls

    一些说明:

    • find()find_all()查找的是符合其括号里条件的结点,如上面第4行代码表示查找标签为divclass值为block5 subject_show knnlike的结点。因为class是Python中的保留字,所以find()中加了一个下划线即class_
    • find()是在html中寻找第一个符合条件的结点,这里的<div id="db-rec-section" class="block5 subject_show knnlike">是唯一的,所以请放心使用find()
    • find_all()是在html中寻找全部的符合条件的结点
    • 使用正則表達式的时候。前面加了一个字母r,表示字符串是“原生的”,不须要进行字符串转义。直接写正則表達式就可以了。假设不加字母r,特殊符号在正則表達式中转义一次。在字符串中转义一次。写起来就十分的麻烦

    3.3.3 _get_new_data()方法实现

    作为一个读者。关注的主要信息就是书名,评分,作者,出版社,出版年,页码以及价钱,其它的基本就不考虑了。因此我们就提取以上列举的信息。

    • 用审查元素的方法提取书名,发现包括书名的结点为<span property="v:itemreviewed">代码大全(第2版)</span>
    • 用审查元素的方法提取评分,发现包括评分的结点为<strong class="ll rating_num " property="v:average"> 9.3 </strong>
    • 用审查元素的方法提取作者等基本信息,发现全部的信息都是<div id="info" class="">结点的子结点

    书名和评分直接使用find()找到相关结点。然后使用.string方法提取结点的内容。可是书本的基本信息这样就不行了,因为作者。出版社等结点的标签是一样。怎么办?既然我们想提取的就是作者。出版社等信息。那么直接依据结点内容搜索

    首先找到<div id="info" class="">结点,然后在该结点中使用find(text='出版社')找到内容为“出版社”的结点,我们想要的“电子工业出版社”就是该结点的下一个结点。使用next_element就能够訪问当前结点的下一个结点。got it!

    内部方法_get_new_data()的实现代码例如以下:

    #file: html_parser.py
    
    def _get_new_data(self, page_url, soup):
            res_data = {}
            #url
            res_data['url'] = page_url
            # <span property="v:itemreviewed">代码大全</span>
            res_data['bookName'] = soup.find('span', property='v:itemreviewed').string
            # <strong class="ll rating_num " property="v:average"> 9.3 </strong>
            res_data['score'] = soup.find('strong', class_='ll rating_num ').string
            '''
            <div id="info" class="">
                <span>
                  <span class="pl"> 作者</span>:
                  <a class="" href="/search/Steve%20McConnell">Steve McConnell</a>
                </span><br>
                <span class="pl">出版社:</span> 电子工业出版社<br>
                <span class="pl">出版年:</span> 2007-8<br>
                <span class="pl">页数:</span> 138<br>
                <span class="pl">定价:</span> 15.00元<br>
            </div>
            '''
            info = soup.find('div', id='info')
            try: #有的页面信息不全
                res_data['author'] = info.find(text=' 作者').next_element.next_element.string
                res_data['publisher'] = info.find(text='出版社:').next_element
                res_data['time'] = info.find(text='出版年:').next_element
                res_data['price'] = info.find(text='定价:').next_element
                res_data['intro'] = soup.find('div', class_='intro').find('p').string
            except:
                return None
            if res_data['intro'] == None:
                return None
    
            return res_data

    一些说明:

    • 有的页面没有“出版社”,“价格”等信息。评分高的书籍信息都是完整的,故使用tyr-except将这些页面舍弃
    • 有的页面有“简单介绍”标签,可是没有简单介绍,眼下发现这样的情况的都是旧版不再印刷的书。故使用tyr-except将这些页面舍弃

    3.4 html输出器实现

    输出器保存已经爬取页面的有价值信息。然后在脚本结束时将这些信息以较为友好的html格式输出。

    输出器将全部的信息保存在一个列表里面,保存数据方法的代码例如以下:

    #file: html_outputer.py
    
    class HtmlOutputer(object):
        def __init__(self):
            self.datas = []
    
        def collect_data(self, data):
            if data is None:
                return
            self.datas.append(data)

    数据以html格式输出既简单又方便。我们能够先用nodepad++编写自己想要的html格式,然后使用浏览器打开观察,不断的改进,终于得到自己想要的数据展现形式,我的html格式例如以下:

    <html>
    <head><title>GoodBooks</title></head>
    <body>
    
    <h2><a href='https://book.douban.com/subject/1477390/' target=_blank>代码大全(第2版)</a></h2>
    <table border="1">
    <tr><td>评分:</td><td><b>9.3</b></td></tr>
    <tr><td>作者:</td><td>[美] 史蒂夫·迈克康奈尔</td></tr>
    <tr><td>定价:</td><td>128.00元</td></tr>
    <tr><td>出版社:</td><td>电子工业出版社</td></tr>
    <tr><td>出版时间:</td><td>2006-3</td></tr>
    </table>
    <p>
    简单介绍:第2版的《代码大全》是著名IT畅销书作者史蒂夫·迈克康奈尔11年前的经典著作的全新演绎:第2版不是第一版的简单修订增补,而是全然进行了重写;添加了非常多与时俱进的内容。

    这也是一本完整的软件构建手冊。涵盖了软件构建过程中的全部细节。它从软件质量和编程思想等方面论述了软件构建的各个问题。并具体论述了紧跟潮流的新技术、高屋建瓴的观点、通用的概念,还含有丰富而典型的程序演示样例。这本书中所论述的技术不仅填补了0基础与高级编程技术之间的空白。并且也为程序猿们提供了一个有关编程技巧的信息来源。

    这本书对经验丰富的程序猿、技术带头人、自学的程序猿及差点儿不懂太多编程技巧的学生们都是大有裨益的。

    能够说,不管是什么背景的读者。阅读这本书都有助于在更短的时间内、更easy地写出更好的程序。 </p> <hr> </body> </html>

    最后的<hr>是切割线。浏览器中的效果:

    点击查看原图

    把具体的内容使用%s格式化输出就可以,须要注意的是字符变量后面加上.encode('utf-8'),将字符的编码格式改为utf-8.

    输出器的输出代码例如以下:

    #file: html_outputer.py
    
    def output_html(self):
            fout = open('GoodBooks.html', 'w')
    
            fout.write('<html>')
            fout.write('<meta charset="UTF-8">') #告诉浏览器是utf-8编码
            fout.write('<title>GoodBooks_moverzp</title>')   
            fout.write('<body>')
    
            for data in self.datas:
                print data['bookName'], data['score']
                fout.write("<h2><a href='%s' target=_blank>%s</a></h2>" % (data['url'].encode('utf-8'), data['bookName'].encode('utf-8')))
                fout.write('<table border="1">')
                fout.write('<tr><td>评分:</td><td><b>%s</b></td></tr>' % data['score'].encode('utf-8'))
                fout.write('<tr><td>作者:</td><td>%s</td></tr>' % data['author'].encode('utf-8'))
                fout.write('<tr><td>定价:</td><td>%s</td></tr>' % data['price'].encode('utf-8'))
                fout.write('<tr><td>出版社:</td><td>%s</td></tr>' % data['publisher'].encode('utf-8'))
                fout.write('<tr><td>出版时间:</td><td>%s</td></tr>' % data['time'].encode('utf-8'))
                fout.write('</table>')
                fout.write('<p>%s' % data['intro'].encode('utf-8'))
                fout.write('</p><hr>') #加上切割线
    
            fout.write('</body>')
            fout.write('</html>')

    3.5 调度器实现

    调度器是爬虫的“大脑”,进行任务的分配。将第2节爬虫框架的步骤写成代码就实现了调度器。

    下面是调度器的代码实现,以《代码大全》为起始url,抓取50个推荐书籍的信息:

    #file: spider_main.py
    
    import url_manager, html_downloader, html_parser, html_outputer
    import time
    
    class SpiderMain(object):
        def __init__(self):
            self.urls = url_manager.UrlManager() #url管理器
            self.downloader = html_downloader.HtmlDownloader() #html网页下载器
            self.parser = html_parser.HtmlParser() #html分析器
            self.outputer = html_outputer.HtmlOutputer() #html输出器
    
        def craw(self, root_url):
            count = 1
            self.urls.add_new_url(root_url)
            try:
                while self.urls.has_new_url():
                    new_url = self.urls.get_new_url() #从url管理器中获取一个未爬取的url
                    print 'craw %d : %s' % (count, new_url)
                    html_cont = self.downloader.download(new_url) #下载该url的html
                    new_urls, new_data = self.parser.parse(new_url, html_cont) #分析html。返回urls和data
                    self.urls.add_new_urls(new_urls) #将获取的urls加入进未爬取的url集合中,排除已爬取过的url
                    self.outputer.collect_data(new_data) #数据都在内存中
                    time.sleep(0.1)
                    if count == 50:
                        break
                    count += 1
    
            except:
                print 'craw failed'
    
            self.outputer.output_html()
    
    if __name__ == "__main__":
        root_url = "https://book.douban.com/subject/1477390/" #起始地址为《代码大全》
        obj_spider = SpiderMain()
        obj_spider.craw(root_url)

    终于爬取的结果例如以下:

    点击查看完整图

    4.存在的问题

    Q1:url管理器中使用set()保存未爬取的url,获取新的url时,使用的是pop()方法,该方法是随机从集合中取出一个元素并删除。这可能会导致我们我们爬取的书籍与我们设置的第一个url相去甚远。最极端的情况是每次得到的url都是推荐书籍中类似度最低的书籍。那么爬不了几次获取的信息都是“垃圾信息”。
    解决方法:使用队列保存未爬取的url,这样爬取的轨迹就是以初始url为中心均匀扩散。

    Q2:不设置抓取页面的次数。在700次左右会发生403Forbidden错误。
    解决方法:八成是豆瓣检測到了爬虫,然后把IP封了。能够使用IP代理的方法防止IP被封。

    5.能够做的改进

    • 保存已爬取和未爬取的url到文件或者数据库。能够实现断点爬取
    • 依照评分推荐层级对爬取的结果进行排序,将可能喜欢的评分更高的书放在前面
    • 本文爬取的数据比較少,所以就直接放在内存中。假设要爬取较多的数据。能够每爬取1条或者n条后,将数据存储在文件或数据库中
    • 当须要做大量数据爬取的时候,能够使用多线程加高速度
    • 添加简单的GUI

    6.总结

    • 源代码在我的GitHub。本节代码仅仅是一个最初版本号,会不断的完好的
    • 本文爬虫的框架有5个模块/文件:调度器,url管理器,下载器,解析器和输出器
    • 爬虫框架是最重要,本文的爬虫比較简单。框架是自己写的,可是在较大型的应用中可能就捉襟见肘了。推荐第三方的爬虫框架Scrapy
    • 基本的库有3个:urllib2BeautifulSoupre
    • server的反爬虫和爬虫一直在互相斗争,爬虫代码一般都有时效性,假设站点html代码变化了,爬虫的解析器就得重写
  • 相关阅读:
    power desinger 学习笔记<五>
    power desinger 学习笔记<四>
    power desinger 学习笔记<八>
    kill session真的能杀掉进程吗
    转: Oracle AWR 报告 每天自动生成并发送邮箱
    Bootstrap 图片
    Bootstrap历练实例:禁用的按钮
    Bootstrap历练实例:点击激活的按钮
    Bootstrap历练实例:块级按钮
    Bootstrap历练实例:超小的按钮
  • 原文地址:https://www.cnblogs.com/claireyuancy/p/7261530.html
Copyright © 2011-2022 走看看