Python一鍵爬取你所關心的書籍資訊

xulinlucas發表於2019-03-05

作者:梅破知春近,準資料分析師

個人簡書專欄:放翁lcf

前言 

平時看到的豆瓣爬蟲基本都是爬豆瓣top100電影、某電影熱評、top100圖書、熱門圖書等,最近遇到的一個需求是根據一堆書名的列表(或者書名Excel檔案)爬取對應的書目資訊,也就是豆瓣圖書頁面上的出版社、出版時間、ISBN、定價、評分、評分人數等資訊,再整合到pandas裡進行處理,最後可以進行資料分析。

需求來源 

最近整理書目的時候需要根據幾百本書的書名整理出對應的出版社、出版時間、ISBN、評分等屬性,書單Excel如下圖1中的表。批次處理肯定是用爬蟲啦,查了一下沒有發現相似的文章,並且自己操作時也遇到了比較有趣的問題,於是把自己的操作思路和過程整理成本文。


Python一鍵爬取你所關心的書籍資訊

圖1,書單資料部分截圖

爬取過程 

頁面分析

首先分析豆瓣圖書首頁:book.douban.com,直接搜尋書名時可以看到搜尋引數是寫在url上的,於是想著直接用{0}&cat=1001'.format('書名'),直接改search_text引數,在這個頁面按F12調出控制檯,失望的是這個url返回的html是不含資料的,如圖2。關鍵是找了一段時間還是沒找到非同步返回的資料json(如果有人找到了豆瓣subject_search?search_text={0}&cat=1001這類頁面的書籍資料的位置歡迎告訴我呀),這時候考慮用Selenium或者查其他介面。


Python一鍵爬取你所關心的書籍資訊

圖2,基於搜尋url的html截圖

json分析

注意到豆瓣圖書的搜尋頁面有一個搜尋提示,於是在控制檯查Network發現搜尋提示返回的直接是一個json,例如查“未來簡史”,結果如下:


Python一鍵爬取你所關心的書籍資訊

圖3,未來簡史搜尋提示

返回json可以用的屬性有:title:書名、url:對應書的豆瓣頁面、pic:書封面圖資源位置等。如果上面的輸入我們們只有書名,就根據書名和返回的json對應,如果有作者、出版年份等屬性,就可以更好的核對是否是我們要找的書,為了簡化,下面只用了返回json資料的第1條。

基本程式碼

根據返回的url就可以從這個url去定位我們需要爬的資訊。走通了就可以正式寫程式碼了,以下程式碼採用jupyter notebook的組織方式,也就是切分得比較細。先引入所需庫:


import json
import requests
import pandas as pd
from lxml import etree

讀取書名Excel資料,只用了"書名"列,先不考慮其他列


bsdf=pd.read_excel('booklistfortest.xlsx') 
blst=list(bsdf['書名'])  #書名列表
#bsdf.head(3)

對書名列表進行迴圈,得到的屬性用字典裝著,每本書的屬性是一個字典,用列表裝各個字典

透過requests.get('{0}'.format(bn))獲取搜尋建議返回的json資料,其中bn是書名字串。
爬蟲的一般解析是用BeautifulSoup或xpath,我更喜歡用xpath,因此下面的程式碼主要基於xpath解析文字。
以評分為例,滑鼠點選評分部分,然後按Ctrl+Shift+I,或者右鍵點選檢查元素,反正就是定位到評分對應的HTML上,定位到評分的程式碼部分後,右鍵,選擇Copy->Copy XPath,例如對於評分來說有:
//*[@id="interest_sectl"]/div/div[2]/strong


Python一鍵爬取你所關心的書籍資訊

圖4,複製評分的xpath

透過con.xpath('//*[@id="interest_sectl"]/div/div[2]/strong/text()')就可以得到評分資料,返回的是列表,一般就是第0個值。同樣,其他地方也是這樣,而作者、出版社那幾個屬性是結構比較散的,需要特殊處理。

Python一鍵爬取你所關心的書籍資訊

圖5,自由度較大的書目資訊部分

透過//*[@id="info"]/span[2]可以確定 出版社 這個屬性,但是屬性的值,具體是哪個出版社不能確定,這些文字是在info這個節點上的。對於這種長度不定的一個html區域,不能寫死xpath解析式,需要理清其HTML樹結構,建立info的樹結構。透過分析幾個具體的頁面的info部分,建立樹結構如下:

Python一鍵爬取你所關心的書籍資訊

圖6,info部分的HTML樹

需要得到的是{'出版社’:'中信出版集團'}這樣的資料,透過HTML樹結構可以看到的特徵是鍵(如出版社)在span裡,值可能在text裡,也可能封裝在span裡的子元素裡,反正每個鍵值對之後都有一個br去切分。考慮這些情況寫出的程式碼如下:

def getBookInfo(binfo,cc):
   i=0
   rss={}
   k=''
   v=''
   f=0
   clw=[]
   for c in cc:
       if '\n' in c:
           if '\xa0' in c:
               clw.append(c)
       else:
           clw.append(c)
   
   for m in binfo[0]:
       if m.tag=='span':
           mlst=m.getchildren()
           if len(mlst)==0:
               k=m.text.replace(':','')
               if '\xa0' in clw[i]:
                   f=1#需要m.tag=='a'下的值
               else:
                   v=clw[i].replace('\n','').replace(' ','')
               i+=1
           elif len(mlst)>0:#下面有子span 一種判斷是m.attrib=={} 不夠精確
               for n in mlst:
                   if n.tag=='span':
                       k=n.text.replace('\n','').replace(' ','') #不至於下面還有span,懶得用遞迴了
                   elif n.tag=='a':
                       v=n.text.replace('\n','').replace(' ','')
       
       elif m.tag=='a':
           if f==1: #是否可以不用這個if
               v=m.text.replace('\n','').replace(' ','')
               f=0
       elif m.tag=='br':
           if k=='':
               print(i,'err')
           else:
               rss[k]=v
       else:
           print(m.tag,i)
   return rss

為了在大迴圈裡好呼叫,上面的部分封裝成函式,呼叫getBookInfo()返回的是一個字典,要整合到已有的字典裡。涉及字典的組合,查了一下可以用d=dict(d,**dw),其中d是舊字典,dw是要加到d裡的新字典,更簡便的方式是用d.update(dw)函式,下面的程式碼就是用的update的。

主迴圈程式碼:

rlst=[]
for bn in blst:
   res={}
   r=requests.get('{0}'.format(bn))
   rj=json.loads(r.text)
   #對rj進行一下驗證和篩選
   html=requests.get(rj[0]['url']) #之後再考慮多個返回值的驗證
   con = etree.HTML(html.text)
   bname=con.xpath('//*[@id="wrapper"]/h1/span/text()')[0] #和bn比較
   res['bname_sq']=bn
   res['bname']=bname
   res['dbid']=rj[0]['id'] #不需要存url,存id就夠了
   #這部分取到info就夠了,之後再用高階方法去匹配需要的元素,目前對應不對
   binfo=con.xpath('//*[@id="info"]')
   cc=con.xpath('//*[@id="info"]/text()')
   res.update(getBookInfo(binfo,cc))  #呼叫上面的函式處理binfo
   bmark=con.xpath('//*[@id="interest_sectl"]/div/div[2]/strong/text()')[0]
   if bmark=='  ':
       bits=con.xpath('//*[@id="interest_sectl"]/div/div[2]/div/div[2]/span/a/text()')[0]
       if bits=='評價人數不足':
           res['評分']=''
           res['評價人數']='評價人數不足'
       else:
           res['評分']=''
           res['評價人數']=''
   else:
       res['評分']=bmark.replace(' ','')
       bmnum=con.xpath('//*[@id="interest_sectl"]/div/div[2]/div/div[2]/span/a/span/text()')[0]
       res['評價人數']=bmnum
   rlst.append(res)
得到的資料可以進行一定的標準化然後進行分析再輸出。上面得到的列表rlst=[{'書名':'a','出版社':'b'},{'','','':''}],可以直接變成dataframe,

outdf=pd.DataFrame(rlst) #轉dataframe
outdf.to_excel('out_douban_binfo.xlsx',index=False) #輸出資料

Python一鍵爬取你所關心的書籍資訊

圖7,爬到的資料概覽

基礎資料統計分析

我們開始時讀入的bsdf有書名、作者、閱讀時間等屬性,因為爬下來的資料可能會有缺失值,將兩個表合併起來進行分析。分析的維度有書名、作者、閱讀時間、出版社、頁數等。首先是用merge整合兩表然後看一些基本的統計量。

bdf=bsdf.merge(outdf,on='書名',how='left') # 資料合併
# 基本統計值
print('一共有{0}本書,{1}個作者,{2}個出版社;'.format(len(bdf),len(set(list(bdf['作
者'
]))),len(set(list(bdf['出版社'])))))

輸出是一共有421本書,309個作者,97個出版社;
我們就來看看前幾位的作者和出版社,透過
bdf['作者'].value_counts().head(7)可以輸出前7位書單裡出現最多的作者,出版社同理,結果如下:

Python一鍵爬取你所關心的書籍資訊

圖8,出版社和作者統計

從作者出現次數來看,前6位都是小說型別的書,可以看一下吳軍的是哪些書:

bdf.loc[bdf['作者']=='吳軍',['書名','閱讀時間','閱讀情況','出版社']]
#output:
'''
                 書名       閱讀時間 閱讀情況      出版社
103             數學之美 2016-10-20   P5  人民郵電出版社
233             智慧時代 2017-06-22   P4    中信出版社
237             矽谷之謎 2017-07-01   P4  人民郵電出版社
383  見識--商業的本質和人生的智慧 2018-10-21   P4    中信出版社
'''

對每月閱讀數量進行統計:

import matplotlib.pyplot as plt #繪圖用到matplotlib庫
%matplotlib inline

bdf['閱讀年月']=bdf['閱讀時間'].apply(lambda x : x.strftime('%Y-%m'))
read_date=bdf['閱讀年月'].value_counts() #每月閱讀量,按月計數
read_date=pd.DataFrame(read_date,columns=['閱讀年月']) #從Series變為DataFrame
read_date=read_date.sort_index()

plt.figure(figsize=(15,5))
plt.xticks(rotation=90)#設定時間標籤顯示格式
plt.plot(read_date) #因為jupyter裡寫了 %matplotlib inline 不用寫 plt.show()

Python一鍵爬取你所關心的書籍資訊

圖9,每月閱讀數量_時間軸折線圖.png

好奇不同年份每個月是否有一定規律呢。要統計這個比較方便的就是用資料透視表了,pandas裡的pivot_table出場。

import numpy as np
bdf['閱讀年']=bdf['閱讀時間'].apply(lambda x : x.strftime('%Y'))
bdf['閱讀月']=bdf['閱讀時間'].apply(lambda x : x.strftime('%m')) #這裡也可以用.month .year
r_dd=bdf.loc[:,['閱讀年','閱讀月']]
r_dd['val']=1 #用以初始化
r_dd=pd.pivot_table(r_dd,values='val',index=['閱讀月'],columns=['閱讀年'],aggfunc=np.sum).fillna(value=0)
#這部分程式碼的細節可以看本人github裡jupyter notebook檔案的輸出
r_dd=r_dd.loc[:,['2016','2017','2018']] #因為其他年份月份不全,只取這3年來看
plt.figure()
r_dd.plot(xticks=range(1,13),figsize=(12,5))

Python一鍵爬取你所關心的書籍資訊

圖10,每月閱讀數量_按年統計

可以看到這3年在2月和7月閱讀普遍數量更多,在7月份之前每月閱讀量是逐年上漲的,而從8月到12月則是遞減的規律,2016年11月閱讀的書籍最多,達到40本以上。
評分是一個數值型變數,用箱線圖[圖片上傳中...(圖12_書單內資料相關的書籍.png-5352ab-1551272966564-0)]
展現其特徵:

b_rank=pd.DataFrame(bdf['評分']) #評分分佈(箱線圖)
b_rank.boxplot()

#另,評分 top 10
#bdf.sort_values(by='評分',ascending=False).head(10).loc[:,['書名','作者','閱讀時間'
,'閱讀情況','出版社','評分']]

Python一鍵爬取你所關心的書籍資訊

圖11,書籍評分箱線圖

從箱線圖來看,書單有評分的書籍的豆瓣平均分在7.8左右,75%的書評分在7.2以上,也有一些書是在4分一下的。

Python一鍵爬取你所關心的書籍資訊

圖12,書單內資料相關的書籍

書單裡書名直接包含資料的書有37本,資料科學相關的書籍數量應該大於這個值。

可以進一步分析的有:

  • 看的書的書名詞雲、作者的詞雲

  • 出版社省份

  • 把字數統計和爬下來的頁數進行擬合,把字數和頁數一起處理

  • 把含有多國貨幣的價格屬性按匯率換算後看價格的分佈

資料輸出

上面透過一個具體的需求實踐了能解決問題的爬蟲,豆瓣還是比較容易爬的,上面解析書目資訊的做法還是很有意義的,當然我是用xpath做的,如果用BeautifulSoup又會是另一種實現方式,但分析問題->建立HTML樹的過程是通用的。上面的程式碼還是比較簡略的,沒有考慮過多的驗證和異常處理,有任何意見或建議歡迎交流。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555699/viewspace-2637594/,如需轉載,請註明出處,否則將追究法律責任。

相關文章