selenium爬取QQ空間 (上)
這幾天在看《從零開始學python網路爬蟲》中的模擬瀏覽器篇,對其中的爬取好友說說比較感興趣,不過書中只是爬取每個好友第一頁說說,因此我稍微改進了下(發書名是尊重作者,不過個人認為這本書講得比較淺,不求甚解)。
先大致說一下我遇到的坑。首先,如果想要看別人的說說,是必須要登入的(使用cookie應該也可以);然後,可能沒有許可權訪問好友空間;最後則是獲取下一頁連結並點選前還要注意可能沒有下一頁了。
本次使用的是selenium和Chrome,書中使用的是PlantomJS,為什麼不使用這個呢?看下圖:
上圖說的是PhantomJS已經廢棄了,不建議使用,可以選用Chrome或FireFox瀏覽器的無介面模式。記得前幾天逛csdn時有人說一開始只有PhantomJS支援selenium的,自從其他大公司支援後,老夥伴PhantomJS就沒落了。。。
1.獲取QQ通訊列表
QQ郵箱中可以匯出好友郵箱。開啟QQ郵箱中的通訊錄:
之後的“工具”|“匯出聯絡人”,選擇以csv格式匯出:
之後下載該csv即可。該檔案欄位大致如下:
我們這裡主要用到的是“姓名”和“電子郵件”。
2.大致流程
因為有了QQ郵箱的存在,所以我們可以通過QQ郵箱來反推得到QQ號,不過需要注意的是,並不是所有的電子郵箱都是QQ郵箱。分析一下QQ郵箱的特徵:
QQ號@qq.com
嗯~,qq號都是數字,那麼可以使用正則表示式來提取出QQ號,具體實現如下:
def get_qq_numbers(filename): ''' 從csv檔案中獲取正確的qq號 並yield ''' #正則匹配出qq號 pattern = re.compile('(.*?)@qq\.com') fp = open(filename, 'r') reader = csv.DictReader(fp) #獲取每個使用者的電子郵箱 for row in reader: name = row['姓名'] email = row['電子郵件'] qqNumber = re.search(pattern, email) if qqNumber: yield qqNumber.group(1), name else: logging.warning('該郵箱不是QQ號: %s %s' % (name, email)) fp.close()
get_qq_number()是讀取剛才匯出的csv檔案,然後獲取“電子郵件”中的值,並使用正則表示式進行提取,如果提起成功,則表示該郵箱是QQ郵箱,那麼得到的一般情況下是qq號。
要匯入的包如下:
import csv import re import logging import json import selenium from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import config logging.basicConfig(level=logging.WARNING,#控制檯列印的日誌級別 filename='new.log', filemode='w', format= '%(asctime)s: %(message)s' ) options = webdriver.ChromeOptions() #無圖模式 prefs = { 'profile.default_content_setting_values': { 'images': 2 } } options.add_experimental_option('prefs', prefs) #瀏覽器隱藏 options.add_argument('--headless') browser = webdriver.Chrome(chrome_options = options) wait = WebDriverWait(browser, 5) browser.implicitly_wait(3)
首先配置了logging,捕捉WARNING及以上並寫入到new.log檔案。
之後設定Chrome不顯示圖片,並且隱藏。
最後建立了一個瀏覽器,並同時設定了隱式等待和顯示等待,個人認為兩者區別不大,當沒有獲得到對應的節點時都會丟擲異常;不過顯示等待相對比較靈活,可以選擇等待條件。
先看一下總呼叫(額,主函式?):
if __name__ == '__main__':
#限定爬取個數
start = 1
end = 5
notes = []
for qq, name in get_qq_numbers('./QQmail.csv'):
L = {}
L['qq'] = qq
L['name'] = name
L['notes'] = []
print(f'crawling {qq} {name}')
for data in get_info(qq, name, 3):
#pprint.pprint(data)
L['notes'].append(data)
if len(L['notes']) != 0:
notes.append(L)
#限制爬取數目
start += 1
if start > end:
break
#寫入檔案
fp = open('notes.json', 'w', encoding = 'utf-8')
fp.write(json.dumps(notes, indent = 2, ensure_ascii = False))
fp.close()
browser.close()
這裡面限定了爬取的個數,可以根據要求自行修改。上述程式碼的邏輯大致如下:
獲取csv檔案中QQ號和對應的暱稱;然後get_info()來獲取好友的若干頁說說(這裡指定為3頁,不指定則爬取所有說說);接著判斷L中的notes列表是否為空,當該好友的空間沒有許可權訪問時,get_info()返回None,因此需要判斷是否為空,不為空則新增。最後把所有的資訊寫入檔案即可。
3.訪問QQ空間
前面的程式碼中並沒有涉及到HTML,而接下來的get_info中大部分要和HTML打交道。首先,下面的函式來判斷是否已經登入:
def is_logining(browser):
'''
判斷是否需要登入
@param browser webdriver的一個例項
@return True 表示在登入中
'''
try:
browser.find_element(By.ID, 'login_frame')
ret = False
except:
ret = True
return ret
login_frame為https://i.qq.com/中的一個標籤的id名,因此可以判斷這個標籤是否存在來判斷是否需要登入,注意這裡用的是隱式等待,在前面已經呼叫過下面這一句:
browser.implicitly_wait(3)
這個函式的說明如下:
從上面可知,在一次會話中只需要被呼叫一次即可(我看的那本書裡面呼叫了這個函式多次),呼叫後應該會對之後的每一次非顯式等待查詢生效。
之後是登入函式:
def login(browser, username, passwd):
'''
QQ登入
@param browser 瀏覽器 webdriver的一個例項
@param username 登入賬號
@param passwd 登入密碼
'''
browser.switch_to.frame('login_frame')
switcher = browser.find_element(By.ID, 'switcher_plogin')
switcher.click()
#輸入賬號和密碼
user_input = browser.find_element(By.ID, 'u')
user_input.clear()
user_input.send_keys(username)
passwd_input = browser.find_element(By.ID, 'p')
passwd_input.clear()
passwd_input.send_keys(passwd)
login_btn = browser.find_element(By.ID, 'login_button')
login_btn.click()
login負責輸入賬號和密碼,然後點選登入。
上面兩個函式都是要在get_info中使用。
def get_info(qq, name = None, maxPage = None):
'''
傳入qq號,爬取其說說
'''
url = f'https://user.qzone.qq.com/{qq}/311'
browser.get(url)
#是否已經登入
logining = is_logining(browser)
#登入
if logining == False:
login(browser, config.username, config.passwd)
#是否允許訪問
try:
browser.find_element(By.ID, 'QM_OwnerInfo_Icon')
permission = True
except:
permission = False
#允許訪問 第一次需要切換frame
if permission == True:
browser.switch_to.frame('app_canvas_frame')
首先根據QQ號碼生成url,之後訪問該連結,然後判斷是否需要登入;最後則通過檢視標籤的方式來判斷是否有許可權來訪問QQ空間,另外使用者名稱和密碼存入了config.py這個檔案中,自行修改即可。
#迴圈獲取說說
index = 1
while permission == True and (maxPage == None or index <= maxPage):
#獲取下一頁按鈕
try:
next_btn = wait.until(EC.presence_of_element_located( \
(By.CSS_SELECTOR, f'#pager_next_{index - 1}')))
#獲取失敗,沒有下一頁
except selenium.common.exceptions.TimeoutException as e:
#退出迴圈條件
maxPage = index
#獲取說說內容和發表時間
contents = browser.find_elements(By.CSS_SELECTOR, '.content')
times = browser.find_elements(By.CSS_SELECTOR, '.c_tx.c_tx3.goDetail')
print('crawling page', index)
for content, tim in zip(contents, times):
data = {
'time' : tim.text,
'content': content.text,
}
yield data
#下一頁
index += 1
if index <= maxPage:
next_btn.click()
#不可訪問頁面
if permission == False:
logging.warning('該使用者不允許你訪問 %s %s' % (name, qq))
print('該使用者不允許你訪問 %s %s' % (name, qq))
接下來的迴圈中就是獲取該頁面的所有說說和發表日期。首先,先顯式等待獲取到下一頁按鈕,如果超時,則表示沒有下一頁,此時獲取該頁面的說說後退出即可;內容和時間都是通過find_elements方法獲取到的,它無法做到像xpath、beautiful soup那樣靈活,如果想要獲取每個說說下的評論的話,就需要修改成使用xpath等來解析頁面。之後返回每個說說和對應的時間。