标签: 爬虫 Pythonhtml
博主比較喜欢看书,购物车里面会放不少书,而后等打折的时候开个大招。node
然而会遇到一个问题,就是不知道什么书是好书,不知道一本书究竟好很差,因此常常会去豆瓣读书看看有什么好书推荐,只是这样效率比較低。近期学习了爬虫的基础知识。有点手痒,故写一个爬取豆瓣推荐书籍的爬虫,和你们分享一下。python
咱们给爬虫设置一个起始url,而后爬取豆瓣在该url推荐的书籍及推荐书籍的推荐书籍……直到达到预设的爬取次数或者某个终止条件。git
由于篇幅有限。不可能解说太多的基础知识,假设你们认为理解有困难的话,可以看看慕课网Python开发简单爬虫的视频。这个视频很是的赞。github
爬虫一共同拥有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种方法,详见代码凝视:markdown
#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()
方法实现内部方法_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
,特殊符号在正則表達式中转义一次。在字符串中转义一次。写起来就十分的麻烦_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
一些说明:
输出器保存已经爬取页面的有价值信息。而后在脚本结束时将这些信息以较为友好的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>')
调度器是爬虫的“大脑”,进行任务的分配。将第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)
终于爬取的结果例如如下:
点击查看完整图
Q1:url管理器中使用set()
保存未爬取的url,获取新的url时,使用的是pop()
方法,该方法是随机从集合中取出一个元素并删除。这可能会致使咱们咱们爬取的书籍与咱们设置的第一个url相去甚远。最极端的状况是每次获得的url都是推荐书籍中相似度最低的书籍。那么爬不了几回获取的信息都是“垃圾信息”。
解决方法:使用队列保存未爬取的url,这样爬取的轨迹就是以初始url为中心均匀扩散。
Q2:不设置抓取页面的次数。在700次左右会发生403Forbidden
错误。
解决方法:八成是豆瓣检測到了爬虫,而后把IP封了。可以使用IP代理的方法防止IP被封。
Scrapy
urllib2
, BeautifulSoup
,re