实战 | 如何用 Python 统计 Jira 数据并可视化

大家好,我是安果!

目前公司使用 Jira 作为项目管理工具,在每一次迭代完成后的复盘会上,我们都需要针对本次迭代的 Bug 进行数据统计,以帮助管理层能更直观的了解研发的代码质量

本篇文章将介绍如何利用统计 Jira 数据,并进行可视化

1. 准备

首先,安装 Python 依赖库

  1. # 安装依赖库
  2. pip3 install jira
  3. pip3 install html-table
  4. pip3 install pyecharts
  5. pip3 install snapshot_selenium

其中

  • jira 使用 jsql 语法从在项目中获取需要的数据

  • html-table 用于生成一个 HTML 格式的表格数据

  • pyecharts 和 snapshot_selenium 用于数据可视化

2. 实战一下

下面我们通过 7 个步骤来实现上面的功能

2-1 登录获取客户端连接对象

  1. from jira import JIRA
  2. class JiraObj(object):
  3.     def __init__(self, bug_style, project_type):
  4.         """
  5.         :param project_name
  6.         :param sprint: 迭代号码
  7.         :param bug_style: BUG状态
  8.         """
  9.         # Jira首页地址
  10.         self.server = 'https://jira.**.team'
  11.         # Jira登录账号信息
  12.         self.basic_auth = ('用户名''密码')
  13.         # 创建一个客户端连接信息
  14.         self.jiraClinet = JIRA(server=self.server, basic_auth=self.basic_auth)

2-2 根据项目类型获取看板 id

  1. ...
  2.         # 获取boards看板
  3.         # 所有看板信息
  4.         boards = [(item.id, item.name) for item in self.jiraClinet.boards()]
  5.         board_id = self.__get_board_id(boards, project_type)
  6.         print("看板id:", board_id)
  7. ...
  8.     def __get_board_id(self, boards, project_type):
  9.         """
  10.         获取看板id
  11.         :param project_type:
  12.         :return:
  13.         """
  14.         board_id = 1
  15.         for item in boards:
  16.             if (project_type == PROJ_TYPE.Type1 and item[1] == 't1') or (
  17.                     project_type == PROJ_TYPE.Type2 and item[1] == 't2'):
  18.                 board_id = item[0]
  19.                 break
  20.         return board_id
  21. ..

2-3 根据看板 id 获取迭代 id 及迭代名称

  1. ...
  2.  # 获取项目Sprint,让用户进行选择
  3.         sprints = self.jiraClinet.sprints(board_id=board_id)
  4.         for item in sprints:
  5.             if str(sprint_no) in item.name:
  6.                 self.sprint_id = item.id
  7.                 self.sprint_name = item.name
  8.                 print(f"选择Sprint,id:{self.sprint_id},name:{self.sprint_name}")
  9.                 break
  10. ...

2-4  根据项目名、Bug 类型、迭代 id 组成 jsql 语句,并查询数据

  1. ...
  2.  def get_bug_status_jsql(self, bug_status: BUG_STATUS):
  3.         """
  4.         通过bug状态,获取jsql
  5.         :param bug_status:
  6.         :return:
  7.         """
  8.         status_jsql = ''
  9.         if bug_status == BUG_STATUS.ALL:
  10.             status_jsql = ' '
  11.         elif bug_status == BUG_STATUS.TO_VERIFY:
  12.             # 待验证(已解决)
  13.             status_jsql = ' AND status = 已解决 '
  14.         elif bug_status == BUG_STATUS.TO_FIXED:
  15.             # 待解决(打开、重新打开、处理中)
  16.             status_jsql = ' AND status in (打开, 重新打开, 处理中) '
  17.         elif bug_status == BUG_STATUS.CLOSED:
  18.             # 关闭
  19.             status_jsql = ' AND status = Closed '
  20.         elif bug_status == BUG_STATUS.TO_FIXED_CONTAIN_DELAY:
  21.             # 待解决(打开、重新打开、处理中、延期处理)
  22.             status_jsql = ' AND status in (打开, 延期处理, 重新打开, 处理中) '
  23.         return status_jsql
  24. ...
  25. jql = f'project = {project_name} and issuetype = 故障  {self.get_bug_status_jsql(self.bug_style)} 
  26. AND Sprint = {self.sprint_id} ORDER BY priority desc, updated DESC'
  27.         print(jql)
  28.         lists = self.get_issue_list(jql)
  29. ...

2-5  生成本地 HTML 统计数据






需要注意的是,使用 a 标签组装的链接不能直接跳转,需要针对数据进行二次替换才能正常进行链接跳转 

  1. from HTMLTable import (
  2.     HTMLTable
  3. )
  4. ...
  5.  def gen_html_table(self, datas):
  6.         """
  7.         初始化表单样式
  8.         :return:
  9.         """
  10.         table = HTMLTable(caption=f'实时BUG统计【{self.project_name}】,一共{len(datas)}个')
  11.         # 表头行
  12.         table.append_header_rows((('ID''状态''优先级''责任人''终端''URL'),))
  13.         # 添加数据
  14.         table.append_data_rows(datas)
  15.         # 设置样式
  16.         table.caption.set_style({'font-size''15px'})
  17.         # 其他样式设置
  18.         ...
  19.         # 替换数据,便于展示href地址
  20.         html = table.to_html().replace("&lt;""<").replace("&gt;"">").replace("&quot;"'"')
  21.         with open(f"./output/{self.project_name}-bug_{current_time()}.html"'w', encoding='utf-8'
  22. as file:
  23.             file.write(html)
  24. ...
  25. # 生成本地文件的数据
  26. output_tuples = tuple([
  27.             (item.get("key"), item.get("status"), item.get("priority"), item.get('duty'), 
  28. item.get('end_type'),
  29.              f'<a href="{item.get("url")}" target="_blank">点我查看</a>'for item in lists])
  30. # 生成本地HTML文件
  31. self.gen_html_table(output_tuples)
  32. ..

2-6 数据统计

首先,这里按 Bug 责任人进行分组,然后按数目进行降序排列

然后,按 Bug 优先等级进行降序排列

最后,获取每一个端的 Bug 总数

  1. ...
  2.         # 2、统计每个人(按数目)
  3.         datas_by_count = {}
  4.         for item in lists:
  5.             datas_by_count[item.get("duty")] = datas_by_count.get(item.get("duty"), 0) + 1
  6.         # 降序排序
  7.         datas_by_count = sorted(datas_by_count.items(), key=lambda item: item[1], reverse=True)
  8.         # print("按Bug总数排序:", datas_by_count)
  9.         # 3、统计每个人(按优先级)
  10.         datas_by_priority = {}
  11.         for item in datas_by_count:
  12.             # 责任人
  13.             name = item[0]
  14.             # 5个优先级对应的数目
  15.             counts = self.get_assignee_count(lists, name)
  16.             datas_by_priority[name] = counts
  17.         # 排序(按优先级多条件降序排列)
  18.         datas_by_priority = sorted(datas_by_priority.items(),
  19.                                    key=lambda item: (item[1][0], item[1][1], 
  20. item[1][2], item[1][3]), reverse=True)
  21.         # print("按Bug优先级排序:", datas_by_priority)
  22.         # 4、根据终端进行统计分类
  23.         keys, values = self.get_end_type_count(lists)
  24. ...

2-7 可视化

针对上面的 3 组数据,使用 pyecharts 绘制成柱状图和饼状图

  1. ...
  2.       def draw_image(self, datas_by_count, datas_by_priority, keys, values):
  3.         """
  4.         绘制图片
  5.         :param values:
  6.         :param keys:
  7.         :param datas_by_count: 按bug总数排序结果
  8.         :param datas_by_priority: 按bug优先级排序结果
  9.         :return:
  10.         """
  11.         # 1、按BUG总数排序绘制
  12.         bar = (
  13.             Bar().set_global_opts(
  14.                 title_opts=opts.TitleOpts(title=f"{self.project_name}"
  15. subtitle=f"{self.sprint_name}")))
  16.         bar.add_xaxis([item[0for item in datas_by_count])
  17.         bar.add_yaxis(f"BUG总数", [item[1for item in datas_by_count])
  18.         # render 会生成本地 HTML 文件,默认会在当前目录生成 render.html 文件
  19.         # 也可以传入路径参数,如 bar.render("mycharts.html")
  20.         # bar.render(path=f'{sprint_name}-BUG总数.html')
  21.         make_snapshot(snapshot, bar.render(), "./output/1.png")
  22.         # 2、按优先级排序绘制
  23.         bar2 = (
  24.             # Bar(init_opts=opts.InitOpts(theme=ThemeType.INFOGRAPHIC))
  25.             Bar()
  26.                 .add_xaxis([item[0for item in datas_by_priority])
  27.                 .add_yaxis(self.__get_priority(BUG_PRIORITY.Highest), 
  28. [item[1][0for item in datas_by_priority],
  29.                            color='#6aa84f')
  30.                 .add_yaxis(self.__get_priority(BUG_PRIORITY.High), 
  31. [item[1][1for item in datas_by_priority],
  32.                            color='#a2c4c9')
  33.                 .add_yaxis(self.__get_priority(BUG_PRIORITY.Medium), 
  34. [item[1][2for item in datas_by_priority],
  35.                            color="#ff9900")
  36.                 .add_yaxis(self.__get_priority(BUG_PRIORITY.Low), 
  37. [item[1][3for item in datas_by_priority],
  38.                            color="#ea9999")
  39.                 .add_yaxis(self.__get_priority(BUG_PRIORITY.Lowest), 
  40. [item[1][4for item in datas_by_priority],
  41.                            color="#980000")
  42.                 .set_global_opts(
  43.                 title_opts=opts.TitleOpts(title=f"{self.project_name}"
  44. subtitle=f"{self.sprint_name}"))
  45.         )
  46.         # bar2.render(path=f'{sprint_name}-BUG优先级.html')
  47.         make_snapshot(snapshot, bar2.render(), "./output/2.png")
  48.         # 3、根据终端来绘制饼图
  49.         if len(keys) > 0 and len(values) > 0:
  50.             c = (
  51.                 Pie()
  52.                     .add("", [list(z) for z in zip(keys, values)])
  53.                     .set_global_opts(title_opts=opts.TitleOpts(title="各端BUG分布"))
  54.                     .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c}"))
  55.             )
  56.             make_snapshot(snapshot, c.render(), f"./output/{self.project_name}_end.png")
  57.         # 4、合并两张图片
  58.         self.concatenate_img(['./output/1.png''./output/2.png'], 
  59. img_name=f'./output/{self.sprint_name}_bug.png',
  60.                              axis=1)
  61. ...

3. 总结

通过上面的操作,每次只需要输入项目类型、迭代版本号、要统计的 Bug 类型,就能统计出所需要的数据并绘制成图表


作者:星安果


欢迎关注微信公众号 :AirPython