您的位置:首页 > 编程语言 > Python开发

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

2016-04-05 21:39 726 查看

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

标签: 爬虫 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和有价值数据,该模块是爬虫最麻烦的模块。

解析器需要用到
BeautifulSoup
re
库。

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行代码表示查找标签为
div
class
值为
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版不是第一版的简单修订增补,而是完全进行了重写;增加了很多与时俱进的内容。这也是一本完整的软件构建手册,涵盖了软件构建过程中的所有细节。它从软件质量和编程思想等方面论述了软件构建的各个问题,并详细论述了紧跟潮流的新技术、高屋建瓴的观点、通用的概念,还含有丰富而典型的程序示例。这本书中所论述的技术不仅填补了初级与高级编程技术之间的空白,而且也为程序员们提供了一个有关编程技巧的信息来源。这本书对经验丰富的程序员、技术带头人、自学的程序员及几乎不懂太多编程技巧的学生们都是大有裨益的。可以说,无论是什么背景的读者,阅读这本书都有助于在更短的时间内、更容易地写出更好的程序。
</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个:
urllib2
BeautifulSoup
re


服务器的反爬虫和爬虫一直在互相斗争,爬虫代码一般都有时效性,如果网站html代码变化了,爬虫的解析器就得重写
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  python 爬虫