1. 程式人生 > >python爬蟲系列(3):使用Selenium和BeautifulSoup獲取12306一個月內所有車次車票情況

python爬蟲系列(3):使用Selenium和BeautifulSoup獲取12306一個月內所有車次車票情況

首先針對標題說明一下,本次的獲取資料是指定出發地和目的地之間的車次,不是整個網站所有車次。

在此操作之前,請確保自己的相關的庫都已經安裝完全,這裡可沒有教安裝庫的方法哦~~~~好的,往下走,這次的目標網頁是 https://kyfw.12306.cn/otn/leftTicket/init,查詢12306餘票情況,老套路開啟瀏覽器的開發者模式。因為這次使用的是Selenium控制瀏覽器模擬人為點選的方式操作,所以在這之前我們需要自己使用瀏覽器點選操作一番,看看怎麼使用這個網頁:

經過一番點選測試,發現出發地和目的地都必須要選擇,出發日也是必選項。但是發現,這些輸入框都不是我們自己輸入內容,只能是先點選這個框框後(如下圖),在下面會出現出發地選項框,在這些選項框中選擇一個出發地,這樣才被網站識別為有效輸入,後面的出發日期也是同樣的道理。等這些必要的資訊都已經輸入完畢後點擊右邊那橘黃色的“查詢”按鈕,這是指定日期的所有車次資訊全部都顯示出來。

現在我們學會了怎麼查詢車班次,這時我們來整理思路,怎麼抓取30天所有的車班次呢?小腦袋瓜稍微的轉一下就能想到,只要輸入完出發地和目的地後,再輸入正確的日期,點選查詢後這一天的資訊就出來了,然後再重新輸入下一天的日期,再點選查詢,這樣反覆30次,ok,30天的資料就擺在我們眼前了。

第一步:定位出發地輸入框,在輸入框右擊,可以看到檢查(審查元素),這時候就會自動跳到開發者模式的elements中的相應的位置,然後右擊copy selector,待會兒在程式碼中會用到。右邊的目的輸入框同樣的方法實現。

    def input_FromTo(self):
        fromStation = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#fromStationText")))  #剛剛copy的內容在這裡使用
        fromStation.click()              #點選輸入框選中
        fromStation.clear()              #刪除其中預設的文字
        fromStation.send_keys('北京')    #輸入出發地
        fStation = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#citem_0 > span:nth-child(1)")))  #等待下拉選項框中指定的元素出來
        fStation.click()                 #等指定的元素出來之後,模擬點選選擇
        toStation = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#toStationText")))
        toStation.click()
        toStation.clear()
        toStation.send_keys('上海')
        tStation = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#citem_0 > span:nth-child(1)")))
        tStation.click()

第二步:搞定時間,在時間上我們可以讀取本地的時間,知道今天的時間,然後在出現的日曆框中輸入指定的月和日,定位可以看出CSS選擇器的規律,只要在child後給出相應的月和日的數值就能點選輸入對應的時間。到此可以寫一個設定時間的方法,方法帶有一個入參,入參為從今天的日期起算,往後推算第n天,入參便是這個n:

        css_month = 'body > div.cal-wrap > div:nth-child(1) > div.cal-top > div.month > ul > li:nth-child(%d)' % month
        css_day = 'body > div.cal-wrap > div:nth-child(1) > div.cal-cm > div:nth-child(%d) > div' % day

    def set_date(self, n):
        needDate = datetime.datetime.now() + datetime.timedelta(days = n)   #獲取今天的時間往後偏移n天      
        css_month = 'body > div.cal-wrap > div:nth-child(1) > div.cal-top > div.month > ul > li:nth-child(%d)' % needDate.month
        css_day = 'body > div.cal-wrap > div:nth-child(1) > div.cal-cm > div:nth-child(%d) > div' % needDate.day
        
        #點選選擇出發時間輸入框
        train_date = self.wait.until(EC.presence_of_element_located((By.ID, "train_date")))
        train_date.click()
        
        #點選左邊選擇月份表
        month_list = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'body > div.cal-wrap > div:nth-child(1) > div.cal-top > div.month > input[type="text"]')))
        month_list.click()
        month = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, css_month)))
        month.click()
        
        get_date_list = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, css_day)))
        get_date_list.click()
        self.click_query()
        return needDate

第三步:點選查詢按鈕等待資料的呈現:

    #點選查詢按鈕
    def click_query(self):
        self.query_ticket = self.wait.until(EC.presence_of_element_located((By.ID, "query_ticket")))
        self.query_ticket.click()

第四步:分析資料獲取資料,可以看到,tobody節點中的tr標籤都是展開都是我們需要的單次列車資訊,需要將這裡面的資料解析出來報文,列印,或者做其他的資料分析處理。這裡就可以使用BeautifulSoup解析庫處理,首先可以根據關鍵字tobody中的id屬性立刻定位到列表資訊的位置所在,然後將tobody中的每一個車次行資訊提取出來,送到單獨處理行資訊的方法中。在該方法中就可以將我們期望需要的資訊提取出來。

    #對每一列車次的資訊進行解析
    def get_detail_info(self, node):
        autoShift = {}
        data = []
        nodes = node.contents
        queryLeftTable = nodes[0].div.div.div.a.string   #車班次,為key
        autoShift[queryLeftTable] = list()
        strong = nodes[0].select('strong')
        for i in range(0,4):
            data.append(strong[i].string)

        for i in range(1,len(nodes)):
        #在node.contents時,列表中有很多空格,會導致異常發生,在之前將資料清洗這裡就可以不用異常判斷,這裡大家可以自己試試
            try:   
                if nodes[i].string != '--':
                    key = nodes[i].attrs['id'].split('_')[0]
                    str = self.ztype[nodes[i].attrs['id'].split('_')[0]] + ":" + nodes[i].string
                    data.append(str)
            except:
                pass
        autoShift[queryLeftTable] = data
        print(autoShift)
        
    #在網頁中找到一頁班次資訊,然後遍歷這些班次資訊
    def get_list(self, date):  
        print('\n', date, ':')
        info = list()        
        soup = BeautifulSoup(self.browser.page_source, 'lxml')
        soup = soup.select('#queryLeftTable')   #在整個網頁中找到該id
        queryLeftTable =  soup[0].select('tr[id^="ticket_"]')     #返回列表,列表每個內容為一個節點tag
        for tr in queryLeftTable:
            self.get_detail_info(tr)  #傳入每一列車次的資訊

在這裡我們沒有將資料儲存下來,只是將其打印出來看我們確實將這些資料得到了,本章做個保留,等到後面我們將資料進行可是化的時候,再回頭對這些資料做一些簡單的處理,並將資料以圖表的形式呈現出來,現將所有的程式碼和執行結果貼出來,寫的並不是完美的,大家可以將這些程式碼複製下來進行改動,達到完美:

from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from bs4 import BeautifulSoup
import io,sys, time, datetime, re

sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030') 

class Info12306(object):
    def __init__(self):
        #座位種類
        self.ztype = {"SWZ" : "商務特等座",
                     "ZY" : "一等座", 
                     "ZE" : "二等座", 
                     "GR" : "高階軟臥", 
                     "RW" : "軟臥", 
                     "SRRB" : "動臥",
                     "YW" : "硬臥", 
                     "RZ" : "軟座", 
                     "YZ" : "硬座", 
                     "WZ" : "無座", 
                     "QT" : "其他"}
        self.url = "https://kyfw.12306.cn/otn/leftTicket/init"
        self.info = {}
        self.browser = webdriver.Chrome()        #初始化瀏覽器 
        self.wait = WebDriverWait(self.browser, 5)
        
    def open_html(self):
        self.browser.get(self.url)
        
    def close_html(self):
        self.browser.close()
        
    #點選查詢按鈕
    def click_query(self):
        self.query_ticket = self.wait.until(EC.presence_of_element_located((By.ID, "query_ticket")))
        self.query_ticket.click()
     
    #點選離今天往後第n天的結果
    def set_date(self, n):
        needDate = datetime.datetime.now() + datetime.timedelta(days = n)   #獲取今天的時間往後偏移n天      
        css_month = 'body > div.cal-wrap > div:nth-child(1) > div.cal-top > div.month > ul > li:nth-child(%d)' % needDate.month
        css_day = 'body > div.cal-wrap > div:nth-child(1) > div.cal-cm > div:nth-child(%d) > div' % needDate.day
        
        #點選選擇出發時間輸入框
        train_date = self.wait.until(EC.presence_of_element_located((By.ID, "train_date")))
        train_date.click()
        
        #點選左邊選擇月份表
        month_list = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'body > div.cal-wrap > div:nth-child(1) > div.cal-top > div.month > input[type="text"]')))
        month_list.click()
        month = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, css_month)))
        month.click()
        
        get_date_list = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, css_day)))
        get_date_list.click()
        self.click_query()
        return needDate

    #選擇面板上的日期
    def select_date(self):
        action = ActionChains(self.browser)     #使用瀏覽器動作模擬庫
        move_to_date = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#date_range > ul > li:nth-child(3)")))
        action.move_to_element(move_to_date).perform()
        action.click(move_to_date).perform()
    
    #輸入出發地和目的地,可以手動新增地址
    def input_FromTo(self):
        fromStation = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#fromStationText")))
        fromStation.click()
        fromStation.clear()
        fromStation.send_keys('北京')
        fStation = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#citem_0 > span:nth-child(1)")))
        fStation.click()
        toStation = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#toStationText")))
        toStation.click()
        toStation.clear()
        toStation.send_keys('上海')
        tStation = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#citem_0 > span:nth-child(1)")))
        tStation.click()
        
    def get_detail_info(self, node):
        autoShift = {}
        data = []
        nodes = node.contents
        queryLeftTable = nodes[0].div.div.div.a.string   #車班次,為key
        autoShift[queryLeftTable] = list()
        strong = nodes[0].select('strong')
        for i in range(0,4):
            data.append(strong[i].string)

        for i in range(1,len(nodes)):
            try:
                if nodes[i].string != '--':
                    key = nodes[i].attrs['id'].split('_')[0]
                    str = self.ztype[nodes[i].attrs['id'].split('_')[0]] + ":" + nodes[i].string
                    data.append(str)
            except:
                pass
        autoShift[queryLeftTable] = data
        print(autoShift)
        
    def get_list(self, date):  
        print('\n', date, ':')
        info = list()        
        soup = BeautifulSoup(self.browser.page_source, 'lxml')
        soup = soup.select('#queryLeftTable')
        queryLeftTable =  soup[0].select('tr[id^="ticket_"]')     #返回列表,列表每個內容為一個節點tag
        for tr in queryLeftTable:
            self.get_detail_info(tr)
   
    def run(self):
        self.open_html()
        self.input_FromTo()
        for seq in range(0,1):
            date = self.set_date(seq)
            time.sleep(1)
            self.get_list(date)
        #self.close_html()
        
Get12306 = Info12306()
Get12306.run()
    

執行結果

小結:使用selenium的好處就是不需要分析後臺傳送的資料,因為後臺傳輸的資料有可能是加密,這樣的資料抓取了也是沒有用的,對於selenium來說這些都是不存的,在頁面可見即可抓,管你加密不加密,這就是selenium,當然這裡的解析庫也可以只用其他的,根據不同的場景選擇不同的解析庫有助於提高抓取效率。