引入
根據筆者以往的爬蟲經驗,大部分的爬蟲是在靜態網頁上完成的,爬蟲所要做的只不過是提交請求,然後分析返回的頁面即可。當然,api本質上也可以作爲靜態頁面來處理。這意味着只要掌握requests就可以完成60%-80%的爬蟲任務。
這是一個很驚人的佔比,這裏解釋一下,靜態頁面可能聽起來很low,但是有着以加載速度更快、易於維護爲核心的一系列優勢,尤其是引入了ajax之後,實現了動態加載,通過更加頻繁的前後端交互,使得用戶的使用更加絲滑流暢。
但是總有一些網站是靜態爬蟲無法應付的。它們就是與js耦合度較高的,需要js進行渲染的頁面,與上文所述的情況(前端只接收數據,而不用對數據進行計算層面的處理)不同,這類網站將部分的計算工作交託給前端,犧牲部分的用戶體驗來實現緩解服務器壓力等一系列目的。
這就是剩下的20%了。如何處理這些刺頭呢?這就引出了本文的主角–splash。
關於splash
Splash是一個針對js的渲染服務。它內置了一個瀏覽器和http接口。基於Python3和Twisted引擎。所以可以異步處理任務。
關於splash,國內目前的大部分博客教程都停留於對官方文檔的翻譯,所以還是推薦有能力的直接看文檔,畢竟還有一個時效性。
安裝 && 運行
docker pull scrapinghub/splash
docker run -p 8050:8050 -p 5023:5023 scrapinghub/splash
一個簡單的splash應用
抓取今日頭條,對比渲染和沒有渲染的效果
import requests
from lxml import etree
url = 'http://localhost:8050/render.html?url=https://www.toutiao.com&timeout=30&wait=0.5'
# url = 'https://www.toutiao.com'
response = requests.get(url)
tree = etree.HTML(response.text)
article_titles = tree.xpath('//div[@class="title-box"]/a/text()')
print(article_titles)
[ '北京2020年初雪已進城!您那兒下起來了嗎?', '論萌娃寫作業時,求生欲有多強:爸爸我給你鼓掌','蘇萊曼尼之死,全世界到底在怕什麼?', '觀景平臺,“零距離”看飛機', '浙江一企業保險箱被撬,120萬現金僅被偷走27萬!小偷:當時想起一句“名言”……', '劃重點2020雙閏年 網友:鼠年要多上一個月的班', '若美伊全面開戰,中國將再獲20年發展機遇期?']
總結
如果沒有渲染,那麼得到的結果就是一個空的數組,只有進行了js渲染才能得到我們想要的結果。
開始玩耍吧~
起因
前幾天看電視劇時一時興起(好吧,這就是老年人),想分析一下近年來的中國電視劇發展趨勢,加上正好某鯊想要學爬蟲,於是就重操舊業,開啓了一個新坑。
首先,從哪裏獲得數據呢?
將目標鎖定到了豆瓣(好吧,從某種層面上來說我也算是豆瓣的老用戶了,經常因爲使用爬蟲被封號的那種(笑))。
具體點說,是豆瓣排行榜,在進行了網頁分析之後,發現所有的數據都是通過api回傳的,這感情好,直接上requests莽一波就完事了。
問題出現
爬完之後發現,豆瓣排行榜只放出了前500條數據,500條能幹啥哦???
嘗試解決
重新尋找,發現豆瓣的搜索功能可以一試。於是搜索關鍵詞“1990+電視劇”,果然,1990年的電視劇就都出來了。但是,分析了半天網頁,沒有發現什麼api?於是開始進行第二輪的地毯式分析,果然,在主頁面的html文件裏發現了一條又臭又長的數據“window.__DATA__”,裏面存放了一大堆的詭異的字符串,這。。。。!?忽然想起來前幾天看的密碼學,不就是這個鯊雕樣子嗎,那麼很大概率就是豆瓣對自己的數據進行了加密!
開始排查
使用url過濾,配合二分法截斷url,再用js斷點調試,最後在bundle.js文件裏發現瞭解密過程。
這時有兩條路可以選擇。
- 解密
- 不解密
於是又刷新了一下網頁,“window.__DATA__”存放的數據變了。。。變了。。。好吧,竟然還採用了動態密鑰,我解密個錘子哦?果斷選擇第二條路。
解決方案
既然你用js渲染解密,那我就等你渲染完了再爬唄~~~
我是示例
數據獲取
import requests
from urllib.parse import quote
import json
import time
import pandas as pd
# 初始化Dataframe
df = pd.DataFrame()
# 記錄序號
index = 0
def get_soup(url_raw):
'''
@description: 獲取指定url的數據並將其解析爲soup
@param {type}
url_raw {string}
@return: BeautifulSoup的解析結果
'''
try:
## lua腳本
lua = '''
function main(splash, args)
assert(splash:go("'''+url_raw+'''"))
return {
html = splash:html(),
png = splash:png(),
har = splash:har(),
}
end
'''
url = 'http://localhost:8050/execute?lua_source=' + quote(lua)
response = requests.get(url)
js = json.loads(response.text)
soup = BeautifulSoup(js['html'])
return soup
except Exception as e:
print(e)
time.sleep(10)
def html_parser(soup,year):
'''
@description: 解析網頁,提取結果
@param {type}
soup {BeautifulSoup} 解析完成的soup
year {int} 第幾年
@return:
'''
tv_detail = [foo for foo in soup.find_all(class_='item-root') if re.search('.*'+str(year)+'.*',foo.find(class_='title-text').text)]
for foo in tv_detail:
index+=1
if foo.find(class_='rating_nums'):
df.loc[index,'year'] = year
df.loc[index,'rating_nums'] = foo.find(class_='rating_nums').text
df.loc[index,'rating_people'] = re.search('\D*(\d*)\D*',foo.find(class_='pl').text).group(1)
df.loc[index,'title'] = foo.find(class_='title-text').text
split_res = foo.find(class_='meta abstract').text.replace(' ','').split('/')
df.loc[index,'country'] = split_res[0]
df.loc[index,'tv_type'] = ','.join([foo for foo in split_res[1:] if len(foo)<=2])
for year in range(1991,2019):
# url後綴參數
start_num = 0
while(1):
url_raw_1 = "https://search.douban.com/movie/subject_search?search_text={}+電視劇&start={}".format(year,start_num*15)
soup = get_soup(url_raw_1)
html_parser(soup,year)
# 判斷是否存在後續頁(若無,則該年結束,繼續下一年的爬取)
if not soup.find(class_='next activate'):
start_num+=1
else:
# 這一過程耗時較長,爲了防止意外導致數據丟失,所以每一年的爬取完成之後,保存結果
df.to_csv('tv_data_1990_2018.csv')
break
df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 7426 entries, 3 to 8817
Data columns (total 6 columns):
year 7426 non-null float64
rating_nums 7426 non-null object
rating_people 7426 non-null object
title 7426 non-null object
country 7426 non-null object
tv_type 7426 non-null object
dtypes: float64(1), object(5)
memory usage: 726.1+ KB
嗯,1990年-2018年一共獲取了7426條數據,似乎沒啥問題。
開始進行數據清洗吧。
數據清洗
- 由於之前的序號不是嚴格順序的(因爲各種因素出現序號斷層),我們將數據重新排序,並且指定新的序號(強迫症了)。
- 在上面的info表中,可以發現,rating_nums和rating_people兩項,我希望它是float類型的。
- 部分國家的電視劇數量太少,由於現在主要的分析目標是中國電視劇發展趨勢,所以將30年來電視劇統計量小於100的剔除掉
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from collections import Counter
import re
dfnew = df.sort_index().reset_index().drop(columns=['index'])
dfnew['rating_nums'] = dfnew['rating_nums'].astype('float')
dfnew['rating_people'] = dfnew['rating_people'].astype('int')
dfnew['year'] = dfnew['year'].astype('int')
countries = [k for k,v in Counter(df.country).items() if v > 300]
df_final = dfnew.drop(index=[index for index in dfnew.index if dfnew.loc[index,'country'] not in countries])
df_final = df_final.sort_index().reset_index().drop(columns=['index'])
df_final.head()
year | rating_nums | rating_people | title | country | tv_type | |
---|---|---|---|---|---|---|
0 | 1991 | 9.4 | 101919 | 東京愛情故事 東京ラブストーリー (1991) | 日本 | 愛情 |
1 | 1991 | 9.6 | 4490 | 成長的煩惱 第七季 Growing Pains Season 7 (1991) | 美國 | 喜劇,家庭 |
2 | 1991 | 9.2 | 1509 | 宋飛正傳 第三季 Seinfeld Season 3 (1991) | 美國 | 喜劇 |
3 | 1991 | 7.4 | 4598 | 外來妹 (1991) | 中國大陸 | 劇情,愛情 |
4 | 1991 | 8.9 | 2140 | 宋飛正傳 第二季 Seinfeld Season 2 (1991) | 美國 | 喜劇 |
開始分析
df_year_rate = df_final[['rating_nums','year','country']].groupby(['year','country'],as_index=False).aggregate(np.average)
df_year_rate.head()
year | country | rating_nums | |
---|---|---|---|
0 | 1990 | 美國 | 9.600000 |
1 | 1991 | 中國大陸 | 6.966667 |
2 | 1991 | 中國香港 | 7.388889 |
3 | 1991 | 日本 | 8.400000 |
4 | 1991 | 美國 | 8.771429 |
plt.figure(figsize=(50,20))
sns.lineplot(x=df_year_rate.year,y=df_year_rate.rating_nums,hue=df_year_rate.country)
plt.xticks(rotation=90)
(array([1985., 1990., 1995., 2000., 2005., 2010., 2015., 2020.]),
<a list of 8 Text xticklabel objects>)
def ten_year_type_count(dften):
s1= ','.join(dften.tv_type)
all_type = re.sub(r',+',',',s1)
return [foo for foo in sorted(Counter(all_type.split(',')).items(),key=lambda item:item[1],reverse=True) if foo[1]>5]
five_list = [ten_year_type_count(df_final[df_final.country=="中國大陸"][df_final.year<=foo][df_final.year>foo-4]) for foo in range(1994,2021,4)]
type_list =list(set([foo1[0] for foo in five_list for foo1 in foo]))
df_five_type = pd.DataFrame(index = [1994,1998,2002,2006,2010,2014,2018],columns=type_list,data=0)
for index,five in enumerate(five_list):
for foo in five:
df_five_type.loc[1994+index*4,foo[0]] = foo[1]
df_five_type
古裝 | 兒童 | 傳記 | 家庭 | 歷史 | 動作 | 驚悚 | 劇情 | 戰爭 | 懸疑 | 愛情 | 奇幻 | 科幻 | 喜劇 | 犯罪 | 武俠 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1994 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 13 | 0 | 0 | 0 | 0 | 0 | 7 | 0 | 0 |
1998 | 0 | 0 | 0 | 0 | 7 | 0 | 0 | 39 | 0 | 0 | 12 | 0 | 0 | 7 | 0 | 0 |
2002 | 26 | 0 | 0 | 7 | 16 | 0 | 0 | 81 | 0 | 0 | 17 | 0 | 0 | 10 | 10 | 9 |
2006 | 30 | 0 | 0 | 13 | 9 | 8 | 0 | 105 | 9 | 6 | 30 | 0 | 0 | 25 | 6 | 0 |
2010 | 12 | 7 | 0 | 34 | 11 | 12 | 0 | 164 | 20 | 13 | 40 | 6 | 0 | 21 | 9 | 0 |
2014 | 48 | 6 | 16 | 108 | 44 | 19 | 0 | 384 | 82 | 26 | 181 | 11 | 6 | 85 | 13 | 9 |
2018 | 74 | 7 | 0 | 58 | 11 | 20 | 8 | 440 | 52 | 66 | 182 | 50 | 9 | 78 | 30 | 11 |
plt.figure(figsize=(20,10))
sns.heatmap(data=df_five_type, annot=True, fmt="d", linewidths=.5)
<matplotlib.axes._subplots.AxesSubplot at 0x1a2ffe5be0>
總結
大概地總結一下吧,這裏一定程度上真實地反映了人們觀念的變化對電視劇市場造成的自然選擇以及歷史的必然趨勢。
之所以說是歷史的趨勢,可以看到,隨着時間的增長,劇種、劇目數量,都呈現上升的趨勢,這是由於經濟基礎的建設,人民滿足了基本的物質需求,繼而開始追求精神需求的體現。
10年以前,戰爭劇稀少,諸如《亮劍》、《團長》等優質的抗戰劇大多誕生於這個時期。10年以後,由於xx的扶持和被優質片打開的市場,戰爭題材的電視劇開始出現井噴,說好聽點良莠不齊,說難聽點,一堆雜草羣魔亂舞,諸多的“神劇”就是這之後的產物。
家庭、愛情劇方面,這一部分我看的較少,不過大概也是可以分析一下。14年以前,受韓劇市場大成功的影響,加上題材容易量產,所以得到了資本的大量傾注,諸如《回家的誘惑》等劇就是這個時間段的產物。而14年以後,市場由“家庭”偏向“愛情”,主要是因爲市場的主力軍變了,新生代的大學生開始成爲左右這一部分市場的中堅力量,自然市場就要向着迎合這一部分人羣的方向發展。
古裝劇漲勢良好,以往(14年以前)的古裝劇,多以正劇、武俠劇爲主(但是因爲武俠又以香港地區爲主,所以該表顯示的武俠劇並沒有大家印象中的那麼多),而14年以後,以於正爲首的抄襲派作家找到了量產古裝劇的套路,同時,古裝劇在“造星”方面有着天然的優勢,所以古裝劇在近幾年發展迅速,尤以“古裝+愛情”這樣的組合見多。
好吧,暫時就先分析這麼多吧,更多的就留着以後有閒工夫了,再做探究。