背景音樂:雨還是不停地落下 - 孫燕姿
繼續(xù)水一篇文章……
前段時間看了電影《神秘巨星》,路轉(zhuǎn)粉啊。
我個人是很喜歡啦醉途,盡管劇情方面簡單了點捕犬,但是音樂很贊呀跷坝。
那其他人是怎么看待《神秘巨星》的呢酵镜?讓我們?nèi)?a target="_blank" rel="nofollow">豆瓣上的影評上了解一下。
可以看到有2861條影評(截止2018年2月10日)柴钻,每條影評可以收集的數(shù)據(jù)包括:作者淮韭、評分、日期贴届、影評內(nèi)容靠粪、點贊數(shù)、反對數(shù)以及評論數(shù)毫蚓。
其中占键,影評部分是被折疊的,想要看到全部內(nèi)容元潘,要么點擊“展開”畔乙,要么點擊題目跳轉(zhuǎn)到指定頁。從操作上來看翩概,前者更容易牲距,因為后者跳轉(zhuǎn)后還涉及到一個返回的過程,會造成更多的不確定性氮帐。
遇到的問題
1 豆瓣的反爬蟲機制
在寫python腳本進行爬蟲的時候嗅虏,最開始是直接用requests模塊發(fā)起get請求,結(jié)果爬了幾頁后上沐,服務(wù)器返回的是臟數(shù)據(jù)皮服。
因為最開始爬蟲是成功的,說明豆瓣的服務(wù)器是通過分析我的行為才識別出爬蟲参咙,那么在這種情況下通常有三種比較簡單的做法:
1)【改用戶】設(shè)置代理服務(wù)器龄广,不斷修改IP;
2)【改行為】延長請求間隔時間蕴侧,減少被檢測出來的概率择同;
3)【改行為】模擬人類行為,用selenium開啟瀏覽器爬染幌敲才;
由于此時我用腳本的方式已經(jīng)無法正常發(fā)起請求了(盡管我盡可能構(gòu)造了合理的頭信息和用有效的cookie還是無能為力),但是瀏覽器可以正常訪問择葡,為了趕時間紧武,我決定采用方法3,同時降低我的爬蟲速度敏储,畢竟阻星,你好我好大家好嘛。
愉快地決定了已添!
2 selenium的等待問題
在用selenium的過程中妥箕,我發(fā)現(xiàn)常常出現(xiàn)點擊<展開>失敗的情況滥酥,后來查了一下,發(fā)現(xiàn)這其實是因為該元素沒有被及時加載畦幢。
現(xiàn)在大部分的網(wǎng)絡(luò)應(yīng)用都使用AJAX技術(shù)坎吻。當(dāng)瀏覽器加載頁面時,該頁面內(nèi)的元素可能會以不同的時間間隔加載呛讲,并且加載速度同時還取決于你的網(wǎng)絡(luò)狀況禾怠。因此,這使得定位元素變得困難:如果DOM中還沒有元素贝搁,則定位函數(shù)將引發(fā)ElementNotVisibleException異常。所以芽偏,有必要對selenium引入等待雷逆,即在執(zhí)行的操作之間保持一些間隔。
Selenium Webdriver提供了兩種類型的等待:隱式和顯式污尉。顯式的等待是指WebDriver在繼續(xù)執(zhí)行之前會等待特定的條件發(fā)生膀哲。隱式的等待是指WebDriver在嘗試查找元素時會輪詢DOM一段時間。
2.1 顯式等待
以百度首頁為例被碗,我希望進入后能點擊右上角的“地圖”button某宪。
其實并不需要等待,這里只是作為示意锐朴。
通過查看網(wǎng)頁代碼兴喂,可以發(fā)現(xiàn)該button的name屬性為tj_trmap。
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 初始化瀏覽器
driver = webdriver.Chrome()
# 打開網(wǎng)頁
driver.get('https://www.baidu.com')
# 等待并點擊
wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.NAME, 'tj_trmap')))
element.click()
上面的腳本的意思是焚志,瀏覽器會最多等待10秒直到指定的元素是clickable的衣迷,然后再點擊。
所謂clickable的酱酬,就是該元素可見(不管在不在視窗之外)并且已啟用壶谒。
2.2 隱式等待
繼續(xù)用上面的case,
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 初始化瀏覽器
driver = webdriver.Chrome()
driver.implicitly_wait(10)
# 打開網(wǎng)頁
driver.get('https://www.baidu.com')
# 點擊“地圖”
element = driver.find_element_by_name('tj_trmap')
element.click()
3. selenium的定位問題
我發(fā)現(xiàn)膳沽,對于已經(jīng)加載好的元素汗菜,如果它是在可視范圍之外,即不滾動則看不到挑社,那么對它的點擊操作將會失斣山纭!
以豆瓣的招聘頁為例滔灶,我的目標(biāo)是讓瀏覽器自動從頂部滾動到底部并點擊“聯(lián)系我們”普碎。
from selenium import webdriver
# 初始化瀏覽器
driver = webdriver.Chrome()
driver.implicitly_wait(10)
# 打開網(wǎng)頁
driver.get('https://jobs.douban.com/')
# 滾動到指定元素并點擊
element = driver.find_element_by_xpath("http://a[@)
driver.execute_script("arguments[0].scrollIntoView();", element)
element.click()
上面的腳本里,我用execute_script來執(zhí)行一個js操作來實現(xiàn)滾動的效果录平,參考了python中selenium操作下拉滾動條方法匯總
爬影評
環(huán)境:python 2.7
系統(tǒng):macOS 10.13.1
模塊:BeautifulSoup麻车、selenium缀皱、pandas、numpy动猬、os啤斗、sys、time
解決了上面三個比較關(guān)鍵的問題后赁咙,開始完善腳本钮莲,爬起來!
from __future__ import print_function
import os
import sys
import time
import pandas as pd
import numpy as np
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from BeautifulSoup import BeautifulSoup
# 初始化瀏覽器
def reset_driver(current_url):
# 設(shè)置Chrome瀏覽器參數(shù)為不加載圖片
chrome_options = webdriver.ChromeOptions()
prefs = {"profile.managed_default_content_settings.images":2}
chrome_options.add_experimental_option("prefs",prefs)
driver = webdriver.Chrome('./chromedriver', chrome_options=chrome_options)
driver.implicitly_wait(10) #設(shè)置智能等待10秒
# 登陸(后來發(fā)現(xiàn)也可以不登錄啦)
# driver.get("https://www.douban.com/accounts/login")
# elem_user = driver.find_element_by_id("email")
# elem_user.send_keys("用戶名")
# elem_pwd = driver.find_element_by_id("password")
# elem_pwd.send_keys("密碼")
# elem_pwd.send_keys(Keys.RETURN)
# 跳轉(zhuǎn)到指定頁
driver.get(current_url)
return driver
# 瀏覽器滾動到指定元素的位置并點擊
def scroll_and_click(element):
wait = WebDriverWait(driver, 10)
element = wait.until(EC.visibility_of(element))
driver.execute_script("arguments[0].scrollIntoView();", element)
element.click()
# 合并結(jié)果
def merge_results():
df_list = []
# 遍歷臨時文件夾下的所有文件
for f in os.listdir(path_tmp):
if not f.startswith('.') and not f.startswith('all') and f.endswith('.csv'):
index = int(f.split('.')[0])
df = pd.read_csv(path_tmp + f)
df_list.append([index, df])
# 按照文件名的數(shù)字排序
df_list = sorted(df_list)
df_list = list(zip(*df_list))[1]
# 合并
df_all = pd.concat(df_list)
# 保存到指定文件
file_target = path_tmp + 'all.csv'
df_all.to_csv(file_target, index=False)
print('{} files -> {}'.format(len(df_list), file_target))
# 爬當(dāng)前頁的影評數(shù)據(jù)
def crawl():
# 評分字典彼水,中文→數(shù)字
rating_dict = {u'力薦':5, u'推薦':4, u'還行':3, u'較差':2, u'很差':1}
comments = []
# 若發(fā)現(xiàn)折疊崔拥,則點擊展開
unfolders = driver.find_elements_by_xpath("http://a[@class='btn-unfold']")
if len(unfolders):
scroll_and_click(unfolders[0])
# 用BeautifulSoup對網(wǎng)頁進行處理
page = BeautifulSoup(driver.page_source)
comment_grids = page.findAll('div', {'typeof':'v:Review'})
total_num = len(comment_grids) # 影評總數(shù)
page_index = int(page.find('span', {'class':'thispage'}).text) # 當(dāng)前頁碼
for N, comment in enumerate(comment_grids):
# 收集本頁的基本信息:姓名、評分凤覆、日期
name = comment.find('a', {'class':'name'}).text
rating = comment.find('span', {'property':'v:rating'})
rating = rating_dict[rating.get('title')] if rating is not None else np.nan # 沒有評分時用缺失值代替
date = comment.find('span', {'property':'v:dtreviewed'}).text
# 點擊展開獲得完整影評
element = driver.find_elements_by_xpath("http://div[@class='short-content']")[N]
scroll_and_click(element)
# 根據(jù)data_cid來定位完整影評链瓦,等待完全加載
data_cid = comment.get('data-cid').encode('utf-8')
xpath = "http://div[@property='v:description'][@data-url='https://movie.douban.com/review/{}/']".format(data_cid)
wait = WebDriverWait(driver, 10)
element = wait.until(EC.visibility_of(driver.find_element_by_xpath(xpath)))
text = element.text
# 添加到列表
comment = {
'name': name,
'rating': rating,
'time': date,
'text': text
}
comments.append(comment)
# 打印進度
print('items: {0:4.0f}:{1:4.0f} / pages: {2:4.0f}:{3:4.0f}'.format(N + 1, total_num, page_index, total_page), end='\r')
sys.stdout.flush()
# 設(shè)置點擊的時間間隔在2~4秒
time.sleep(2 + np.random.uniform() * 2)
# 保存到臨時文件
file_tmp_result = path_tmp + str(page_index) + '.csv'
df = pd.DataFrame(comments)
df.to_csv(file_tmp_result, encoding='utf-8', index=False)
# 在當(dāng)前頁創(chuàng)建一個臨時文件用于存儲臨時數(shù)據(jù)
path_tmp = './tmp/'
if not os.path.exists(path_tmp):
os.mkdir(path_tmp)
# 初始化瀏覽器
current_url = 'https://movie.douban.com/subject/26942674/reviews?start=0'
driver = reset_driver(current_url)
# 設(shè)置總頁數(shù)
page = BeautifulSoup(driver.page_source)
total_page = int(page.find('span', {'class':'next'}).findPrevious('a').text)
# 開始爬
for i in range(total_page):
crawl() # 爬取當(dāng)前頁
if i < total_page - 1:
scroll_and_click("http://a[text()='后頁>']") # 翻頁
# 合并結(jié)果
merge_results()
運行起來吧!估算完成時間為2.5個小時盯桦。
中途可以很方便地看到目前的進展慈俯,爬到了第86頁的第9個影評
items: 9: 20 / pages: 86: 147
最后可以看到147個文件合并成all.csv:
147 files -> ./tmp/all.csv
打開看結(jié)果,還不錯:
后記
在爬的過程中拥峦,發(fā)現(xiàn)極少數(shù)影評居然沒有評分贴膘,導(dǎo)致報錯,這個也是蠻神奇的略号,后來不得不在腳本里額外加了個判斷刑峡。
這次先把爬蟲的腳本寫好,之后有時間了再對這些文本進行數(shù)據(jù)分析璃哟。
有部分影評是被折疊的氛琢,打開看了以后并沒有覺得不合適,所以這次都爬了随闪。
2~4秒的點擊間隔是多次嘗試的結(jié)果阳似,對于我的網(wǎng)絡(luò)狀況而言,這樣剛好不被豆瓣服務(wù)器判定為爬蟲铐伴。如果網(wǎng)絡(luò)狀況不太好的話撮奏,得把點擊間隔設(shè)得更大一些,同時等待時間也要設(shè)得長一些当宴。
爬好的數(shù)據(jù)在如下鏈接畜吊,僅供學(xué)習(xí):https://pan.baidu.com/s/1hsTQLPa 密碼:1vki