大鍋在做外賣,給我說能否統計出這半年點餐次數最多的10個顧客,我不知道APP本身是否有這個功能,想了下最近用selenium較多,就用selenium嘗試下吧。
1 定義一個類,這里先描述需要的屬性和方法,后面再依次具體分析:

1 class Order: 2 def __init__(self, url, username, password): 3 # URL以及用戶名和密碼 4 self.url = url 5 self.username = username 6 self.password = password 7 # webdriver對象 8 self.driver = None 9 # 日期列表 10 self.dateList = list() 11 # 存儲訂單的dict 12 self.orderDict = dict() 13 14 # 設置訂單的時間范圍 15 def setDate(self, startdate, enddate): 16 pass 17 # 登錄 18 def login(self): 19 pass 20 # 退出 21 def logout(self): 22 pass 23 # 切換到歷史訂單頁面 24 def switchToHistoryOrder(self): 25 pass 26 # 選擇一個時間范圍 27 def selectDate(self, startDate, endDate): 28 pass 29 # 訂單信息存入self.orderDict 30 def saveOrderIntoDict(self, tel, name, address): 31 pass 32 # 處理當前頁面的訂單 33 def searchOrderListInCurrentPage(self): 34 pass 35 # 判斷是否還有下一頁 36 def hasNextPage(self): 37 pass 38 # 切換到下一頁 39 def enterNextPage(self): 40 pass 41 # 抓取設定日期范圍內的所有訂單 42 def getAllOrders(self, startdate, enddate): 43 pass 44 # 篩選出點餐次數排名前N的顧客 45 def getTopN(self, n=10): 46 pass
2 設置欲篩選的訂單日期范圍
這里設置的日期格式必須是'yyyy-mm-dd',由於該網站在查詢訂單的時候,時間范圍必須是7天以內,比如直接查詢2016-01-01到2016-02-28之間的訂單是不行的,因此需要先將這段時間以7天為周期分割為多個時間段,然后再分段處理;
分割后的時間段存放在self.dateList中,list的元素為tuple,一個tuple表示一個時間段:

1 # 設置訂單的時間范圍,日期格式必須是'yyyy-mm-dd' 2 # 然后以7天為周期,將日期范圍分割成list,list的每個元素為一個tuple,分別存放起止日期 3 # 例如 [('2016-01-01', '2016-01-07'), ('2016-01-08', '2016-01-14')] 4 def setDate(self, startdate, enddate): 5 # 通過正則表達式檢查日期格式 6 pdate = re.compile('\d{4}-\d{2}-\d{2}') 7 if pdate.search(startdate) and pdate.search(enddate): 8 # 轉換為datetime格式,便於日期計算 9 startdate = datetime.datetime.strptime(startdate, '%Y-%m-%d') 10 enddate = datetime.datetime.strptime(enddate, '%Y-%m-%d') 11 12 # 將日期范圍以7天為周期分割 13 days = (enddate - startdate).days + 1 14 cnt = days / 7 15 left = days - 7*cnt 16 for x in range(cnt): 17 d1 = (startdate + datetime.timedelta(days=7*x)) 18 d2 = (d1 + datetime.timedelta(days=6)) 19 # datetime轉換為str,再加入list中 20 self.dateList.append((d1.strftime('%Y-%m-%d'), d2.strftime('%Y-%m-%d'))) 21 if left > 0: 22 self.dateList.append(((startdate+datetime.timedelta(days=cnt*7)).strftime('%Y-%m-%d'), enddate.strftime('%Y-%m-%d'))) 23 else: 24 print u'日期格式錯誤,必須為yyyy-mm-dd格式' 25 exit(1)
測試一下:

1 order = Order('url', 'username', 'password') 2 order.setDate('2016-01-01', '2016-01-31') 3 print order.dateList 4 #輸出為 5 [('2016-01-01', '2016-01-07'), ('2016-01-08', '2016-01-14'), ('2016-01-15', '2016-01-21'), ('2016-01-22', '2016-01-28'), ('2016-01-29', '2016-01-31')]
3 登錄與退出
登錄比較簡單,直接過id定位到用戶名和密碼輸入框,然后定位登錄按鈕點擊登錄即可,只是定位到輸入框后需要先將框內的提示信息清除掉。
退出就更簡單了,直接關閉瀏覽器即可。

1 # 登錄 2 def login(self): 3 # 采用chrome瀏覽器 4 self.driver = webdriver.Chrome() 5 # 窗口最大化 6 self.driver.maximize_window() 7 # 設置超時時間 8 self.driver.implicitly_wait(10) 9 self.driver.get(self.url) 10 11 # 查找用戶名輸入框,先清除提示信息,再輸入用戶名 12 usr = self.driver.find_element_by_id('account-name') 13 usr.clear() 14 usr.send_keys(self.username) 15 16 # 查找密碼輸入框,先清除提示信息,再輸入密碼 17 passwd = self.driver.find_element_by_id('account-password') 18 passwd.clear() 19 passwd.send_keys(self.password) 20 21 # 點擊登錄 22 self.driver.find_element_by_id('account-login-btn').click() 23 return 24 25 # 退出 26 def logout(self): 27 self.driver.close() 28 return
4 切換到歷史訂單頁面
登錄后點擊訂單管理,然后點擊歷史訂單,切換到歷史訂單頁面,如下圖所示:
由於“訂單管理”和“歷史訂單”這兩個元素都是超鏈接,因此可以直接用超鏈接定位:

1 # 切換到歷史訂單頁面 2 def switchToHistoryOrder(self): 3 self.driver.find_element_by_partial_link_text(u'訂單管理').click() 4 self.driver.find_element_by_partial_link_text(u'歷史訂單').click() 5 6 # 切換frame,因為后續的所有處理都是在hashframe中,所有在這里切換 7 self.driver.switch_to.frame('hashframe') 8 return
注意該方法最后有個切換frame的操作,下一步就知道為什么要添加這句了。
5 選擇訂單日期范圍,篩選出該日期范圍內的訂單
注意這里的日期范圍與第二步設置的日期范圍不一樣,第二步設置的日期范圍是我們想要篩選的起止時間,這里的日期范圍是第二步分割出來的其中一段。
與用戶名密碼框一樣,這里的日期輸入框也可采用id定位,同樣定位后需要先將輸入框預置的日期清除:

1 # 在頁面上設置訂單的時間范圍,並篩選出該時間范圍內的所有訂單 2 # 調用該方法時,參數需從self.dateList中獲取,dateList中的每一個tuple對應一組參數 3 def selectDate(self, startDate, endDate): 4 # 設置起始日期 5 s = self.driver.find_element_by_id('J-start') 6 s.clear() 7 s.send_keys(startDate) 8 9 # 設置終止日期 10 e = self.driver.find_element_by_id('J-end') 11 e.clear() 12 e.send_keys(endDate) 13 return
這里說一下第4步中的切換frame,我們先將切換frame這一句注釋掉,然后來測試下選擇日期:

1 order.login() 2 order.switchToHistoryOrder() 3 order.selectDate('2016-07-01', '2016-07-02') 4 # 輸出 5 selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"id","selector":"J-start"}
可以看到出錯了,提示沒有需要定位的元素,為什么呢?通過HTML代碼可以看到,這里采用了框架frame,所定位的元素在frame里面,如下所示:
因此需要先切換到該frame里面,然后才能定位到frame里面的元素;由於后續所有的訂單操作都在該frame里面,因此在上一步切換到歷史訂單頁面后,就先切換到該frame,便於后續操作。
6 將訂單信息存入self.orderDict
該方法是下一步需要調用的,因此這里先實現。每一個訂單我們只需3個信息:姓名、電話、地址,然后將這三個元素表示的訂單存入orderDict,但是由於我們要統計出點餐次數最多的10個顧客,
因此還要保存每個顧客的點餐次數。這里dict的元素格式為{'tel': ['name', 'address', cnt]},由於姓名可能重復,因此采用了電話作為key值。如果某個顧客第一次點餐,保存時將點餐次數cnt初始化為1;
如果不是第一次點餐,則將該顧客對應的cnt值加1。

1 # 將tel, name, address表示的訂單信息存入self.orderDict 2 # self.orderDict元素的形式為 {'tel': ['name', 'address', cnt]} 3 # 以電話號碼為key,以姓名、地址、點餐次數組成的list為value 4 # 當要添加的key不存在時,就將此訂單加入orderDict,並且cnt初始化為1 5 # 當要添加的key已經存在時,直接將該key對應的cnt加1 6 def saveOrderIntoDict(self, tel, name, address): 7 if self.orderDict.has_key(tel): 8 self.orderDict[tel][2] += 1 9 else: 10 self.orderDict[tel] = [name, address, 1] 11 return
7 提取當前頁面的所有訂單,存入self.orderDict
先來分析下訂單部分的HTML代碼,如下所示:
簡單說就是:用<ul>表示該頁的所有訂單,<ul>下面的每一個<li>表示一個具體訂單,<li>下面的類型為user-info的<div>表示該訂單的用戶信息,<div>下面就是姓名電話等信息。
因此定位過程如下:先用find_elements方法找到<ul>下面的所有<li>;然后遍歷每一個<li>,定位用戶信息;最后再從用戶信息中定位姓名電話等。這里用到了層級定位。

1 # 抓取當前頁面的所有訂單,並調用saveOrderIntoDict將每個訂單信息都存入self.orderDict 2 def searchOrderListInCurrentPage(self): 3 # 這里采用CSS定位,但是使用了find_elements,一次定位所有的訂單 4 orders = self.driver.find_elements_by_css_selector('ul.order-list.J-order-list > li') 5 6 # 遍歷每一個訂單,提取其中的用戶信息,這里用到了層級定位 7 for one in orders: 8 # 先定位用戶信息 9 uesrinfo = one.find_element_by_css_selector('div.user-info') 10 # 再定位具體每項信息 11 name = uesrinfo.find_element_by_css_selector('span.b-title.user-name').text 12 # 有的用戶沒有寫名字 13 if name == '': 14 name = u'無名氏' 15 tel = uesrinfo.find_element_by_css_selector('span.b-title.user-phone.J-search-phone').text 16 address = uesrinfo.find_element_by_css_selector('div > span.fl.J-search-address').text 17 # 信息存入self.orderDict 18 self.saveOrderIntoDict(tel, name, address) 19 return
8 判斷是否還有下一頁訂單
這里使用頁面低端的翻頁符號來定位,如下圖所示。
實現思路可參考 使用selenium實現簡單網絡爬蟲抓取MM圖片 ,檢查是否到達頁面底部的方法:

1 # 判斷是否還有下一頁 2 # 通過符號>>的上級標簽<li>的class屬性來判斷,當還存在下一頁(>>可點擊)時,<li>的class屬性值為空;當不存在下一頁時,<li>的class屬性值為disabled 3 # 因此,當我們找到符號>>及其父元素<li class="disabled">時,即可認為不存在下一頁,否則存在下一頁 4 def hasNextPage(self): 5 try: 6 # 注意這里的選擇器一定要包含a[aria-label="Next"],不能只用li.disabled,因為上一頁的符號也可能存在li.disabled,必須使用父子元素同時定位 7 self.driver.find_element_by_css_selector('ul.J-pagination.pagination.pagination-md.pull-right > li.disabled > a[aria-label="Next"]') 8 # 如果沒拋出異常,說明找到了元素<li class="disabled">和子元素<a aria-label="Next">,沒有下一頁了 9 return False 10 except NoSuchElementException as e: 11 # 拋出異常說明還存在下一頁 12 return True
9 切換到下一頁訂單
切換很簡單,直接定位翻頁符號點擊即可:

1 # 切換到下一頁 2 # 這個比較簡單,直接采用類似上面的定位器即可 3 def enterNextPage(self): 4 self.driver.find_element_by_css_selector('ul.J-pagination.pagination.pagination-md.pull-right a[aria-label="Next"] > span').click() 5 return
10 抓取設置日期范圍內的所有訂單
上面的方法都是些內部實現,無需直接調用,而該方法是供我們直接調用的,這里只是對上面那些方法的組合以及簡單處理:

1 # 抓取設定日期范圍內的所有訂單,抓取訂單時直接調用該接口即可 2 def getAllOrders(self, startdate, enddate): 3 # 設置訂單的時間范圍 4 self.setDate(startdate, enddate) 5 6 # 登錄 7 self.login() 8 9 # 切換到歷史訂單頁面 10 self.switchToHistoryOrder() 11 12 # 遍歷處理dateList中的所有日期對 13 for date in self.dateList: 14 # 選擇date所標示的時間范圍 15 self.selectDate(startDate=date[0], endDate=date[1]) 16 17 # 依次處理篩選出來的每一頁訂單 18 while True: 19 # 處理當前頁面的訂單 20 self.searchOrderListInCurrentPage() 21 # 是否還有下一頁 22 if self.hasNextPage(): 23 self.enterNextPage() 24 else: 25 break 26 27 # 退出 28 self.logout() 29 return
11 篩選出點餐次數TOP N的顧客
調用第10步的方法后,所有的訂單信息都在self.orderDict中了,這里只需將dict轉換為list,然后按照cnt降序排序,再輸出前10的信息即可。

1 # 篩選出點餐次數排名前N的顧客 2 def getTopN(self, n=10): 3 # 先將orderDict轉換為list 4 # {'tel': ['name', 'address', cnt]} -> [('name', 'tel', 'address', cnt)] 5 orderList = [(v[0], k, v[1], v[2]) for (k, v) in self.orderDict.iteritems()] 6 7 # 按照list中每個元素的cnt排序 8 orderList.sort(key=lambda x: x[3], reverse=True) 9 10 # 輸出TOP N 11 num = len(orderList) 12 if num < n: 13 n = num 14 for i in range(n): 15 print orderList[i][0] 16 print orderList[i][1] 17 print orderList[i][2] 18 print orderList[i][3]
現在測試一下整個代碼:

1 if __name__ == '__main__': 2 order = Order('url', 'username', 'password') 3 # 抓取制定時間范圍內的所有訂單 4 order.getAllOrders('2016-05-23', '2016-05-25') 5 # 輸出TOPN 6 order.getTopN(n=10)
輸出形式如下:

1 姓名1 2 13512345678 3 地址1 4 6 5 姓名2 6 13612345678 7 地址2 8 3 9 姓名3 10 13712345678 11 地址3 12 1