这个想法源于在公司工作时,需要每天关注bug的情况,每次在页面上做条件删选比较麻烦,而且不够直观,无法保存过滤的数据。
接下来要明确这个程序需要实现的功能:
- 最基本的,禅道的数据爬取
- 爬取的数据进行存储,并且生成直观的图表
- 可以将爬取的数据发送到钉钉的工作群中
- 可以定时执行,比如每天上班前,或者下班后,自动执行
首先是数据的爬取:
禅道有多个bug分类页面,我的程序就是将每个页面的bug爬取,并对这些数据进行分析。这里我以所有为例
爬虫所用到的内容很简单,就是通过selenium将页面的HTML代码爬取,然后通过对html代码进行解析,得到我们所需要的数据:
1 import time 2 from lxml import etree 3 from selenium import webdriver 4 from datetime import datetime 5 from selenium.webdriver.common.by import By 6 7 import weekday 8 import csv 9 from selenium.webdriver.support.ui import WebDriverWait 10 from selenium.webdriver.support import expected_conditions as EC 11 from ImageCreate import Project_Status_Image, Daily_Bugs
用到的包如上,etree为解析HTML所需,WebDriverWait为显示等待所需。
然后,使用执行selenium代码可以分为显式运行和静默执行,这里我为了让程序执行在Linux服务器上,而Linux服务器是没有图形化界面的,所以我这里使用了静默执行的方式,当然在程序还没有完善之前是需要图像界面来调试的:
1 # 设置selenium静默执行 2 chrome_options = webdriver.ChromeOptions() 3 chrome_options.headless = True 4 5 # 在Linux中运行selenium中进行必要的设置 6 chrome_options.add_argument('--no-sandbox') 7 chrome_options.add_argument('--disable-gpu') 8 chrome_options.add_argument('--disable-dev-shm-usage') 9 10 11 driver = webdriver.Chrome(options=chrome_options) 12 13 # driver = webdriver.Chrome() 14 driver.maximize_window()
headless就是设置静默执行,此外还有一些options是运行在Linux上必须设置的属性,然后为了元素定位方便,需要让浏览器全屏。
接下来就是selenium的元素操作,登录禅道,进入bug的所有页面。
在爬取页面数据时,如果一个页面的数据爬取完了,那我们要点击下一页来进入下一页,但是我们并不是要获取所有的bug信息,我们应该只是要获取某一部分,比如前20页,或者最近一周的数据,所以,在进行翻页或者数据爬取的过程我们需要做一个判断,看当前的数据是不是我们想要的。这里我想获取的当前周的bug情况,这里我根据bug的创建日期属性判断是否进行爬取:
1 ext = True 2 while next: 3 response = etree.HTML(driver.page_source) 4 5 ths = response.xpath("//table[@id='bugList']/thead/tr/th") # 这里是标题行 6 keys = [] 7 for th in ths[:-1]: 8 title = th.xpath("@title")[0] 9 print(title) 10 keys.append(title) 11 # print('keys:', keys) 12 13 trs = response.xpath("//table[@id='bugList']/tbody/tr") # 这里是bug记录行 14 print('tr存在%s' % len(trs)) 15 16 nums = len(keys) 17 print('nums', nums) 18 19 for tr in trs: 20 tds = tr.xpath("./td") 21 values = [] # 保存一条bug记录值 22 for td in tds[:nums]: 23 if tds.index(td) == keys.index('ID'): 24 content = td.xpath(".//a/text()")[0] 25 if tds.index(td) == keys.index('级别'): 26 content = td.xpath(".//span/@data-severity")[0] 27 if tds.index(td) == keys.index('P'): 28 content = td.xpath("./span/text()")[0] 29 if tds.index(td) == keys.index('确认'): 30 content = td.xpath("./span/text()")[0] 31 if tds.index(td) == keys.index('Bug标题'): 32 content = td.xpath(".//a/text()")[0] 33 if tds.index(td) == keys.index('所属项目'): 34 try: 35 content = td.xpath("./text()")[0] 36 except: 37 content = '' 38 if tds.index(td) == keys.index('状态'): 39 content = td.xpath(".//span/text()")[0] 40 # if tds.index(td) == keys.index('相关需求'): 41 # try: 42 # content = td.xpath(".//a/text()")[0] # 这个字段有可能为空的情况 43 # except: 44 # content = '' 45 if tds.index(td) == keys.index('创建者'): 46 content = td.xpath("./text()")[0] 47 if tds.index(td) == keys.index('创建日期'): 48 content = td.xpath("./text()")[0] 49 if tds.index(td) == keys.index('指派给'): 50 content = td.xpath(".//span/text()")[0] 51 if tds.index(td) == keys.index('解决日期'): 52 content = td.xpath("./text()")[0] 53 54 values.append(content) 55 bug_dict = dict(zip(keys, values)) 56 print(bug_dict) 57 if bug_dict['创建日期'].split(' ')[0] in weekday.get_week_day()['list']: # 获取当前周的日期 58 bug_list.append(bug_dict) 59 else: 60 next = False 61 break
思路就是在大循环外设置一个标志变量,如果为True,就继续爬,如果不是想要的数据,就设置为False,程序就不会继续爬取了。
这里我在静默模式下运行时,本来没问题的程序报错了,大致意思就是元素定位不到。但是在图形界面运行是没问题的,后来排查发现,通过排除法,断定这是因为在静默模式下,selenium在爬取数据时不会自动滚动到数据的位置(这点在图形界面是可以的),导致下一页按钮虽然获取到了,但是无法点击。
解决办法就是我们通过代码让它滚动到页面底部:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") # 操作页面滚动到底部
1 next_click = WebDriverWait(driver, 10, 0.5).until( 2 EC.presence_of_element_located((By.XPATH, "//*[@id='bugForm']/div[3]/ul/li[6]/a"))) # 显示等待 3 print('next:', response.xpath("//*[@id='bugForm']/div[3]/ul/li[6]/a/@title")) 4 5 # 如果没有这个滚动到底部的操作,selenium静默执行时就会报这个元素不是可操作的 6 driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") # 操作页面滚动到底部 7 try: 8 # webdriver.ActionChains(driver).move_to_element(next_click).click(next_click).perform() # 静默模式使用这种方式点击元素 9 10 next_click.click() 11 print('点击了下一页') 12 except Exception as e: 13 print('没有下一页了') 14 print(e) 15 break
在进行数据处理时,为了方便管理,我将之前爬取的所有bug的数据都存储到了一个列表中,列表的每个元素都是一个bug信息的字典,[{},{},{}]这种数据类型。
这样,我在后面进行数据分类时只需要将整个列表过一遍,将符合某一要求的数据添加到某一个列表中,就可以只遍历一遍这个列表完成多种分类。
1 project = {} # 项目bug集合 2 status = {} # bug状态集合 3 project_stauts = {} # 项目bug状态集合 4 project_bugs = {} 5 daliy_bugs = {} 6 7 today_weekday_num = weekday.get_week_day()['today_weekday_num'] 8 until_today_weekday = [i for x in weekday.get_week_day()['list'][:today_weekday_num + 1]] 9 weekdays = weekday.get_week_day()['list'] 10 for i in weekdays: 11 daliy_bugs[i] = 0 12 13 today_bug_num = 0 14 for bug in bug_list: 15 if bug_s in bug['创建日期']: 16 today_bug_num += 1 17 if bug['所属项目'] in project: 18 project[bug['所属项目']].append(bug) 19 else: 20 project[bug['所属项目']] = [] 21 project[bug['所属项目']].append(bug) 22 23 # if bug['创建日期'].split(' ')[0] in daliy_bugs: 24 # daliy_bugs[bug['创建日期'].split(' ')[0]] += 1 25 # else: 26 # daliy_bugs[bug['创建日期'].split(' ')[0]] = 1 27 28 daliy_bugs[bug['创建日期'].split(' ')[0]] += 1 29 30 # if ('%s-%s' % (bug['所属项目'], bug['状态'])) in project_stauts: 31 # project_stauts['%s-%s' % (bug['所属项目'], bug['状态'])] += 1 32 # else: 33 # project_stauts['%s-%s' % (bug['所属项目'], bug['状态'])] = 1 34 35 ps = project.keys() 36 for p in ps: 37 bs = project[p] 38 status = {'激活': 0, '已解决': 0, '已关闭': 0} # 设置每个状态初始值为0 39 for b in bs: 40 # if b['状态'] in status: 41 # status[b['状态']] += 1 42 # else: 43 # status[b['状态']] = 1 44 # if b['状态'] in status: 45 status[b['状态']] += 1 46 # else: 47 # status[b['状态']] = 1 48 49 print('%sbug状态分布' % p, status) 50 Project_Status_Image(p, status)
可以看到这里我调用了Project_Status_Image这个类,这个类的作用就是根据数据生成图表,是一个绘图的功能。
绘图我使用的是matplotlib中的pyplot,绘制条形图,绘图的细节这里不在细说:
1 import matplotlib.pyplot as plt 2 3 images_name = [] 4 5 6 def Project_Status_Image(p, status): 7 plt.rcParams['font.sans-serif'] = ['SimHei'] 8 plt.rcParams['axes.unicode_minus'] = False 9 plt.rcParams['font.size'] = 13 10 # plt.autoscale(enable=True, axis='y', tight=True) # tight layout 11 12 plt.figure(figsize=(15, 13)) 13 x = list(status.keys()) 14 y = list(int(i) for i in status.values()) 15 16 # align = 'center'指定x轴上对齐方式可为center edge 17 plt.bar(x, y, width=0.5, bottom=0, align='center', color='steelblue', edgecolor='w', linewidth=2) 18 19 # x轴斜体 20 # plt.xticks(rotation=30) 21 22 # 设置y轴为整数 23 yint = [] 24 locs, labels = plt.yticks() 25 for each in locs: 26 yint.append(int(each)) 27 plt.yticks(yint) 28 29 # 绘制标题 30 plt.title(p, size=26) 31 32 # 设置轴标签 33 plt.xlabel('状态', size=24) 34 plt.ylabel('BUG数', size=24) 35 plt.savefig(r'D:\Python_projects\test\images\%s.png' % p, ) 36 name = '%s.png' % p 37 images_name.append(name) 38 plt.show()
将图片绘制好并且保存在本地。
此时程序已经实现了爬取并分析数据的功能。
本地调试通过后可以将代码移植到服务器上调试。可看上一篇操作:https://www.cnblogs.com/x991788x/p/16588387.html
其实在服务器上调试程序所出现的问题远不止这些,记录有限,碰到问题的时候能分析解决就行。
到这一步,我想的应该是如何将数据发送到钉钉的群里。
通过钉钉机器人那不用说了。可以参考钉钉自定义机器人的文档:https://open.dingtalk.com/document/robots/custom-robot-access
读懂文档后我们依葫芦画瓢,写一个简单的demo,消息发送成功。
普通的text类型是比较简单的,但是我们要发送的图片类型的文件,经过官方认定,钉钉机器人是不能发送文件的,图片也是文件,那不是发送不了?后来我看到钉钉开发文档里的Markdown消息类型是附带图片的,那Markdown为啥能够发送图片呢?原因就是Markdown中的图片并不是一个文件,而是一个图片链接,钉钉机器人通过加载这个链接,将图片显示出来。
那思路就清晰了,我将本地统计生成的图片上传到网上,然后生成链接地址,再将这个地址嵌入Markdown消息中就可以了。
那么怎么将本地的图片上传到网上并获得链接呢?
看网上解决办法说是将图片上传到gitee仓库,这个应该可行,但我之前用Django写项目的时候用到过一个网站七牛云,这个网站可以上传图片并通过链接查看到这个图片。
使用七牛云存储的时候需要注意,我们创建的对象存储空间必须设置为公共空间,这样才可以通过链接加载这个图片。其次就是一大堆的配置了,这个七牛云做的很好,他们官网有方便的python版本的SDK,我们本地只需要pip install qiniu就可以调用SDK中封装的方法,非常简单:https://developer.qiniu.com/kodo/1242/python
因为的程序每次生成的图片每次都是一样的,这样如果上传相同名字的文件是上传不上去的,所以我先调用七牛云的删除方法清除这些名字的图片,然后再上传最新的图片。因为七牛云的上传接口上传成功后没有返回文件的链接,但是我们可以访问空间链接,让后将图片名称拼接到后面就能获得图片的链接了:
1 # 构建鉴权对象 2 q = Auth(access_key, secret_key) 3 4 # 要上传的空间 5 bucket_name = 'bugs-images' 6 7 for i in images_name: 8 # 上传后保存的文件名 9 key = i 10 11 # 生成上传 Token,可以指定过期时间等 12 token = q.upload_token(bucket_name, key) 13 14 # 要上传文件的本地路径 15 localfile = './images/%s' % i 16 17 ret, info = put_file(token, key, localfile, version='v2') 18 print(info) 19 assert ret['key'] == key 20 assert ret['hash'] == etag(localfile) 21 22 img_url = 'http://rgnc4j2kz.sabkt.gdipper.com/' + key 23 image_url_li.append(img_url)
这样获取图片链接后,我们再通过钉钉机器人的接口,将图片链接包含其中,发送Markdown类型消息:
1 image_urls = '' 2 for i in image_url_li: 3 image_urls = image_urls + '> ![screenshot](%s)\n' % i 4 5 data = { 6 "msgtype": "markdown", 7 "markdown": { 8 "title": "杭州天气", 9 # 这里的%需要用%%转义 10 "text": "杭州天气 @150XXXXXXXX \n > 9度,西北风1级,空气良89,相对温度73%%\n > %s >10点20分发布 [天气](https://www.dingtalk.com) \n" % image_urls 11 # "text": "杭州天气 @150XXXXXXXX \n > 9度,西北风1级,空气良89,相对温度73%\n > <img src='D:\图片\pest.jpeg'>\n >10点20分发布 [天气](https://www.dingtalk.com) \n" 12 }, 13 'at': {'atMobiles': [], 'atUserIds': [], 'isAtAll': 'false'} 14 } 15 response = requests.post(url=complete_url, data=json.dumps(data), headers=headers) 16 print(response.text)
这样就实现了钉钉机器人发送图片的功能,同样,实现这部后,也可以将代码同步到服务器进行调试。
最后就是在服务器定时执行了。其实定时执行有两种方式,一个是在windows本地定时运行,一个是在服务器定时运行。在本地运行的话通过windows自带的定时任务功能就能实现,但是必须保证电脑不关机,不睡眠。所以还是让程序在服务器上跑比较方便。
Linux上定时执行程序使用的是cron,使用起来也非常简单:http://t.zoukankan.com/felixzh-p-4950437.html
需要注意的是我们要运行py文件的时候,因为要执行多个命令,命令之间需要用;隔开
标签:xpath,index,keys,爬取,td,content,bug,禅道 From: https://www.cnblogs.com/x991788x/p/16597043.html