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,當然這裡的解析庫也可以只用其他的,根據不同的場景選擇不同的解析庫有助於提高抓取效率。