标签: 爬虫 Python
博主比较喜欢看书,购物车里面会放许多书,然后等打折的时候开个大招。然而会遇到一个问题,就是不知道什么书是好书,不知道一本书到底好不好,所以常常会去豆瓣读书看看有什么好书推荐,不过这样效率比较低。最近学习了爬虫的基础知识,有点手痒,故写一个爬取豆瓣推荐书籍的爬虫,和大家分享一下。
我们给爬虫设置一个起始url,然后爬取豆瓣在该url推荐的书籍及推荐书籍的推荐书籍……直到达到预设的爬取次数或者某个终止条件。
由于篇幅有限,不可能讲解太多的基础知识,如果大家觉得理解有困难的话,可以看看慕课网Python开发简单爬虫的视频,这个视频非常的赞。
爬虫一共有5个模块:调度器,url管理器,html下载器,html解析器和html输出器。
爬虫调度器通过调度其它的模块完成任务,上面推荐的视频中有一张非常棒的图说明了爬虫通过调度器运行的流程:
其中的应用模块对应的是输出器,解释一下运行流程:
(1) 调度器查询是否有未爬取的url
(2) 如果“无”则跳转至(8),如果“有”则获取一个url
(3) 下载器根据获取的url下载html数据
(4) 解析器解析下载的html数据,获得新的url和有价值数据
(5) 调度器将获得的url传递给url管理器
(6) 调度器将获得的有价值数据传递给输出器
(7) 跳转至(1)
(8) 将输出器中的有价值数据全部输出
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
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数据
解析器解析下载的html数据,获得新的url和有价值数据,该模块是爬虫最麻烦的模块。
解析器需要用到BeautifulSoup
和re
库。
BeautifulSoup
是用Python写的一个HTML/XML的解析器,可以很方便的从HTML/XML字符串中提取信息。
re
是Python默认的正则表达式模块,提供正则表达式相关的操作。
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
_get_new_urls()
方法实现内部方法 内部方法 一些说明: 作为一个读者,关注的主要信息就是书名,评分,作者,出版社,出版年,页码以及价钱,其他的基本就不考虑了。因此我们就提取以上列举的信息。 书名和评分直接使用 首先找到 内部方法 一些说明: 输出器保存已经爬取页面的有价值信息,然后在脚本结束时将这些信息以较为友好的html格式输出。 输出器将所有的信息保存在一个列表里面,保存数据方法的代码如下: 数据以html格式输出既简单又方便,我们可以先用nodepad++编写自己想要的html格式,然后使用浏览器打开观察,不断的改进,最终得到自己想要的数据展现形式,我的html格式如下: 最后的 把具体的内容使用 输出器的输出代码如下: %s' 调度器是爬虫的“大脑”,进行任务的分配。将第2节爬虫框架的步骤写成代码就实现了调度器。 以下是调度器的代码实现,以《代码大全》为起始url,抓取50个推荐书籍的信息: 最终爬取的结果如下: Q1:url管理器中使用 Q2:不设置抓取页面的次数,在700次左右会发生_get_new_urls()
从传递的beautifulsoup
对象中提取url信息,那么到底提取的哪个部分的url?我们以豆瓣《代码大全》页面为例子进行讲解。
打开该页面,在“喜欢读‘代码大全(第2版)’的人也喜欢”处(即1处)点击鼠标右键,审查元素,这时会在浏览器下方弹出网页代码,不过我们要的不是这个标题,将鼠标移动到其父结点处(即2处),会发现推荐的书籍都被蓝色覆盖了,即
在设计模式处点击鼠标右键,审查元素,可以看到《设计模式》的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()
#同样喜欢区域:
find()
与find_all()
查找的是符合其括号中条件的结点,如上面第4行代码表示查找标签为div
,class
值为block5 subject_show knnlike
的结点。由于class
是Python中的保留字,所以find()
中加了一个下划线即class_
find()
是在html中寻找第一个符合条件的结点,这里的find()
find_all()
是在html中寻找所有的符合条件的结点r
,表示字符串是“原生的”,不需要进行字符串转义,直接写正则表达式就行了。如果不加字母r
,特殊符号在正则表达式中转义一次,在字符串中转义一次,写起来就十分的麻烦3.3.3
_get_new_data()
方法实现
代码大全(第2版)
9.3
find()
找到相关结点,然后使用.string
方法提取结点的内容,但是书本的基本信息这样就不行了,因为作者,出版社等结点的标签是一样。怎么办?既然我们想提取的就是作者,出版社等信息,那么直接根据结点内容搜索。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
# 代码大全
res_data['bookName'] = soup.find('span', property='v:itemreviewed').string
# 9.3
res_data['score'] = soup.find('strong', class_='ll rating_num ').string
'''
'''
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
3.4 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>
<head><title>GoodBookstitle>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.3b>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-3td>tr>
table>
<p>
简介:第2版的《代码大全》是著名IT畅销书作者史蒂夫·迈克康奈尔11年前的经典著作的全新演绎:第2版不是第一版的简单修订增补,而是完全进行了重写;增加了很多与时俱进的内容。这也是一本完整的软件构建手册,涵盖了软件构建过程中的所有细节。它从软件质量和编程思想等方面论述了软件构建的各个问题,并详细论述了紧跟潮流的新技术、高屋建瓴的观点、通用的概念,还含有丰富而典型的程序示例。这本书中所论述的技术不仅填补了初级与高级编程技术之间的空白,而且也为程序员们提供了一个有关编程技巧的信息来源。这本书对经验丰富的程序员、技术带头人、自学的程序员及几乎不懂太多编程技巧的学生们都是大有裨益的。可以说,无论是什么背景的读者,阅读这本书都有助于在更短的时间内、更容易地写出更好的程序。
p>
<hr>
body>
html>
是分割线,浏览器中的效果:
点击查看原图%s
格式化输出即可,需要注意的是字符变量后面加上.encode('utf-8')
,将字符的编码格式改为utf-8.#file: html_outputer.py
def output_html(self):
fout = open('GoodBooks.html', 'w')
fout.write('')
fout.write('') #告诉浏览器是utf-8编码
fout.write('
%s
" % (data['url'].encode('utf-8'), data['bookName'].encode('utf-8')))
fout.write('')
fout.write('
')
fout.write(' ' % data['score'].encode('utf-8'))
fout.write('评分: %s ' % data['author'].encode('utf-8'))
fout.write('作者: %s ' % data['price'].encode('utf-8'))
fout.write('定价: %s ' % data['publisher'].encode('utf-8'))
fout.write('出版社: %s ' % data['time'].encode('utf-8'))
fout.write('出版时间: %s
') #加上分割线
fout.write('')
fout.write('')
3.5 调度器实现
#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.存在的问题
set()
保存未爬取的url,获取新的url时,使用的是pop()
方法,该方法是随机从集合中取出一个元素并删除。这可能会导致我们我们爬取的书籍与我们设置的第一个url相去甚远,最极端的情况是每次得到的url都是推荐书籍中相似度最低的书籍,那么爬不了几次获取的信息都是“垃圾信息”。
解决方法:使用队列保存未爬取的url,这样爬取的轨迹就是以初始url为中心均匀扩散。403Forbidden
错误。
解决方法:八成是豆瓣检测到了爬虫,然后把IP封了。可以使用IP代理的方法防止IP被封。5.可以做的改进
6.总结
Scrapy
urllib2
, BeautifulSoup
,re