使用selenium找出外卖点餐次数最多的10个顾客
大锅在做外卖,给我说能否统计出这半年点餐次数最多的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