用Python爬蟲爬取豆瓣電影、讀書Top250並排序
更新:已更新豆瓣電影Top250的指令碼及網站
概述
經常用豆瓣讀書的童鞋應該知道,豆瓣Top250用的是綜合排序,除使用者評分之外還考慮了很多比如是否暢銷、點選量等等,這也就導致了一些近年來評分不高的暢銷書在這個排行榜上高高在上遠比一些經典名著排名還高,於是在這裡打算重新給Top250純按照使用者打分排一下序。思路就是:先用爬蟲爬下來豆瓣Top250的所有書目,然後存到本地之後進行排序,最後再做一個網站把排完序的書單重新掛上去。
先掛上成品網站:
豆瓣讀書Top250逆序 | Alanzjl
豆瓣電影Top250逆序 | Alanzjl
這個網站完全按照豆瓣原網站製作,html程式碼都是直接扒豆瓣的。或者作為我的個人網站內嵌使用:
豆瓣電影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