更新:已更新豆瓣电影Top250的脚本及网站
概述
经常用豆瓣读书的童鞋应该知道,豆瓣Top250用的是综合排序,除用户评分之外还考虑了很多比如是否畅销、点击量等等,这也就导致了一些近年来评分不高的畅销书在这个排行榜上高高在上远比一些经典名著排名还高,于是在这里打算重新给Top250纯按照用户打分排一下序。思路就是:先用爬虫爬下来豆瓣Top250的所有书目,然后存到本地之后进行排序,最后再做一个网站把排完序的书单重新挂上去。
先挂上成品网站:
豆瓣读书Top250逆序 | Alanzjl
豆瓣电影Top250逆序 | Alanzjl
这个网站完全按照豆瓣原网站制作,html代码都是直接扒豆瓣的。或者作为我的个人网站内嵌使用:
豆瓣读书Top250 | Homepage of Alan
豆瓣电影Top250 | Homepage of Alan
可以看到已经按照打分排序。
OK下面进入正文
代码
先说一下大体流程:
爬虫爬取信息->排序->生成Html文件
从爬取信息开始。
先打开豆瓣Top250网站,看看其URL:http://book.douban.com/top250?start=0,点一下“下一页”就可以看到跳转到了start=25的地址.哈,分析一下就可以知道每页显示25本书,正好10页,那就循环遍历10个页面就可以了。那要是不是25的整数倍呢?start=1呢?打开一看也是可以的,网站上面显示的第一本书就变成了第二本(start=0)是第一本,也就是说只要我们从start=0遍历到start=250这样每本书就是当前URL的第一本书。对于爬虫来讲这两种遍历方式(每次跳25个,爬虫爬取每页25本书以及每次跳一个,爬虫爬取得每页第一本书)都是可以的,但是实践发现前者使用的时候经常会有遗漏,比如这一次爬全了,下一次突然就不全了这样,也不太清楚是怎么回事就用第二种方法吧。
爬虫主要用到两个Python库,urllib(打开网站)和re(正则表达式)库,先写一下整体代码框架:
import re
import urllib
class book:
#book class, used as container
title = ""
author = ""
url = ""
img = ""
rate = 0.0
def __init__(self, Title, Author, Url, Img, Rate):
self.title = Title
self.author = Author
self.url = Url
self.img = Img
self.rate = Rate
def content(self):
return "Title:%s\tAuthor:%s\tUrl:%s\tImg:%s\tRate:%s\n"\
%(self.title,self.author,self.url,self.img,self.rate)
def makeHtml(blist, path):
#generate html file
def run():
count = 0
while count < 250:
url = 'http://book.douban.com/top250?start=%d'%count
page = urllib.urlopen(url).read()
count += 1
bookList = []
run()
bookListSorted = sorted(bookList, key=lambda ele:ele.rate, reverse=1)
makeHtml(bookListSorted,'DoubanBook.html');
大概就是这样了,解释一下每个函数/类的作用:
book类:存储每本书需要用到的信息,有title、author、豆瓣链接、图片链接、rate
makeHtml函数:根据传入的参数(blist书目列表以及生成html file位置)生成html
run函数:遍历250本书,进行正则表达式信息提取
先从run()
说起
首先一个count=0
记录遍历的书目序号,然后进入循环,url = 'http://book.douban.com/top250?start=%d'%count
生成当前需要遍历书目的url地址,使用page = urllib.urlopen(url).read()
保存这个页面的内容。可以print一下这个page的内容,就是html文件。
然后进行信息提取。以题目为例,从豆瓣网站中找到任意一本书的题目,
(可以在chrome中使用开发者工具,然后点开发者工具中左上角的小箭头,这样鼠标移到页面中的哪一个位置就可以自动在源代码中标记出来,移到题目处就可以看到源代码)
单独复制出来:
<a href="http://book.douban.com/subject/1084336/"
onclick="moreurl(this,{i:'0'})"
title="小王子">小王子</a>
多试几个就可以看到,每本书的题目在源代码的定义方式都是:
" title="题目">题目</a>
保留前面的"
只是为了缩小范围提高精确度,这样,我们就可以写出其正则表达式:
(?<=" title=").*?(?=")
简单分析一下,(?<=" title=")
表示前缀是" title="
,然后跟.*?
表示满足条件的尽量少的任意字符,然后(?=")
表示后缀为"
,连起来就是满足前缀和后缀要求的中间尽量少的字符集。相似的方法,我们可以找出其他信息的正则表达式表述。就是:
titlePat = re.compile(r'(?<=" title=").*?(?=")')
authorPat = re.compile(r'(?<=<p class="pl">).*?(?=/)')
urlPat = re.compile(r'(?<=<a href=").*?(?=" onclick=")')
imgPat = re.compile(r'(?<=<img src=").*?(?=" width="64" />)')
ratePat = re.compile(r'(?<="rating_nums">).*?(?=<)')
这样就可以将正则表达式编译,用XXXPat存储。编译好之后的使用直接调用re.search(arg1,arg2)
就可以,其中arg1是正则表达式,arg2是需要查找的页面。python中还有另外两种正则表达式查询的方法,下面简单介绍一下三种方法的不同之处:
re.search(pattern, page)
在全文件中查找需要的pattern,找到一个之后停止
re.match(pattern, page)
仅在文章开头处查找需要的pattern,即只有文章最开头的内容符合正则表达式规则才会被找出
re.findall(pattern, page)
在全文查找,返回一个列表,存储所有查找到的元素
为什么不使用findall呢?我最开始用的就是findall,但是不知道为什么有的时候能找全有的时候不能,即同样的代码同样的页面每次运行有的时候能找到25个有的时候只能找到24个。注意除了findall直接返回列表保存找到的信息之外,search和match都是需要使用group()
函数来进行查找的(关于正则表达式的分组请自行百度),举个例子,
pat = re.compile(r'sample')
mat = re.search(r'abcdefg')
mat2 = re.search(r'a simple sample')
执行之后,mat为None,mat2非None并且有成员函数,mat2.group()
值为sample
为判断是否执行成功,我讲这一部分单独写了一个函数:
def finder(page, pat):
content = re.search(pat,page)
if not content:
print "Failed in url"
exit(1)
return content.group()
这样如果没有找到,程序就会报错退出。到这里爬虫部分就全部完成了,每爬下来一本书就新建一个book类的对象,然后进行赋值,然后将这个对象添加到bookList列表中,就完成了250本书的爬取工作。
接下来是排序,排序直接实用sorted函数即可:
bookListSorted = sorted(bookList, key=lambda ele:ele.rate, reverse=1)
其中,key=lambda ele:ele.rate
表示按照每一个元素的rate成员排序,lambda用法请自行百度,reverse=1
表示逆序排列(即从大到小)
那OK贴一下此时除html生成之外的完整代码:
#!/bin/python2.7
# -*- coding:utf-8 -*-
import re
import urllib
class book:
title = ""
author = ""
url = ""
img = ""
rate = 0.0
def __init__(self, Title, Author, Url, Img, Rate):
self.title = Title
self.author = Author
self.url = Url
self.img = Img
self.rate = Rate
def content(self):
return "Title:%s\tAuthor:%s\tUrl:%s\tImg:%s\tRate:%s\n"\
%(self.title,self.author,self.url,self.img,self.rate)
def finder(page, pat):
content = re.search(pat,page)
if not content:
print "Failed in url"
exit(1)
return content.group()
def makeHtml(blist,path):
def run():
count = 0
run_times = 0;
while count < 250:
url = 'http://book.douban.com/top250?start=%d'%count
page = urllib.urlopen(url).read()
titlePat = re.compile(r'(?<=" title=").*?(?=")')
authorPat = re.compile(r'(?<=<p class="pl">).*?(?=/)')
urlPat = re.compile(r'(?<=<a href=").*?(?=" onclick=")')
imgPat = re.compile(r'(?<=<img src=").*?(?=" width="64" />)')
ratePat = re.compile(r'(?<="rating_nums">).*?(?=<)')
title = finder(page, titlePat)
author = finder(page, authorPat)
url = finder(page, urlPat)
img = finder(page, imgPat)
rate = finder(page, ratePat)
#print "%s %s %s %s %s\n"%(len(title),len(author),len(url),len(img),len(rate))
newBook = book(title, author, url, img, rate)
bookList.append(newBook)
print "%s: "%count
print newBook.content(),
count += 1
bookList = []
run()
bookListSorted = sorted(bookList, key=lambda ele:ele.rate, reverse=1)
makeHtml(bookListSorted,'DoubanBook.html');
fp = open('123','w+') //Store in file, debug
count = 1
for i in bookListSorted:
fp.write("%s: "%count)
fp.write(i.content())
count+=1
#print rateList
有一点需要注意,因为有中文所以需要python声明一下编码方式,在文件开头需要加上# -*- coding:utf-8 -*-
注意:编码方式只能在第一或者第二行声明
下面介绍html的生成,非常简单,先用一个变量Start保存html头信息(包含编码信息、样式表、页面标题等等),实际上这是一个常量,不会被改变,太长省略掉。然后使用一个变量content
保存每一本书显示用的代码,每本书的这一部分都不一样,所以需要注意一下,
content = '''<table width=%"100%">
<tr class="item">
<td width="100" valign="top">
<a class="nbg" href="'''+'%s'%url+'''"
onclick="moreurl(this,{i:'0'})"
>
<img src="'''+'%s'%img+'''" width="64" />
</a>
</td>
<td valign="top">
<div class="pl2">
<a href="'''+'%s'%url+'''" onclick="moreurl(this,{i:'0'})" title="'''+'%s'%title+'''">
'''+'No.%s %s'%(count,title)+'''
</a>
<br/>
</div>
<p class="pl">'''+'%s'%author+'''</p>
<div class="star clearfix">
<span class="allstar45"></span>
<span class="rating_nums">'''+'%s'%rate+'''</span>
</div>
</td>
</tr>
</table>
<p class='ul'></p>
就不分析这个了,哪里需要填充为什么内容代码表述的比较清楚,有一个地方需要注意就是因为需要多行显示并且字符串中有单双引号,所以我使用了三引号来括起来字符串。最后再加上一个End
存储文件尾就OK了。
这样整体的makeHtml部分:
def makeHtml(blist,path):
fp = open(path,'w+')
Start = '''<!DOCTYPE html>
<html lang="zh-cmn-Hans" class=" book-new-nav">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>豆瓣图书Top250排序 | Alanzjl</title>
<script>!function(f){var h=function(o,n,m){var k=new Date(),j,l;n=n||30;m=m||"/";k.setTime(k.getTime()+(n*24*60*60*1000));j="; expires="+k.toGMTString();for(l in o){f.cookie=l+"="+o[l]+j+"; path="+m}},d=function(m){var l=m+"=",o,n,j,k=f.cookie.split(";");for(n=0,j=k.length;n<j;n++){o=k[n].replace(/^\s+|\s+$/g,"");if(o.indexOf(l)==0){return o.substring(l.length,o.length).replace(/\"/g,"")}}return null},e=f.write,b={"douban.com":1,"douban.fm":1,"google.com":1,"google.cn":1,"googleapis.com":1,"gmaptiles.co.kr":1,"gstatic.com":1,"gstatic.cn":1,"google-analytics.com":1,"googleadservices.com":1},a=function(l,k){var j=new Image();j.onload=function(){};j.src="http://www.douban.com/j/except_report?kind=ra022&reason="+encodeURIComponent(l)+"&environment="+encodeURIComponent(k)},i=function(k){try{e.call(f,k)}catch(j){e(k)}},c=/<script.*?src\=["']?([^"'\s>]+)/ig,g=/http:\/\/(.+?)\.([^\/]+).+/i;f.writeln=f.write=function(k){var j=c.exec(k),l;if(!j){i(k);return}l=g.exec(j[1]);if(!l){i(k);return}if(b[l[2]]){i(k);return}if(d("hj")==="tqs"){return}a(j[1],location.href);h({hj:"tqs"},1);setTimeout(function(){location.replace(location.href)},50)}}(document);</script>
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="Sun, 6 Mar 2005 01:00:00 GMT">
<script>var _head_start = new Date();</script>
<link href="http://img3.douban.com/f/book/9378a88cec03259a21648c0c3b55eaa6fa577d45/css/book/master.css" rel="stylesheet" type="text/css">
<style type="text/css"></style>
<script src="http://img3.douban.com/f/book/e9d9543ebc06f2964039a2e94898f84ce77fc070/js/book/lib/jquery/jquery.js"></script>
<script src="http://img3.douban.com/f/book/36c6bb0e275c61fbb7b3294e6bccb7a2ba992522/js/book/master.js"></script>
<script> </script>
<link rel="stylesheet" href="http://img3.douban.com/misc/mixed_static/752a6657ef371706.css">
</head>
<body>
<script>var _body_start = new Date();</script>
<div id="db-nav-book" class="nav">
<div class="nav-wrap">
<div class="nav-primary">
<div class="nav-logo">
<a href="http://alanzjl.com">豆瓣读书</a>
</div>
</div>
</div>
</div>
<div id="wrapper">
<div id="content">
<h1>豆瓣读书Top250逆序 | Alanzjl</h1>
<div class="grid-16-8 clearfix">
<div class="article">
<div class="indent">
<p class="ulfirst"></p>
'''
End = '''<div id="footer">
<span id="icp" class="fleft gray-link">
© 2015-2016 www.alanzjl.com Contact with me at [email protected]
</span>
</div>
</body>
</html>
'''
fp.write(Start)
count = 1;
for i in blist: #开始添加书信息
url = i.url
title = i.title
img = i.img
author = i.author
rate = i.rate
content = '''<table width=%"100%"> #生成每本书的代码
<tr class="item">
<td width="100" valign="top">
<a class="nbg" href="'''+'%s'%url+'''"
onclick="moreurl(this,{i:'0'})"
>
<img src="'''+'%s'%img+'''" width="64" />
</a>
</td>
<td valign="top">
<div class="pl2">
<a href="'''+'%s'%url+'''" onclick="moreurl(this,{i:'0'})" title="'''+'%s'%title+'''">
'''+'No.%s %s'%(count,title)+'''
</a>
<br/>
</div>
<p class="pl">'''+'%s'%author+'''</p>
<div class="star clearfix">
<span class="allstar45"></span>
<span class="rating_nums">'''+'%s'%rate+'''</span>
</div>
</td>
</tr>
</table>
<p class='ul'></p>
'''
fp.write(content)
fp.write('\n\n\n')
count+=1
fp.write(End) #写文件尾
OK大功告成!所有代码比较长,就不单独再粘上来了,需要的童鞋可以去我的Github repo下载:
Github | DoubanBookSort Alanzjl
Github | DoubanMovieSort Alanzjl