實(shí)戰(zhàn) | 如何用 Python 統(tǒng)計(jì) Jira 數(shù)據(jù)并可視化
大家好,我是安果!
目前公司使用 Jira 作為項(xiàng)目管理工具,在每一次迭代完成后的復(fù)盤會(huì)上,我們都需要針對(duì)本次迭代的 Bug 進(jìn)行數(shù)據(jù)統(tǒng)計(jì),以幫助管理層能更直觀的了解研發(fā)的代碼質(zhì)量
本篇文章將介紹如何利用統(tǒng)計(jì) Jira 數(shù)據(jù),并進(jìn)行可視化
1. 準(zhǔn)備
首先,安裝 Python 依賴庫(kù)
- # 安裝依賴庫(kù)
- pip3 install jira
- pip3 install html-table
- pip3 install pyecharts
- pip3 install snapshot_selenium
其中
jira 使用 jsql 語(yǔ)法從在項(xiàng)目中獲取需要的數(shù)據(jù)
html-table 用于生成一個(gè) HTML 格式的表格數(shù)據(jù)
pyecharts 和 snapshot_selenium 用于數(shù)據(jù)可視化
2. 實(shí)戰(zhàn)一下
下面我們通過(guò) 7 個(gè)步驟來(lái)實(shí)現(xiàn)上面的功能
2-1 登錄獲取客戶端連接對(duì)象
- from jira import JIRA
-
- class JiraObj(object):
- def __init__(self, bug_style, project_type):
- """
- :param project_name
- :param sprint: 迭代號(hào)碼
- :param bug_style: BUG狀態(tài)
- """
- # Jira首頁(yè)地址
- self.server = 'https://jira.**.team'
-
- # Jira登錄賬號(hào)信息
- self.basic_auth = ('用戶名', '密碼')
-
- # 創(chuàng)建一個(gè)客戶端連接信息
- self.jiraClinet = JIRA(server=self.server, basic_auth=self.basic_auth)
2-2 根據(jù)項(xiàng)目類型獲取看板 id
- ...
- # 獲取boards看板
- # 所有看板信息
- boards = [(item.id, item.name) for item in self.jiraClinet.boards()]
- board_id = self.__get_board_id(boards, project_type)
- print("看板id:", board_id)
- ...
- def __get_board_id(self, boards, project_type):
- """
- 獲取看板id
- :param project_type:
- :return:
- """
- board_id = 1
- for item in boards:
- if (project_type == PROJ_TYPE.Type1 and item[1] == 't1') or (
- project_type == PROJ_TYPE.Type2 and item[1] == 't2'):
- board_id = item[0]
- break
- return board_id
- ..
2-3 根據(jù)看板 id 獲取迭代 id 及迭代名稱
- ...
- # 獲取項(xiàng)目Sprint,讓用戶進(jìn)行選擇
- sprints = self.jiraClinet.sprints(board_id=board_id)
- for item in sprints:
- if str(sprint_no) in item.name:
- self.sprint_id = item.id
- self.sprint_name = item.name
- print(f"選擇Sprint,id:{self.sprint_id},name:{self.sprint_name}")
- break
- ...
2-4 根據(jù)項(xiàng)目名、Bug 類型、迭代 id 組成 jsql 語(yǔ)句,并查詢數(shù)據(jù)
- ...
- def get_bug_status_jsql(self, bug_status: BUG_STATUS):
- """
- 通過(guò)bug狀態(tài),獲取jsql
- :param bug_status:
- :return:
- """
- status_jsql = ''
- if bug_status == BUG_STATUS.ALL:
- status_jsql = ' '
- elif bug_status == BUG_STATUS.TO_VERIFY:
- # 待驗(yàn)證(已解決)
- status_jsql = ' AND status = 已解決 '
- elif bug_status == BUG_STATUS.TO_FIXED:
- # 待解決(打開(kāi)、重新打開(kāi)、處理中)
- status_jsql = ' AND status in (打開(kāi), 重新打開(kāi), 處理中) '
- elif bug_status == BUG_STATUS.CLOSED:
- # 關(guān)閉
- status_jsql = ' AND status = Closed '
- elif bug_status == BUG_STATUS.TO_FIXED_CONTAIN_DELAY:
- # 待解決(打開(kāi)、重新打開(kāi)、處理中、延期處理)
- status_jsql = ' AND status in (打開(kāi), 延期處理, 重新打開(kāi), 處理中) '
- return status_jsql
- ...
- jql = f'project = {project_name} and issuetype = 故障 {self.get_bug_status_jsql(self.bug_style)}
- AND Sprint = {self.sprint_id} ORDER BY priority desc, updated DESC'
- print(jql)
- lists = self.get_issue_list(jql)
- ...
2-5 生成本地 HTML 統(tǒng)計(jì)數(shù)據(jù)
需要注意的是,使用 a 標(biāo)簽組裝的鏈接不能直接跳轉(zhuǎn),需要針對(duì)數(shù)據(jù)進(jìn)行二次替換才能正常進(jìn)行鏈接跳轉(zhuǎn)
- from HTMLTable import (
- HTMLTable
- )
-
- ...
- def gen_html_table(self, datas):
- """
- 初始化表單樣式
- :return:
- """
- table = HTMLTable(caption=f'實(shí)時(shí)BUG統(tǒng)計(jì)【{self.project_name}】,一共{len(datas)}個(gè)')
-
- # 表頭行
- table.append_header_rows((('ID', '狀態(tài)', '優(yōu)先級(jí)', '責(zé)任人', '終端', 'URL'),))
-
- # 添加數(shù)據(jù)
- table.append_data_rows(datas)
-
- # 設(shè)置樣式
- table.caption.set_style({'font-size': '15px'})
-
- # 其他樣式設(shè)置
- ...
-
- # 替換數(shù)據(jù),便于展示href地址
- html = table.to_html().replace("<", "<").replace(">", ">").replace(""", '"')
-
- with open(f"./output/{self.project_name}-bug_{current_time()}.html", 'w', encoding='utf-8')
- as file:
- file.write(html)
- ...
- # 生成本地文件的數(shù)據(jù)
- output_tuples = tuple([
- (item.get("key"), item.get("status"), item.get("priority"), item.get('duty'),
- item.get('end_type'),
- f'<a href="{item.get("url")}" target="_blank">點(diǎn)我查看</a>') for item in lists])
-
- # 生成本地HTML文件
- self.gen_html_table(output_tuples)
- ..
2-6 數(shù)據(jù)統(tǒng)計(jì)
首先,這里按 Bug 責(zé)任人進(jìn)行分組,然后按數(shù)目進(jìn)行降序排列
然后,按 Bug 優(yōu)先等級(jí)進(jìn)行降序排列
最后,獲取每一個(gè)端的 Bug 總數(shù)
- ...
- # 2、統(tǒng)計(jì)每個(gè)人(按數(shù)目)
- datas_by_count = {}
- for item in lists:
- datas_by_count[item.get("duty")] = datas_by_count.get(item.get("duty"), 0) + 1
-
- # 降序排序
- datas_by_count = sorted(datas_by_count.items(), key=lambda item: item[1], reverse=True)
-
- # print("按Bug總數(shù)排序:", datas_by_count)
-
- # 3、統(tǒng)計(jì)每個(gè)人(按優(yōu)先級(jí))
- datas_by_priority = {}
-
- for item in datas_by_count:
- # 責(zé)任人
- name = item[0]
- # 5個(gè)優(yōu)先級(jí)對(duì)應(yīng)的數(shù)目
- counts = self.get_assignee_count(lists, name)
- datas_by_priority[name] = counts
-
- # 排序(按優(yōu)先級(jí)多條件降序排列)
- datas_by_priority = sorted(datas_by_priority.items(),
- key=lambda item: (item[1][0], item[1][1],
- item[1][2], item[1][3]), reverse=True)
-
- # print("按Bug優(yōu)先級(jí)排序:", datas_by_priority)
-
- # 4、根據(jù)終端進(jìn)行統(tǒng)計(jì)分類
- keys, values = self.get_end_type_count(lists)
- ...
2-7 可視化
針對(duì)上面的 3 組數(shù)據(jù),使用 pyecharts 繪制成柱狀圖和餅狀圖
- ...
- def draw_image(self, datas_by_count, datas_by_priority, keys, values):
- """
- 繪制圖片
- :param values:
- :param keys:
- :param datas_by_count: 按bug總數(shù)排序結(jié)果
- :param datas_by_priority: 按bug優(yōu)先級(jí)排序結(jié)果
- :return:
- """
- # 1、按BUG總數(shù)排序繪制
- bar = (
- Bar().set_global_opts(
- title_opts=opts.TitleOpts(title=f"{self.project_name}",
- subtitle=f"{self.sprint_name}")))
- bar.add_xaxis([item[0] for item in datas_by_count])
- bar.add_yaxis(f"BUG總數(shù)", [item[1] for item in datas_by_count])
-
- # render 會(huì)生成本地 HTML 文件,默認(rèn)會(huì)在當(dāng)前目錄生成 render.html 文件
- # 也可以傳入路徑參數(shù),如 bar.render("mycharts.html")
- # bar.render(path=f'{sprint_name}-BUG總數(shù).html')
- make_snapshot(snapshot, bar.render(), "./output/1.png")
-
- # 2、按優(yōu)先級(jí)排序繪制
- bar2 = (
- # Bar(init_opts=opts.InitOpts(theme=ThemeType.INFOGRAPHIC))
- Bar()
- .add_xaxis([item[0] for item in datas_by_priority])
- .add_yaxis(self.__get_priority(BUG_PRIORITY.Highest),
- [item[1][0] for item in datas_by_priority],
- color='#6aa84f')
- .add_yaxis(self.__get_priority(BUG_PRIORITY.High),
- [item[1][1] for item in datas_by_priority],
- color='#a2c4c9')
- .add_yaxis(self.__get_priority(BUG_PRIORITY.Medium),
- [item[1][2] for item in datas_by_priority],
- color="#ff9900")
- .add_yaxis(self.__get_priority(BUG_PRIORITY.Low),
- [item[1][3] for item in datas_by_priority],
- color="#ea9999")
- .add_yaxis(self.__get_priority(BUG_PRIORITY.Lowest),
- [item[1][4] for item in datas_by_priority],
- color="#980000")
- .set_global_opts(
- title_opts=opts.TitleOpts(title=f"{self.project_name}",
- subtitle=f"{self.sprint_name}"))
- )
- # bar2.render(path=f'{sprint_name}-BUG優(yōu)先級(jí).html')
- make_snapshot(snapshot, bar2.render(), "./output/2.png")
-
- # 3、根據(jù)終端來(lái)繪制餅圖
- if len(keys) > 0 and len(values) > 0:
- c = (
- Pie()
- .add("", [list(z) for z in zip(keys, values)])
- .set_global_opts(title_opts=opts.TitleOpts(title="各端BUG分布"))
- .set_series_opts(label_opts=opts.LabelOpts(formatter=": {c}"))
- )
- make_snapshot(snapshot, c.render(), f"./output/{self.project_name}_end.png")
-
- # 4、合并兩張圖片
- self.concatenate_img(['./output/1.png', './output/2.png'],
- img_name=f'./output/{self.sprint_name}_bug.png',
- axis=1)
- ...
3. 總結(jié)
通過(guò)上面的操作,每次只需要輸入項(xiàng)目類型、迭代版本號(hào)、要統(tǒng)計(jì)的 Bug 類型,就能統(tǒng)計(jì)出所需要的數(shù)據(jù)并繪制成圖表
作者:星安果
歡迎關(guān)注微信公眾號(hào) :AirPython