使用webdriver+urllib爬取網頁數據(模擬登陸,過驗證碼)


urilib是python的標准庫,當我們使用Python爬取網頁數據時,往往用的是urllib模塊,通過調用urllib模塊的urlopen(url)方法返回網頁對象,並使用read()方法獲得url的html內容,然后使用BeautifulSoup抓取某個標簽內容,結合正則表達式過濾。但是,用urllib.urlopen(url).read()獲取的只是網頁的靜態html內容,很多動態數據(比如網站訪問人數、當前在線人數、微博的點贊數等等)是不包含在靜態html里面的,例如我要抓取這個bbs網站中點擊打開鏈接 各個板塊的當前在線人數,靜態html網頁是不包含的(不信你查看頁面源代碼試試,只有簡單的一行)。像這些動態數據更多的是由JavaScript、JQuery、PHP等語言動態生成的,因此再用抓取靜態html內容的方式就不合適了。

本文將通過selenium webdriver模塊的使用,以獲取這些動態生成的內容,尤其是一些重要的動態數據。其實selenium模塊的功能不是僅僅限於抓取網頁,它是網絡自動化測試的常用模塊,在Ruby、Java里面都有廣泛使用,Python里面雖然使用相對較少,但也是一個非常簡潔高效容易上手的自動化測試模塊。通過利用selenium的子模塊webdriver的使用,解決抓取動態數據的問題,還可以對selenium有基本認識,為進一步學習自動化測試打下基礎。

一 環境配置

1、下載geckodriver.exe:下載地址:https://github.com/mozilla/geckodriver/releases請根據系統版本選擇下載;(如Windows 64位系統)


2、下載解壓后將getckodriver.exe復制到Firefox的安裝目錄下,
    如(C:\Program Files\Mozilla Firefox),並在環境變量Path中添加路徑:C:\Program Files\Mozilla Firefox

3、安裝selenium

pip install selenium

4、beautifulsoup4的安裝,Beautiful Soup 是一個可以從HTML或XML文件中提取數據的Python庫.它能夠通過你喜歡的轉換器實現慣用的文檔導航,

pip install beautifulsoup4
pip install lxml
pip install html5lib

 5、安裝faker

pip install faker

 

二 如何爬取落網某一期數據信息

落網的網址是http://www.luoo.net,我們利用火狐瀏覽器打開該網址,隨便選擇一期音樂,這里我點擊的是搖滾,

然后點擊F12,選擇第989期,進入該頁面,我們就以爬取這一頁的內容為例:

進入之后,我們可以看到該網頁主要包括以下內容:期刊編號,期刊標題,期刊封面,期刊描述,歌單。

通過下方開發工具中的查看器,可以獲取我們感興趣數據的標簽,選擇器等信息。以歌單為例:

程序代碼如下:

# -*- coding: utf-8 -*-
"""
Created on Thu May 24 16:35:36 2018

@author: zy
"""
import os
from bs4 import BeautifulSoup
import random
from faker import Factory
import queue
import threading
import urllib.request as urllib  
from selenium import webdriver
import time



#"gbk" codec can't encode character "\xXX" in position XX : https://www.cnblogs.com/feng18/p/5646925.html
#即字符編碼是utf-8的字節,但是並不能轉換成utf-8編碼的字符串     

'''
爬取網頁信息的類
'''
def random_proxies(proxy_ips):
    '''
    從proxy_ips中隨機選取一個代理ip    
    
    args:
        proxy_ips:list 每個元素都是一個代理ip
    '''
    ip_index = random.randint(0, len(proxy_ips)-1)
    res = { 'http': proxy_ips[ip_index] }
    return res


def fix_characters(s):
    '''
    替換掉s中的一些特殊字符 
    
    args:
        s:字符串
    '''
    for c in ['<', '>', ':', '"', '/', '\\', '|', '?', '*']:
        s = s.replace(c, '')
    return s


def get_static_url_response_html(url):    
    '''
    爬取靜態頁面數據:https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html#find
    
    reture:
        返回html代碼
    '''
    fake = Factory.create()
    # 這里配置可用的代理IP,可以寫多個
    proxy_ips = [
                '183.129.151.130' 
            ]
    headers = {'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 
            'Accept-Charset':'GB2312,utf-8;q=0.7,*;q=0.7', 
            'Accept-Language':'zh-cn,zh;q=0.5', 
            'Cache-Control':'max-age=0', 
            'Connection':'keep-alive', 
            'Host':'John', 
            'Keep-Alive':'115', 
            'Referer':url, 
            'User-Agent': fake.user_agent()
            #'User-Agent': 'User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
            }
    req = urllib.Request(url=url,origin_req_host=random_proxies(proxy_ips),headers = headers)  
    #當該語句讀取的返回值是bytes類型時,要將其轉換成utf-8才能正常顯示在python程序中
    response = urllib.urlopen(req).read()   
    #需要進行類型轉換才能正常顯示在python中      
    response = response.decode('utf-8')         
    return response


def download(url,dstpath=None):
    '''
    利用urlretrieve()這個函數可以直接從互聯網上下載文件保存到本地路徑下
    
    args:
        url:網頁文件或者圖片以及其他數據路徑,盡量不要下載網頁,因為下載的是靜態網頁
        dstpath:保存全路況
    '''        
    if dstpath is None:
        dstpath = './code.jpg'
    try:
        urllib.urlretrieve(url,dstpath)                    
        print('Download from {} finish!'.format(url))
    except Exception as e:
        print('Download from {} fail!'.format(url))
        
        

def get_dynamic_url_response_html(url):
    '''    
    selenium是一個用於Web應用自動化程序測試的工具,測試直接運行在瀏覽器中,就像真正的用戶在操作一樣
    selenium支持通過驅動真實瀏覽器(FirfoxDriver,IternetExplorerDriver,OperaDriver,ChromeDriver)
    selenium支持通過驅動無界面瀏覽器(HtmlUnit,PhantomJs)
    

    1、下載geckodriver.exe:下載地址:https://github.com/mozilla/geckodriver/releases請根據系統版本選擇下載;(如Windows 64位系統)
    2、下載解壓后將getckodriver.exe復制到Firefox的安裝目錄下,
    如(C:\Program Files\Mozilla Firefox),並在環境變量Path中添加路徑:C:\Program Files\Mozilla Firefox
    
    return:
        返回html代碼  獲取url頁面信息后關閉連接
    '''         
    browser = webdriver.Firefox(executable_path = r'D:\ff\geckodriver.exe')    
    #html請求
    browser.get(url)    
    html = browser.page_source
    time.sleep(2)
    #html = browser.page_source.decode('utf-8')
    #關閉瀏覽器
    browser.quit() 
    return html


class UrlSpyder(threading.Thread):
    '''
    使用Threading模塊創建線程:http://www.runoob.com/python/python-multithreading.html
    使用Threading模塊創建線程,直接從threading.Thread繼承,然后重寫__init__方法和run方法:
    
    主要思路分成兩部分:
        1.用來發起http請求分析出播放列表然后丟到隊列中
        2.在隊列中逐條下載文件到本地(如果需要下載文件)
        一般分析列表速度較快,下載速度比較慢,可以借助多線程同時進行下載
    '''
    def __init__(self, base_url, releate_urls, que=None):
        '''
        args:
            base_url:網址
            releate_urls:相對於網址的網頁路徑  list集合
            que:隊列  Python的queue模塊中提供了同步的、線程安全的隊列類,包括FIFO(先入先出)隊列Queue,LIFO(后入先出)隊列LifoQueue,
                      和優先級隊列PriorityQueue。這些隊列都實現了鎖原語,能夠在多線程中直接使用。可以使用隊列來實現線程間的同步。
        '''
        threading.Thread.__init__(self)
        print('Start spider\n')
        print ('=' * 50)
        
        #保存字段信息
        self.base_url = base_url
        if queue is None:
            self.queue = queue.Queue()     
        else:
            self.queue = que   
        self.releate_urls = releate_urls

    def run(self):
        '''
        把要執行的代碼寫到run函數里面 線程在創建后會直接運行run函數
        '''
        #遍歷每一個網頁 並開始爬取
        for releate_url in self.releate_urls:
            self.spider(releate_url)
        print ('\nCrawl end\n\n')
    
    def spider(self, releate_url):
        '''        
        爬取指定網頁數據,並提取我們所需要的網頁信息的函數
        
        args:
            releate_url:網頁的相對路徑            
        '''
        url = os.path.join(self.base_url,releate_url)        
        print ('Crawling: ' + url + '\n')

        '''
        解析html  針對不同的網址,解析內容也不一樣
        '''
        #獲取指定網頁的html代碼
        response = get_dynamic_url_response_html(url)
        #response = getUrlRespHtml(url)        )
     
        #使用BeautifulSoup解析這段代碼,能夠得到一個 BeautifulSoup 的對象,並能按照標准的縮進格式的結構輸出:
        #soup = BeautifulSoup(response, 'lxml')
        soup = BeautifulSoup(response, 'html.parser')
    
        #輸出網頁信息
        #print(soup.prettify())
        #with open('./read.html','w',encoding='utf-8') as f:
            #兩個內容是一樣的 只是一個是標准縮進格式,另一個不是
            #f.write(str(soup.prettify()))
            #f.write(str(response))
        '''
        根據爬取網站不同,下面代碼也會不一樣
        解析該網站某一期的所有音樂信息
        主要包括:
            期刊信息 
            期刊封面 
            期刊描述 
            節目清單
        '''        
        #<span class="vol-number rounded">989</span>
        num = soup.find('span',class_='vol-number').get_text()
        
        #<span class="vol-title">曙色初動</span>  解析標題  find每次只返回第一個元素
        title = soup.find('span',class_='vol-title').get_text()
        '''
        <div class="vol-cover-wrapper" id="volCoverWrapper">
            <img src="http://img-cdn2.luoo.net/pics/vol/554f999074484.jpg!/fwfh/640x452" alt="曙色初動" class="vol-cover">
            <a href="http://www.luoo.net/vol/index/739" class="nav-prev" title="后一期" style="display: inline; opacity: 0;">&nbsp;</a><a href="http://www.luoo.net/vol/index/737" class="nav-next" title="前一期" style="display: inline; opacity: 0;">&nbsp;</a>
        </div>
        解析對應的圖片背景
        '''        
        cover = soup.find('img', class_='vol-cover').get('src')
        '''
        <div class="vol-desc">
        本期音樂為史詩氛圍類音樂專題。<br><br>史詩音樂的美好之處在於能夠讓人有無限多的宏偉想象。就像這一首首曲子,用歲月滄桑的厚重之聲,鏗鏘有力的撞擊人的內心深處,綻放出人世間的悲歡離合與決絕!<br><br>Cover From Meer Sadi    
        </div>
        解析描述文本
        '''
        desc = soup.find('div', class_='vol-desc').get_text() 
        
        '''
        <a href="javascript:;" rel="nofollow" class="trackname btn-play">01. Victory</a>
        <a href="javascript:;" rel="nofollow" class="trackname btn-play">02. Mythical Hero</a>
        
        解析歌單   find_all返回一個列表 所有元素
        '''        
        track_names = soup.find_all('a', attrs={'class': 'trackname'})
        track_count = len(track_names)
        tracks = []
        # 12期前的音樂編號1~9是1位(如:1~9),之后的都是2位 1~9會在左邊墊0(如:01~09)
        for track in track_names:            
            _id = str(int(track.text[:2])) if (int(releate_url) < 12) else track.text[:2]  
            _name = fix_characters(track.text[4:])
            tracks.append({'id': _id, 'name': _name})

        phases = {
            'phase': num,                        # 期刊編號
            'title': title,                      # 期刊標題
            'cover': cover,                      # 期刊封面
            'desc': desc,                        # 期刊描述
            'track_count': track_count,          # 節目數
            'tracks': tracks                     # 節目清單(節目編號,節目名稱)
            }   
                
        #追加到隊列
        self.queue.put(phases)



if __name__ == '__main__':
    luoo_site = 'http://www.luoo.net/vol/index/'
     
    spyder_queue = queue.Queue()

    ## 創建新線程
    luoo = UrlSpyder(luoo_site, releate_urls=['1372','1370'], que=spyder_queue)
    luoo.setDaemon(True)
    #開啟線程
    luoo.start()

    '''
    從隊列中取數據,並進行下載
    '''
    count = 1
    while True:        
        if spyder_queue.qsize() <= 0:
            time.sleep(10)
            pass
        else:
            phases = spyder_queue.get()
            print(phases)
            #下載圖片
            download(phases['cover'],'%d.jpg'%(count))
            count += 1
            

運行結果如下:

並爬取了如下兩種圖片:

 注意:我們一般使用 urllib.urlretrieve()函數下載圖片,文件等,但是不要使用這個函數下載網頁,因為下載下來也是靜態網頁。如果要保存網頁html內容,我們使用selenium獲取網頁內容,然后寫入到指定文件:

with open(file_name, 'w',encoding='utf-8') as f:  
        f.write(self.browser.page_source) 

 三 使用selenium模擬登陸

剛才我們爬取的網站是不需要登陸的,如果遇到一個需要先登錄,才能爬取數據怎么辦?幸運的是selenium提供了模擬鼠標點擊,和鍵盤輸入的功能,下面我們將會演示一個登陸微博的程序,並利用微博進行搜索我們需要的內容,並把數據保存下來。

下面我先列入完整代碼,一會一點點講解:

# -*- coding: utf-8 -*-
"""
Created on Thu May 24 16:35:36 2018

@author: zy
"""
import os
import time
from selenium import webdriver
#from selenium.common.exceptions import NoSuchElementException  
from bs4 import BeautifulSoup


class WeiboSpyder():
    '''
    python爬蟲——基於selenium用火狐模擬登陸爬搜索關鍵詞的微博:https://blog.csdn.net/u010454729/article/details/51225388
    0.安裝火狐 
    1.安裝selenium,可通過pip安裝:pip install selenium 
    2.程序里面改三處:用戶名、密碼、搜過的關鍵詞search_text 
    3.需要手動輸入驗證碼,並且驗證碼大小寫敏感,若是輸錯了,等3秒再輸入。 
    4.爬下來了,用BeautifulSoup提取,並且將相同文本的放在一起,並且排序 
     
    時間:5秒一個,不可縮短,不然加載不出來下一頁這個按鈕,然后程序就掛了,若網速快些,延時可以短些。 在每個點擊事件之前或者之后都加time.sleep(2)  
    '''  
    def __init__(self):
        #微博登錄用戶名,用戶命名
        self.username_  = "你的用戶名"  
        self.password_  = "你的密碼"  
        self.href = 'http://s.weibo.com/'  
              
        #獲取火狐瀏覽器對象
        self.browser = webdriver.Firefox(executable_path = r'D:\ff\geckodriver.exe')       
        #html請求
        self.browser.get(self.href)  
        time.sleep(2)  
              
        #獲取網頁右上登陸元素 並點擊
        login_btn = self.browser.find_element_by_xpath('//a[@node-type="loginBtn"]')  
        login_btn.click()  
        time.sleep(2)  
              
        #獲取選擇賬號登錄元素 並點擊  
        name_login = self.browser.find_element_by_xpath('//a[@action-data="tabname=login"]')  
        name_login.click()  
        time.sleep(2)  
              
        #獲取輸入用戶名,密碼元素 並輸入用戶名和密碼
        username = self.browser.find_element_by_xpath('//input[@node-type="username"]')  
        password = self.browser.find_element_by_xpath('//input[@node-type="password"]')  
        username.clear()  
        username.send_keys(self.username_)  
        password.clear()  
        password.send_keys(self.password_)  
              
        #獲取提交登陸元素 並點擊
        sub_btn = self.browser.find_element_by_xpath('//a[@suda-data="key=tblog_weibologin3&value=click_sign"]')  
        sub_btn.click()  
        time.sleep(5)  
        
        '''
        #下面是驗證碼部分,如果需要驗證碼的化
        while True:  
            try:  
                verify_img = browser.find_element_by_xpath('//img[@node-type="verifycode_image"]')  
            except NoSuchElementException:  
                break  
            if verify_img:  
                # 輸入驗證碼  
                verify_code = browser.find_element_by_xpath('//input[@node-type="verifycode"]')  
                verify_code_ = input('verify_code > ')  
                verify_code.clear()  
                verify_code.send_keys(verify_code_)  
          
                # 提交登陸  
                sub_btn = browser.find_element_by_xpath('//a[@suda-data="key=tblog_weibologin3&value=click_sign"]')  
                sub_btn.click()  
                time.sleep(2)  
            else:  
                break  
        '''    
        #獲取搜索欄元素
        #self.search_form = self.browser.find_element_by_xpath('//input[@class="searchInp_form"]')        
         
 
    def get_weibo_search(self,search_text,max_length = 20):  
        '''
        在網頁中搜索指定信息,搜多到一頁信息后,保存,然后搜索下一頁信息,直至到max_length頁
        默認在當前路徑下生成一個  名字為 search_text(去除下划線) 的文件夾,下面存放爬取得網頁
        args:
            search_text:需要搜索的內容
            max_length:最大爬取網頁個數
        return:
            dst_dir:返回爬取網頁所在的文件夾
        ''' 
        #將關鍵詞送到搜索欄中,進行搜索  
        self.search_form = self.browser.find_element_by_xpath('//input[@class="searchInp_form"]')        
        self.search_form.clear() 
        self.search_form.send_keys(search_text)
        time.sleep(5)
        
        #獲取搜索按鈕元素 只有窗口最大化 才有搜索按鈕
        self.search_btn = self.browser.find_element_by_xpath('//a[@class="searchBtn"]') 
        #點擊搜索   
        self.search_btn.click()  
        #進入循環之前,讓第一頁先加載完全。  
        time.sleep(2)
             
        print('Try download html for : {}'.format(search_text))       
        topics_name = search_text  
        #將名字里面的空格換位_   創建以搜索內容為名字的文件夾,保存爬取下來的網頁
        topics_name = topics_name.replace(" ","_")
        os_path = os.getcwd()  
        dst_dir = os.path.join(os_path,topics_name)  
        if not os.path.isdir(dst_dir):  
            os.mkdir(dst_dir)  
        #捕獲異常,有的搜索可能沒有下一頁 遇到錯誤會跳過
        try:            
            count = 1              
            while count <= max_length:  
                '''
                #保存網頁   構建目標文件路徑
                '''
                file_name = os.path.join(dst_dir,'{}_{}.html'.format(topics_name, count))  
                #必須指定編碼格式 
                with open(file_name, 'w',encoding='utf-8') as f:  
                    f.write(self.browser.page_source)  
                print('Page {}  download  finish!'.format(count))  
              
                time.sleep(3)
                #獲取下一頁元素
                self.next_page = self.browser.find_element_by_css_selector('a.next')  
                #next_page = browser.find_element_by_xpath('//a[@class="page next S_txt1 S_line1"]')              
                #有的時候需要手動按F5刷新,不然等太久依然還是出不來,程序就會掛,略脆弱。  
                self.next_page.click()
                count += 1  
                #完成一輪之前,保存之前,先讓其加載完,再保存   如果報錯,可以通過調節時間長度去除錯誤
                time.sleep(10)
        except Exception as e:
            print('Error:',e)
        return dst_dir
  
    def get_weibo_text(self,file_name):
        '''
        將html文件里面的<p></p>標簽的內容提取出來  
        
        args:
            text:返回一個list  每一個元素都是p標簽的內容
        args:
            file_name:html文件路徑 
        '''        
        text = []  
        soup = BeautifulSoup(open(file_name,encoding='utf-8'),'lxml')          
        #獲取tag為div class_="WB_cardwrap S_bg2 clearfix"的所有標簽
        items = soup.find_all("div",class_="WB_cardwrap S_bg2 clearfix")  
        if not items:  
            text = []  
        #遍歷每一額標簽,提取為p的子標簽內容
        for item in items:  
            line = item.find("p").get_text()  
            #print line  
            text.append(line)  
        return text  
      
    def get_weibo_all_page(self,path):
        '''
        文件夾下所有文件內容提取出來,然后合並起來
        
        args:
            path:list 每個元素都是一個文件路徑,加入我們在新浪搜索內容為火影忍者,則在當前路徑下會生成一個為火影忍者的文件夾
                path就是這個文件夾的路徑
        return:
            texts_all:返回合並之后的內容 是一個list            
        '''        
        texts_all = []  
        file_names = os.listdir(path)  
        #遍歷當前文件夾下每個文件
        for file_name in file_names:              
            #將html文件里面的<p></p>標簽的內容提取出來
            texts = self.get_weibo_text(os.path.join(path,file_name))  
            #遍歷每一個元素
            for text in texts:  
                text = text.replace("\t","")  
                text = text.strip("\n")  
                text = text.strip(" ")  
                #若是重了,不加入到里面  
                if text in texts_all:
                    pass  
                else:  
                    texts_all.append(text)  
        return texts_all  
      
        
    def get_results_weibo(self,weibos_name):
        '''
        合並若干個文件夾下提取出來的微博  
        
        args:
            weibos_name:list 每一個元素都是一個搜索項提取出來的文本文件
        args:
            
        '''
        texts = []  
        for file_name in weibos_name:  
            with open(file_name,encoding='utf-8') as f:  
                text = f.readlines()  
                for line in text:  
                    line = line.strip("\n")  
                    if line not in texts:  
                        texts.append(line)  
        return texts  




if __name__ == '__main__':
    print('開始搜索')
    search = WeiboSpyder()
    
    #在新浪搜索中需要搜索內容
    searchs_text = ["火影忍者","蒼井空","波多野結衣"]  
    
    #遍歷要搜索的每一行
    for search_text in searchs_text:  
        path = search.get_weibo_search(search_text,max_length=5)          
        texts_all = search.get_weibo_all_page(path)  
        #文本排序
        texts_all_sorted = sorted(texts_all)  
        weibo_text_name = path + "_weibos.txt"  
        with open(weibo_text_name,"w",encoding='utf-8')  as f:
            #一行一行的寫入
            for text in texts_all_sorted:  
                f.write(text + "\n")          


    #將幾個_weibos.txt文件合並到一起  
    print("Together:")  
    file_names_weibos = [i for i in os.listdir(os.getcwd()) if i.endswith("_weibos.txt")]  
    texts = search.get_results_weibo(file_names_weibos)  
    with open("results.txt","w",encoding='utf-8')  as f:
        for text in sorted(texts):  
            f.write(text+"\n")      
    print("Together finish!")  
    

微博的官網是http://s.weibo.com/,同樣我們點擊F12,打開網頁:我們先獲取右上角登陸按鈕的標簽信息:

我們怎樣可以實現點擊這個登陸的事件呢,在上面代碼可以看到我定義了一個WeiboSpyder的類,其中在構造函數中負責網頁的登陸功能。

我們首先通過向指定網址發送html請求,進行加載網頁:

        #獲取火狐瀏覽器對象
        self.browser = webdriver.Firefox(executable_path = r'D:\ff\geckodriver.exe')              
        #html請求
        self.browser.get(self.href)  
        time.sleep(2)  

然后通過find_element_by_xpath()函數獲取登陸標簽,獲取該標簽之后,我們觸發點擊事件,為了等到網頁加載出來,我們在點擊時候,睡眠2s。

        #獲取選擇賬號登錄元素 並點擊  
        name_login = self.browser.find_element_by_xpath('//a[@action-data="tabname=login"]')  
        name_login.click()  
        time.sleep(2)  

同理,后面我們獲取選擇賬號登陸標簽,然后獲取登陸名,登錄密碼元素,然后利用send_keys()方法輸入登陸信息,最后獲取登陸標簽,開始登陸:

        #獲取選擇賬號登錄元素 並點擊  
        name_login = self.browser.find_element_by_xpath('//a[@action-data="tabname=login"]')  
        name_login.click()  
        time.sleep(2)  
              
        #獲取輸入用戶名,密碼元素 並輸入用戶名和密碼
        username = self.browser.find_element_by_xpath('//input[@node-type="username"]')  
        password = self.browser.find_element_by_xpath('//input[@node-type="password"]')  
        username.clear()  
        username.send_keys(self.username_)  
        password.clear()  
        password.send_keys(self.password_)  
              
        #獲取提交登陸元素 並點擊
        sub_btn = self.browser.find_element_by_xpath('//a[@suda-data="key=tblog_weibologin3&value=click_sign"]')  
        sub_btn.click()  
        time.sleep(5)  

由於這里不需要驗證碼登陸,我們就跳過驗證碼了,在下一個案例中我會教大家如何過驗證碼。

完成登陸后,我們在主程序中開始利用微博的搜索引擎搜索指定內容:

if __name__ == '__main__':
    print('開始搜索')
    search = WeiboSpyder()
    
    #在新浪搜索中需要搜索內容
    searchs_text = ["火影忍者","蒼井空","波多野結衣"]  
    
    #遍歷要搜索的每一行
    for search_text in searchs_text:  
        path = search.get_weibo_search(search_text,max_length=5)          
        texts_all = search.get_weibo_all_page(path)  
        #文本排序
        texts_all_sorted = sorted(texts_all)  
        weibo_text_name = path + "_weibos.txt"  
        with open(weibo_text_name,"w",encoding='utf-8')  as f:
            #一行一行的寫入
            for text in texts_all_sorted:  
                f.write(text + "\n")          


    #將幾個_weibos.txt文件合並到一起  
    print("Together:")  
    file_names_weibos = [i for i in os.listdir(os.getcwd()) if i.endswith("_weibos.txt")]  
    texts = search.get_results_weibo(file_names_weibos)  
    with open("results.txt","w",encoding='utf-8')  as f:
        for text in sorted(texts):  
            f.write(text+"\n")      
    print("Together finish!")  

我在這里搜索的內容主要包括:

 searchs_text = ["火影忍者","蒼井空","波多野結衣"]  

然后遍歷每一項,開始調用get_weibo_search()方法:

 def get_weibo_search(self,search_text,max_length = 20):  
        '''
        在網頁中搜索指定信息,搜多到一頁信息后,保存,然后搜索下一頁信息,直至到max_length頁
        默認在當前路徑下生成一個  名字為 search_text(去除下划線) 的文件夾,下面存放爬取得網頁
        args:
            search_text:需要搜索的內容
            max_length:最大爬取網頁個數
        return:
            dst_dir:返回爬取網頁所在的文件夾
        ''' 
        #將關鍵詞送到搜索欄中,進行搜索  
        self.search_form = self.browser.find_element_by_xpath('//input[@class="searchInp_form"]')        
        self.search_form.clear() 
        self.search_form.send_keys(search_text)
        time.sleep(5)
        
        #獲取搜索按鈕元素 只有窗口最大化 才有搜索按鈕
        self.search_btn = self.browser.find_element_by_xpath('//a[@class="searchBtn"]') 
        #點擊搜索   
        self.search_btn.click()  
        #進入循環之前,讓第一頁先加載完全。  
        time.sleep(2)
             
        print('Try download html for : {}'.format(search_text))       
        topics_name = search_text  
        #將名字里面的空格換位_   創建以搜索內容為名字的文件夾,保存爬取下來的網頁
        topics_name = topics_name.replace(" ","_")
        os_path = os.getcwd()  
        dst_dir = os.path.join(os_path,topics_name)  
        if not os.path.isdir(dst_dir):  
            os.mkdir(dst_dir)  
        #捕獲異常,有的搜索可能沒有下一頁 遇到錯誤會跳過
        try:            
            count = 1              
            while count <= max_length:  
                '''
                #保存網頁   構建目標文件路徑
                '''
                file_name = os.path.join(dst_dir,'{}_{}.html'.format(topics_name, count))  
                #必須指定編碼格式 
                with open(file_name, 'w',encoding='utf-8') as f:  
                    f.write(self.browser.page_source)  
                print('Page {}  download  finish!'.format(count))  
              
                time.sleep(3)
                #獲取下一頁元素
                self.next_page = self.browser.find_element_by_css_selector('a.next')  
                #next_page = browser.find_element_by_xpath('//a[@class="page next S_txt1 S_line1"]')              
                #有的時候需要手動按F5刷新,不然等太久依然還是出不來,程序就會掛,略脆弱。  
                self.next_page.click()
                count += 1  
                #完成一輪之前,保存之前,先讓其加載完,再保存   如果報錯,可以通過調節時間長度去除錯誤
                time.sleep(10)
        except Exception as e:
            print('Error:',e)
        return dst_dir

這個函數主要就做了以下事情:

  • 將關鍵詞送到搜索欄中,進行搜索
  • 創建一個文件夾,把剛才搜索的網頁保存下來
  • 獲取下一頁元素,並觸發點擊事件,然后再把新的網頁保存下來
  • 重復上一步,直至保存了max_length頁,或者觸發的異常,停止搜索
  • 返回文件夾路徑

程序運行結果如下:

如果我們運行出現類似以下的錯誤:

這主要是因為我們頁面沒有加載完,所以才會找不到指定元素,我們可以通過延長睡眠時間解決該問題。

並且我們會生成三個文件夾:

為什么搜索蒼井空和波多野結衣只有一條數據,主要是因為被屏蔽了:

后面的代碼get_weibo_all_page()函數以及get_weibo_text(),get_results_weibo()函數就是利用BeautifulSoup對剛才獲取的幾個網頁內容進行提取,這里就不過多介紹了,最終還會生成幾個文件:

results.txt是對另外三個文件進行合並,內容如下:

 

 四 爬取需要驗證碼的網站

 其實爬取需要驗證碼網站和上一個案例是類似的,只不過需要我們輸入驗證碼,如何過驗證碼主要有兩種方式,一種是利用手動輸入,還有一種是采用自動識別技術識別出驗證碼,驗證碼的識別需要對圖像處理具有一定的基礎,或者我們利用人工神經網絡識別,但是這里為了簡單,我們可以采用

tesseract.exe這個軟件來識別,不過識別准確率很低,這個軟件使用之前需要准備工作

然后我們可以安裝相對應的python庫,來調用該軟件:

pip install  pytesser3

 不過我使用的時候總是報錯,因此就把該庫下載了,並修改了原來庫的代碼,保存在pytesser.py

# -*- coding: utf-8 -*-
"""
Created on Sat May 26 09:19:09 2018

@author: lenovo
"""
import subprocess
import os
from PIL import Image

'''
使用之前需要准備工作
1.下載tesseract.exe  https://www.polarxiong.com/archives/python-pytesser-tesseract.html 
  安裝在D:\Tesseract-OCR
2.配置環境變量 TESSDATE PREFIX = D:\Tesseract-OCR
'''

pwd = os.getcwd()
#tesseract.exe在命令行調用的命令 或者tesseract.exe文件的全路徑
tesseract_exe_name = 'D:\\Tesseract-OCR\\tesseract' 
#把圖像保存成tesseract兼容格式bmp
scratch_image_name = pwd + os.sep + "temp.bmp"
#識別后txt文件保存的路徑
scratch_text_name_root = pwd + os.sep + "temp" 
#識別之后是否清楚生成的臨時文件
cleanup_scratch_flag = True  


def image_to_scratch(im, scratch_image_name):
    '''
    Saves image in memory to scratch file.  .bmp format will be read correctly by Tesseract
    保存圖像到路徑scratch_image_name 即把圖片轉換成Tesseract兼容格式 bmp
    '''
    im.save(scratch_image_name, dpi=(200,200))


def    retrieve_text(scratch_text_name_root):
    '''
    從識別的txt文件中讀取文本
    '''
    with open(scratch_text_name_root + '.txt','r',encoding='utf-8') as f:
        text = f.read()    
    return text


def perform_cleanup(scratch_image_name, scratch_text_name_root):
    '''
    清除臨時文件
    '''
    for name in (scratch_image_name, scratch_text_name_root + '.txt', "tesseract.log"):
        try:
            os.remove(name)
        except OSError:
            pass

def call_tesseract(input_filename, output_filename):  
    '''
    Calls external tesseract.exe on input file (restrictions on types),
     outputting output_filename +"txt"
    調用 tesseract.exe識別圖片
    '''
    print(tesseract_exe_name,input_filename,output_filename)
    args = [tesseract_exe_name, input_filename, output_filename]
    proc = subprocess.Popen(args)
    retcode = proc.wait()
    if retcode!=0:
        print('Open process error')
        
        
def image_to_string(im, cleanup = cleanup_scratch_flag):
    """Converts im to file, applies tesseract, and fetches resulting text.
    If cleanup=True, delete scratch files after operation."""
    try:
       #保存圖片為scratch_image_name
        image_to_scratch(im, scratch_image_name)
       #調用tesseract生成識別文本
        call_tesseract(scratch_image_name, scratch_text_name_root)
       # 提取文本內容
        text = retrieve_text(scratch_text_name_root)
    finally:
        if cleanup:
            perform_cleanup(scratch_image_name, scratch_text_name_root)
    return text


def image_file_to_string(filename, cleanup = cleanup_scratch_flag, graceful_errors=True):
    """Applies tesseract to filename; or, if image is incompatible and graceful_errors=True,
    converts to compatible format and then applies tesseract.  Fetches resulting text.
    If cleanup=True, delete scratch files after operation."""
    try:
        try:
            call_tesseract(filename, scratch_text_name_root)
            text = retrieve_text(scratch_text_name_root)
        except:
            im = Image.open(filename)
            text = image_to_string(im, cleanup)
    finally:
        if cleanup:
            perform_cleanup(scratch_image_name, scratch_text_name_root)
    return text

下面我以爬取超星網站的評論數據為例:網址http://passport2.chaoxing.com/login?fid=2218&refer=http://i.mooc.chaoxing.com

按下F12,打開該網址,我們可以看到有一處需要輸入驗證碼:

同樣我定義了一個chaoxingSpy類,其中有兩個函數是用來處理驗證碼的:

 def read_code(self,file=None,auto=True):
        '''
        識別驗證碼
        
        args:
            file:驗證碼圖片所在本機路徑,如果為None,則自動從網頁中截取驗證碼圖片保存到'./code.jpg'
            auto:表示是否自動識別 為True:自動識別 False:手動識別  自動識別准確的不高
        '''
        '''
        對驗證碼進行區域截圖,並保存
        '''
        if file is None:
            #裁切后的驗證碼路徑
            file = './code.jpg'
            #對網頁進行全截圖 
            self.browser.get_screenshot_as_file('./all.jpg')
            img = Image.open('./all.jpg')
            #設置要裁剪的區域
            box = (490,340,570,370)
            #裁切        
            region = img.crop(box)   
            #RGBA to RGB
            region= region.convert('RGB')
            region.save(file)
            print('圖片裁切成功!')                
        else:
            img = Image.open(file)
        
        if auto:
            verify_code = self.image_file_to_string(file)
        else:
            #顯示圖片
            region.show()
            #手動輸入驗證碼  獲取驗證碼輸入元素        
            verify_code = input('verify_code > ')      
        return verify_code
    

    def image_file_to_string(self,file):
        '''
        圖像增強,自動識別簡單驗證碼
        
        args:
            file:驗證碼文件的路徑
        return:
            code:識別的驗證碼
        '''
        img = Image.open(file)
        #圖像加強,二值化
        imgry = img.convert('L')
        #對比度增強
        sharpness = ImageEnhance.Contrast(imgry) 
        sharp_img = sharpness.enhance(2.0)
        #保存
        sharp_img.save(file)
               
        code = pytesser.image_file_to_string(file)
        #去除非數字
        new_code = ''
        for i in code:
            if i in '0123456789':
                new_code += i
            
        print('識別的驗證碼為:',new_code)
        return new_code

當read_code()函數的auto參數傳入False時,表示手動輸入驗證碼,即在程序運行的時候,我們利用input()函數讀取我們控制台輸入的驗證碼,然后返回驗證碼的值。當傳入True的時候,我們通過調用tesseract.exe識別驗證碼,為了提高准確率,我們事先對圖像進行了處理,默認下我們截取當前網頁的驗證碼圖片,然后通過對圖片二值化,對比度增強處理然后在進行自動識別。

完整代碼如下:

# -*- coding: utf-8 -*-
"""
Created on Fri May 25 18:04:13 2018

@author: zy
"""


# -*- coding: utf-8 -*-
"""
Created on Thu May 24 16:35:36 2018

@author: zy
"""
import time
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException  
import urllib.request as urllib  
from PIL import Image,ImageEnhance
import pytesser

'''
webdriver使用說明:https://wenku.baidu.com/view/d64005c6af45b307e9719715.html
'''


class chaoxingSpy():
    def __init__(self):
        '''
        登錄系統
        '''
        #登錄用戶名,用戶命名
        self.username_  = "你的用戶名"  
        self.password_  = "你的密碼"  
        url = 'http://passport2.chaoxing.com/login?fid=2218&refer=http://i.mooc.chaoxing.com'
        #獲取火狐瀏覽器對象 
        self.browser = webdriver.Firefox(executable_path = r'D:\ff\geckodriver.exe')   
        #html請求
        self.browser.get(url)
        #存放登錄前后的cookies
        self.cookies = {'before':'','after':''}
        self.cookies['before'] = self.browser.get_cookies()
        print('登陸前:\n')
        for cookie in self.cookies['before']:
            print('%s -> %s' % (cookie['name'], cookie['value']))
    
        #登錄系統,循環嘗試登錄,登錄成功,就會拋異常,跳出循環繼續執行
        while True:
            try:
                #獲取登錄用戶名元素
                username = self.browser.find_element_by_id('unameId')
                #獲取登錄密碼元素
                password = self.browser.find_element_by_id('passwordId')   
                #輸入用戶名和密碼
                username.clear()  
                username.send_keys(self.username_)  
                password.clear()  
                password.send_keys(self.password_)      
                
                #判斷是否需要輸入驗證碼
                try:  
                     #獲取驗證碼圖片
                     verify_img = self.browser.find_element_by_id('numVerCode')
                     #獲取驗證碼url 就是驗證碼產生的網頁
                     #code_src= verify_img.get_attribute('src')
                except NoSuchElementException:  
                     print('不需要輸入驗證碼進行驗證!') 
                     
                if verify_img:  
                    #獲取驗證碼
                    verify_code_ = self.read_code(auto=False)
                    verify_code = self.browser.find_element_by_id('numcode')
                    verify_code.clear()  
                    verify_code.send_keys(verify_code_)  
                    
                #提交登錄
                login_btn = self.browser.find_element_by_xpath('//input[@class="zl_btn_right"]') 
                login_btn.click()
                time.sleep(3)         
                #判斷是否已經登錄進系統
                if url == self.browser.current_url:
                    #刷新網頁
                    self.browser.refresh()
                
            except:
                #cookies = self.browser.get_cookies()          
                print('登錄成功!')
                #存放登錄前后的cookies
                self.cookies['after'] = self.browser.get_cookies()
                print('登陸后:\n')
                for cookie in self.cookies['after']:
                    print('%s -> %s' % (cookie['name'], cookie['value']))
                break
                
        #利用登陸后的cookies 跳轉到需要爬取的頁面
        url = 'http://mooc1.chaoxing.com/mycourse/teachercourse?moocId=93196686&clazzid=3216311&edit=true'
        self.browser.get(url)     
        time.sleep(2) 
        #點擊超鏈接打開一個新的窗口
        moretipic_a = self.browser.find_element_by_xpath('//a[@class="moreTopic"]') 
        moretipic_a.click() 
        time.sleep(2)                 
        #0是第一個打開的窗口  1是最后一個打開的窗口 轉到最后一個窗口 超鏈接的點擊最好都要加這句代碼
        #https://www.cnblogs.com/nullbaby/articles/7205247.html
        #https://www.cnblogs.com/studyddup0212/p/9030455.html
        self.browser.switch_to_window(self.browser.window_handles[1])
        time.sleep(2)  

        
    def get_text(self):       
        '''
        獲取所有的文本
        '''
        texts = []
        count = 1
        #寫文件
        with open('./out_topics.txt', 'w',encoding='utf-8') as f:
            '''
            爬取文件夾外部數據
            '''
            txts = self.get_url_txt()
            for txt in txts:
                txt = '%s. %s'%(count,txt)
                texts.append(txt)
                f.write(txt)
                count += 1
        print('外部數據寫入成功!')     
        '''
        遍歷每一個文件夾的數據
        '''        
        with open('./in_topics.txt', 'w',encoding='utf-8') as f:
            folder_urls = self.spyder_folder()                
            for url in folder_urls:
                txts = self.get_url_txt(url)
                for txt in txts:
                    txt = '%s. %s'%(count,txt)
                    texts.append(txt)
                    f.write(txt)
                    count += 1
        print('文件夾數據寫入成功!')        

                
    def get_url_txt(self,url=None):
        '''
        遍歷一個網頁中的所有數據
        
        args:
            url:網頁網址 如果是None,就是從當前網頁讀取數據
        return:
            texts:獲取所有數據  list類型 每個元素對應一行數據                
        '''
        texts = []
        if url:
            #html請求
            self.browser.get(url)
            time.sleep(2)  
        count = 1
        while True:
            try:
                #如果有查看更多,獲取查看更多的超鏈接  循環次數越多,等待時間越長
                getmoretopic_a = self.browser.find_element_by_id('getMoreTopic')
                getmoretopic_a.click()        
                time.sleep(5)   
                print(count,' 點擊查看更多成功!')                  
                count += 1
                if count> 40:
                    self.browser.switch_to_window(self.browser.window_handles[1])
                    time.sleep(20)
                    break
            except:              
                self.browser.switch_to_window(self.browser.window_handles[1])
                time.sleep(5)
                break
        #獲取所有話題
        divlist = self.browser.find_elements_by_class_name('content1118')               
        #遍歷每一個話題
        for div in divlist:
            name = div.find_element_by_class_name('name').text
            date = div.find_element_by_class_name('gray').text               
            text = div.find_element_by_class_name('stuFont').find_element_by_tag_name('span').text                
            row = '%s  %s : %s\n'%(date,name,text)        
            texts.append(row)
            #print(row)
        print('Download from {0} finish!'.format(self.browser.current_url))
        return texts
                 
    def spyder_folder(self):
        '''
        爬取所有的文件夾路徑
        
        args:
            folderList:list每一個元素對應一個文件夾的網址
        '''
        ul = self.browser.find_element_by_xpath('//ul[@class="folderList"]')         
        folderList = []
        alist = ul.find_elements_by_tag_name('a')
        for a in alist:
            folderList.append(a.get_attribute('href'))
        return  folderList
        
    
    def read_code(self,file=None,auto=True):
        '''
        識別驗證碼
        
        args:
            file:驗證碼圖片所在本機路徑,如果為None,則自動從網頁中截取驗證碼圖片保存到'./code.jpg'
            auto:表示是否自動識別 為True:自動識別 False:手動識別  自動識別准確的不高
        '''
        '''
        對驗證碼進行區域截圖,並保存
        '''
        if file is None:
            #裁切后的驗證碼路徑
            file = './code.jpg'
            #對網頁進行全截圖 
            self.browser.get_screenshot_as_file('./all.jpg')
            img = Image.open('./all.jpg')
            #設置要裁剪的區域
            box = (490,340,570,370)
            #裁切        
            region = img.crop(box)   
            #RGBA to RGB
            region= region.convert('RGB')
            region.save(file)
            print('圖片裁切成功!')                
        else:
            img = Image.open(file)
        
        if auto:
            verify_code = self.image_file_to_string(file)
        else:
            #顯示圖片
            region.show()
            #手動輸入驗證碼  獲取驗證碼輸入元素        
            verify_code = input('verify_code > ')      
        return verify_code
    

    def image_file_to_string(self,file):
        '''
        圖像增強,自動識別簡單驗證碼
        
        args:
            file:驗證碼文件的路徑
        return:
            code:識別的驗證碼
        '''
        img = Image.open(file)
        #圖像加強,二值化
        imgry = img.convert('L')
        #對比度增強
        sharpness = ImageEnhance.Contrast(imgry) 
        sharp_img = sharpness.enhance(2.0)
        #保存
        sharp_img.save(file)
               
        code = pytesser.image_file_to_string(file)
        #去除非數字
        new_code = ''
        for i in code:
            if i in '0123456789':
                new_code += i
            
        print('識別的驗證碼為:',new_code)
        return new_code
        
      
    
    def download(self,url,dstpath=None):
        '''
        利用urlretrieve()這個函數可以直接從互聯網上下載文件保存到本地路徑下
        
        args:
            url:網頁文件或者圖片以及其他數據路徑
            dstpath:保存全路況
        '''        
        if dstpath is None:
            dstpath = './code.jpg'
        try:
            urllib.urlretrieve(url,dstpath)                    
            print('Download from {} finish!'.format(url))
        except Exception as e:
            print('Download from {} fail!'.format(url))
            
        

if __name__ == '__main__':
    #加載頁面
    spy = chaoxingSpy()
    spy.get_text()
 

我們主要是爬取下面網頁中的評論人時間,評論人姓名,評論人內容:

注意:在程序運行時如點擊下面的查看更多話題,會創建一個新的窗口:

而我們需要獲取的數據保存在討論列表頁面的話,這時候我們需要切換窗口,代碼如下:

        #點擊超鏈接打開一個新的窗口
        moretipic_a = self.browser.find_element_by_xpath('//a[@class="moreTopic"]') 
        moretipic_a.click() 
        time.sleep(2)                 
        #0是第一個打開的窗口  1是最后一個打開的窗口 轉到最后一個窗口 超鏈接的點擊最好都要加這句代碼
        #https://www.cnblogs.com/nullbaby/articles/7205247.html
        #https://www.cnblogs.com/studyddup0212/p/9030455.html
        self.browser.switch_to_window(self.browser.window_handles[1])
        time.sleep(2)  

程序運行后,會生成另個文件out_topics.txt和in_topics.txt,下面我們顯示其中一個文件的的內容:


 

為了避免我們每次都要進行登錄,我們可以利用cookies繞過登錄,詳細內容可以搜索其他博客。

參考文獻

[1]Beautiful Soup 4.2.0 文檔

[2]python爬蟲——基於selenium用火狐模擬登陸爬搜索關鍵詞的微博

[3]webdriver使用說明

[4]selenium - switch_to.window() - 多窗口切換

[5]Python 爬蟲入門(四)—— 驗證碼上篇(主要講述驗證碼驗證流程,不含破解驗證碼)

[6]Python3.6+selenium+pytesser3 實現爬蟲:含驗證碼和彈框的頁面信息爬取

[7]使用Threading模塊創建線程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM