爬取豆瓣TOP250圖書榜的出版社分布(一)-urllib2
0. 需求
現(xiàn)在準(zhǔn)備爬取豆瓣上的圖書TOP250然后看哪個出版社的圖書上榜最多。將使用兩種辦法分別實現(xiàn),urllib2與scrapy框架。爬取的內(nèi)容包括書的名字,書的出版社,書的評分,書的鏈接。
1. 使用urllib2實現(xiàn)
1.1 分析網(wǎng)站鏈接
http://book.douban.com/top250?start=0這個是豆瓣網(wǎng)站上圖書TOP250首頁的鏈接,很容易就會發(fā)現(xiàn)規(guī)律,就是?start=0,表示的是這一個頁面的開始是從第0本書開始的,那么第二頁就是從start=25開始的,了解了這一點,首先就可以把要爬取網(wǎng)站的url搞定了。
def __init__(self):
self.addr = 'http://book.douban.com/top250'#?start=0
self.colums=1
1.2 獲取網(wǎng)頁源代碼
為了防止豆瓣網(wǎng)檢測出是機(jī)器爬蟲訪問網(wǎng)站,從而封掉自己的ip,因此在發(fā)送request請求的時候,附帶上模擬瀏覽器訪問的headers。
headers通過瀏覽器訪問豆瓣網(wǎng),然后右鍵點擊審查元素,就可以查詢出用瀏覽器訪問的時候產(chǎn)生的headers和cookie。
下面的代碼中cookie有點長。
獲取相應(yīng)的網(wǎng)站的源代碼步驟是:
- 構(gòu)建url
- 構(gòu)建request(本次要加上headers)
- 發(fā)送請求
- 讀取返回的源代碼數(shù)據(jù)
獲取網(wǎng)頁源代碼數(shù)據(jù)的代碼如下:
def getPage(self,page):
'''獲取每個頁面的源代碼,豆瓣網(wǎng)站編碼是utf-8'''
start=(page-1)*25
headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8',
'Connection': 'keep-alive',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36',
'Host':'book.douban.com',
'Referer':'http://book.douban.com/top250?start=0',
'Cookie':'bid="uJg2YuOLKDc"; ll="118371"; gr_user_id=f349a8b5-04e7-4132-b6c3-659f79f124fa; _pk_ref.100001.3ac3=%5B%22%22%2C%22%22%2C1454551498%2C%22http%3A%2F%2Fwww.douban.com%2F%22%5D; viewed="1770782_21323941_19952400_10432347_10436668_3155710_4830438_1152111_1015813"; _pk_id.100001.3ac3=c7eb454cb471541c.1439423804.9.1454552955.1448876420.; __utma=30149280.2012237960.1437482698.1451533495.1454551966.15; __utmc=30149280; __utmz=30149280.1451533495.14.11.utmcsr=baidu|utmccn=(organic)|utmcmd=organic; __utma=81379588.2012449174.1439423804.1448876271.1454551966.9; __utmc=81379588; __utmz=81379588.1448876271.8.6.utmcsr=baidu|utmccn=(organic)|utmcmd=organic'
}
url=self.addr+'?start='+str(start)
try:
request=urllib2.Request(url,headers=headers)
response=urllib2.urlopen(request)
result=response.read()
except urllib2.URLError,e:
print '無法連接,原因:',e.reason
else:
return result
在這里有一個需要注意的tip就是,最好不要直接return response.read()這樣有可能導(dǎo)致返回不了完整的數(shù)據(jù)。
1.3 利用正則表達(dá)式匹配需要的信息
首先分析豆瓣網(wǎng)頁源代碼,找到圖書信息在HTML代碼中相應(yīng)的位置,建立合理的正則表達(dá)式。
然后利用上一小節(jié)getPage()方法返回的網(wǎng)頁源代碼信息進(jìn)行匹配。
利用re模塊里面的findall()函數(shù)進(jìn)行匹配,將獲取到整個頁面的25本圖書的信息results,并且返回的是一個列表,其中每一本書在這個列表中也是以列表存在的,因此迭代返回的列表,獲取對應(yīng)的信息。
[日] 東野圭吾 / 劉子倩 / 南海出版公司 / 2008-9 / 28.00
其中出版社這一條,可以通過觀察發(fā)現(xiàn),出版社都在網(wǎng)站相應(yīng)字段的倒數(shù)第三個,因此解析出版社數(shù)據(jù)的時候利用resultlt=result[2].split('/')將字符串分裂開,然后使用resultlt[-3]即可提取出出版社的信息。
整個處理方法代碼如下:
def getContent(self,page):
'''通過獲得的源代碼將所需要的圖書信息匹配出來'''
response=self.getPage(page)
#建立正則表達(dá)式
pattern=re.compile('<div class="pl2".*?<a href="(.*?)".*?title="(.*?)".*?<p class="pl">(.*?)</p>.*?<span class="rating_nums">(.*?)</span>',re.S)
results=re.findall(pattern,response)
contents=[]
#1是鏈接2是名字3是作者與出版社信息4是得分
for result in results:
resultlt=result[2].split('/')
contents.append([result[1],result[0],resultlt[-3].strip(),result[3]])
return contents
#只是直接打印列表的話才不會顯示正常字符,如果直接打印就會打印出中文字符
1.4 將數(shù)據(jù)寫入文件
將獲取到的數(shù)據(jù)按照對應(yīng)字段寫入txt文本文檔還是相當(dāng)容易的,主要的問題是字符串的編碼問題,豆瓣網(wǎng)采用了utf-8編碼,因此在讀取網(wǎng)頁源代碼以及寫入文件的時候并不需要考慮編碼問題。
直接將要寫入文件的字符串格式建立好就可以直接寫入了。
寫入函數(shù)如下所示:
def writeFile(self,contents):
'''將數(shù)據(jù)寫入到文本文件中'''
file=open('douban.txt','a')
for content in contents:
line='\n'+str(self.colums)
info='\nname:'+content[0]+'\naddress:'+content[1]+'\npress:'+content[2]+'\nrate:'+content[3]+'\n'
file.write(line)
file.write(info)
self.colums+=1
file.close()
寫入后的文本效果如下圖:

1.5 開始程序
在這里將控制程序打開豆瓣圖書TOP250的10個頁面,然后分別進(jìn)行數(shù)據(jù)提取。代碼如下:
def start(self):
'''開始程序,首先讀取content然后將數(shù)據(jù)保存到shelve模塊中'''
i=1
while i<=10:
print '正在讀取并存儲第',str(i),'頁內(nèi)容'
contents=self.getContent(i)
self.writeData(contents)
self.writeFile(contents)
i+=1
time.sleep(10)
這里需要注意的是在打開頁面之間最好留有間隔,time.sleep(10),間隔10秒,否則豆瓣網(wǎng)站會禁止訪問。
1.6 數(shù)據(jù)分析
#######1.6.1 從文本文件提取出版社信息
下面進(jìn)行的內(nèi)容就是將存入文件的TOP250的圖書信息提取出來,然后將每本書的出版社匯總分析,看每個出版社出現(xiàn)了多少次,然后做成直觀的統(tǒng)計圖表進(jìn)行顯示。
做到這一切的核心思想是:將文本文件的文本提取出來(使用readlines()),然后使用正則表達(dá)式提取press的信息,創(chuàng)建一個字典,出版社名為鍵,出版社出現(xiàn)的次數(shù)作為值。
使用字典的has_key方法判斷是否有該出版社鍵值,沒有則將該出版社作為鍵加入字典,設(shè)置默認(rèn)值1。如果有的話,將該出版社對應(yīng)的值加一。最后獲得整個的出版社次數(shù)的字典。
另外程序還使用了shelve作為對出版社數(shù)據(jù)的簡單的存儲,shelve的使用非常方便,只需要將其當(dāng)成普通的字典,這一點剛好符合我們的提取信息的方案,將其存入shelve后,在下一步的數(shù)據(jù)可視化中就可以方便的將數(shù)據(jù)提取出來。
代碼如下:
#coding=utf-8
'''這個程序是將存入文件的豆瓣圖書書單的出版社提取出來,存入字典
使用setdefault()方法,如果有相應(yīng)的鍵則返回鍵對應(yīng)的值
否則返回默認(rèn)值1'''
import re
import shelve
class ExtractPress():
'''首先定義一個存放出版社和數(shù)量的字典,然后是輸入,以及匹配的內(nèi)容'''
def __init__(self):
self.pressDic={}
self.pattern=re.compile('press:(.*?).rate',re.S)
self.database=shelve.open('press.dat')
def getAllValue(self):
text=open('douban.txt').readlines()
strText=''.join(text)
results=re.findall(self.pattern,strText)
for result in results:
if self.pressDic.has_key(result):
number=int(self.pressDic[result])
number+=1
self.pressDic[result]=number
else:
self.pressDic.setdefault(result,1)
def printDic(self):
for press in self.pressDic:
print press+':'+str(self.pressDic[press])
self.database[press]=self.pressDic[press]
self.database.close()
if __name__=='__main__':
example=ExtractPress()
example.getAllValue()
example.printDic()
#######1.6.2 對提取的信息進(jìn)行可視化處理
在這里我將使用matplotlib進(jìn)行數(shù)據(jù)分析,有關(guān)matplotlib的環(huán)境配置,請移步利用matplotlib進(jìn)行數(shù)據(jù)分析(一)一文,其中包含了matplotlib以及相關(guān)依賴的安裝配置方法。
這里直接上代碼,然后對代碼進(jìn)行分析,關(guān)于數(shù)據(jù)可視化編程的知識,參考Python數(shù)據(jù)可視化編程實戰(zhàn) [愛爾蘭]Igon Milovanovic一書。
#coding=utf-8
import shelve
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
class dataAnalyseGraph():
'''這個類將上面的出版社的數(shù)據(jù)信息提取出來,做出一個可視化的圖表'''
def __init__(self):
self.database=shelve.open('press.dat')
self.keys=[]
self.data=[]
self.font=FontProperties(fname=r'C:\WINDOWS\Fonts\STSONG.TTF',size=10)
def getData(self):
for key in self.database.iterkeys():
if self.database[key]<=1:
continue
self.keys.append(key.decode('utf-8'))
self.data.append(self.database[key])
print self.data
for n in self.keys:
print n
def makeGraph(self):
y_pos=range(len(self.keys))
# colors=np.random.rand(len(self.keys))
#水平柱狀圖
plt.barh(y_pos,self.data,align='center',alpha=0.4)
#刻度
plt.yticks(y_pos,self.keys,fontproperties=self.font)
#繪圖
for datas,y_pos in zip(self.data,y_pos):
plt.text(datas,y_pos,datas,horizontalalignment='center',
verticalalignment='center',weight='bold')
#set y limit
plt.ylim(+32,-1.0)
plt.title(u'豆瓣圖書TOP250出版社統(tǒng)計',fontproperties=self.font)
plt.ylabel(u'出版社',fontproperties=self.font)
plt.subplots_adjust(bottom=0.15)
plt.xlabel(u'出版社出現(xiàn)次數(shù)',fontproperties=self.font)
plt.show()
plt.savefig("douban_book_press.png")
if __name__=='__main__':
graph=dataAnalyseGraph()
graph.getData()
graph.makeGraph()
建立了一個專門作為數(shù)據(jù)可視化的類,在該類的構(gòu)造方法中,打開了shelve文件(上一小節(jié)中存儲的出版社數(shù)據(jù)),初始化了兩個列表,同時打開了一個宋體的文字文件作為圖表顯示文字。
getData方法中將文件的數(shù)據(jù)提取出來,這里由于出版社過多,因此只提取了出現(xiàn)1次以上的出版社,將出版社和出現(xiàn)次數(shù)按順序存放至建立好的keys和data列表中。
makeGraph是數(shù)據(jù)可視化的核心部分。barh是繪制水平柱狀圖的函數(shù),接收y_pos為豎直坐標(biāo),而self.data作為柱狀圖的值,align指定了矩形條的居中設(shè)置,alpha指定了透明度。yticks是豎直方向的刻度設(shè)置,y_pos指定了刻度,而self.keys指定了刻度的顯示,這兩個是一一對應(yīng)的。然后將每一組data與y_pos的柱指定它的文本,坐標(biāo)就是datas(出版社出現(xiàn)次數(shù),橫坐標(biāo)的值),y_pos是y坐標(biāo),datas是顯示的數(shù)字。然后ylim設(shè)置y的上下限。設(shè)置標(biāo)題,設(shè)置y軸的標(biāo)簽,x的標(biāo)簽,然后show顯示該圖像。
最后的可視化效果如圖所示:
