1. 程式人生 > >selenium爬取QQ空間 (上)

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等來解析頁面。之後返回每個說說和對應的時間。