整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          使用wkhtmltopdf生成基于Jira的測試報告

          使用wkhtmltopdf生成基于Jira的測試報告

          踐過程中,我們使用Jira做統一的項目管理和缺陷管理。在測試完成之后,測試同學需要發送測試報告或者release notes給項目組成員,雖然Jira自身提供了基于release管理的release notes的自動生成,但其界面不太友好,所以有必要我們做一下二次開發。

          一般測試報告的生成,采用word格式(直接套用定義好的模板,填入數據)、html格式或者mardown等格式,其中html和markdown格式非常方便程序的自動生成,我們兩種方式都支持。

          而在測試報告的發送這一塊,可以發送一個鏈接,可以發送一個附件,也可以在郵件內部鏈接上報告的內容。如此,html格式最為適合。其中在附件類型選擇上,html或者pdf都具有很好的顯示效果和跨平臺性。此處為了方便存檔,我們增加了pdf格式的測試報告的自動生成。

          wkhtmltopdf簡介

          其官網為:https://wkhtmltopdf.org/index.html。

          首先我們復制一下官網介紹:

          What is it?

          wkhtmltopdf and wkhtmltoimage are open source (LGPLv3) command line tools to render HTML into PDF and various image formats using the Qt WebKit rendering engine. These run entirely "headless" and do not require a display or display service.

          There is also a C library, if you're into that kind of thing.

          簡單翻譯一下:wkhtmltopdf和wkhtmltoimage是基于開源LGPLv3協議的命令行工具,它使用Qt webkit渲染引擎把HTML渲染為PDF。運行的時候,是“無頭”的并不需要顯示器或者顯示服務。

          同時該工具也提供了基于C的庫文件。

          How do I use it?

          1. Download a precompiled binary or build from source
          2. Create your HTML document that you want to turn into a PDF (or image)
          3. Run your HTML document through the tool.
          4. For example, if I really like the treatment Google has done to their logo today and want to capture it forever as a PDF:
          5. wkhtmltopdf http://google.com google.pdf

          再簡單翻譯一下使用方法:下載二進制文件或者從源文件構建;創建html文件;以google為例進行html到pdf的轉換: wkhtmltopdf http://google.com google.pdf即可。

          整體看,wkhtmltopdf使用起來很方便,pdf轉換效果很理想,安裝也簡單。其下載地址為:https://wkhtmltopdf.org/downloads.html

          定義html模板文件

          測試報告的html模板,這里列出一些共性的東西:上線內容、缺陷統計、研發效率等,大家可以結合自身的業務需要進行添加。簡單的實現,可以直接做字符串替換,麻煩點的,就可以用thymeleaf等模板文件了:

          <html>
           <meta http-equiv="content-type" content="text/html;charset=utf-8">
           <head>
           <style type="text/css">
           body{
           font-size: 9pt;
           }
           table{border-collapse: collapse;width:800px;}
           tr{}
           td{border: 1px solid #0D3349;padding:5px;font-size:9pt;}
           .tr-label{
           background-color: #2D64B3;
           color:#ffffff;
           }
           a{
           text-decoration: none;
           color:#000000;
           }
           </style>
           <title>{{REPORT_TITLE}}</title>
           </head>
           <body>
           <h1>{{REPORT_TITLE}}</h1>
           <hr style="size:1px;"/>
           <p><h2>上線內容:</h2></p>
           <table>
           <tr class="tr-label">
           <td width="150px">類別</td>
           <td>內容</td>
           </tr>
           {{RELEASE_NOTE}}
           </table>
           <p><h2>缺陷分析 - 根據模塊:</h2></h2></p>
           <table>
           <tr class="tr-label">
           <td width="150px">模塊名稱</td>
           <td>個數</td>
           </tr>
           {{ISSUE_COMPONENT}}
           </table>
           <p><h2>缺陷分析 - 根據根據狀態:</h2></p>
           <table>
           <tr class="tr-label">
           <td width="150px">狀態</td>
           <td>個數</td>
           </tr>
           {{ISSUE_STATUS}}
           </table>
           <p><h2>缺陷分析 - 根據優先級:</h2></p>
           <table>
           <tr class="tr-label">
           <td width="150px">優先級</td>
           <td>個數</td>
           </tr>
           {{ISSUE_PRIORITY}}
           </table>
           <p><h2>缺陷分析 - 根據經辦人:</h2></p>
           <table>
           <tr class="tr-label">
           <td width="150px">經辦人</td>
           <td>個數</td>
           </tr>
           {{ISSUE_ASSIGNEE}}
           </table>
           <p><h2>缺陷分析 - 根據報告人:</h2></p>
           <table>
           <tr class="tr-label">
           <td width="150px">報告人</td>
           <td>個數</td>
           </tr>
           {{ISSUE_REPORTER}}
           </table>
           <p><h2>缺陷開發測試效率</h2></p>
           <table>
           <tr class="tr-label">
           <td width="150px">類別</td>
           <td>耗時(小時)</td>
           </tr>
           <tr>
           <td width="150px">開發響應平均耗時</td>
           <td>{{DEV_REACT}}</td>
           </tr>
           <tr>
           <td width="150px">開發處理平均耗時</td>
           <td>{{DEV_PROCESS}}</td>
           </tr>
           <tr>
           <td width="150px">測試響應平均耗時</td>
           <td>{{TEST_REACT}}</td>
           </tr>
           <tr>
           <td width="150px">測試處理平均耗時</td>
           <td>{{TEST_PROCESS}}</td>
           </tr>
           </table>
           </body>
          </html>
          

          結果數據的獲取

          上文提到,我們才用Jira做項目管理,所以測試數據的獲取,主要還是使用Jira-client這個三方jar,個別地方,比如缺陷時間這一塊,采用直接query數據庫的方式實現以提高效率,關鍵代碼如下(注意里面夾雜了mardown格式的):

          /**
           * 獲取根據jql生成的issue
           * @param jql
           * @return
           * @throws Exception
           */
          public List<Issue> getIssues(String jql) throws Exception{
           Iterator<Issue> iterator=jiraClient.searchIssues(jql).iterator();
           List<Issue> list=new ArrayList<>();
           while(iterator.hasNext()){
           list.add(iterator.next());
           }
           return list;
          }
          /**
           * 生成報告
           * @param title
           * @param jql
           * @return
           * @throws Exception
           */
          public String[] generateReport(String title, String jql) throws Exception{
           String[] content=generateReport(getIssues(jql));
           for(int i=0; i < content.length; i ++){
           content[i]=content[i].replace("{{REPORT_TITLE}}", title);
           }
           return content;
          }
          /**
           * 測試報告生成pdf
           * @param title
           * @param jql
           * @throws Exception
           */
          public File generateReportPdf(String title, String jql) throws Exception{
           String html=generateReport(title, jql)[1];
           LocalDateTime localDate=LocalDateTime.now();
           String date=localDate.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
           String htmlFileName=jiraReportPath + "/" + date + ".html";
           IOUtils.write(html, new FileOutputStream(new File(htmlFileName)));
           String pdfFileName=jiraReportPath + "/" + date + ".pdf";
           String cmd=wkthmltopdfCmd + " " + htmlFileName + " " + pdfFileName;
           Process process=Runtime.getRuntime().exec(cmd);
           process.waitFor();
           return new File(pdfFileName);
          }
          private int timediffToMinutes(String timeDiff){
           String[] info=timeDiff.split(":");
           return Integer.valueOf(info[0]) * 60 + Integer.valueOf(info[1]);
          }
          /**
           * 獲取issue的開發、測試響應和處理耗時
           * @param issueList
           * @return
           */
          private String[] issueReactProcess(List<Issue> issueList){
           List<Integer> devReact=new ArrayList<>();
           List<Integer> devProcess=new ArrayList<>();
           List<Integer> testReact=new ArrayList<>();
           List<Integer> testProcess=new ArrayList<>();
           String[] result=new String[8];
           int maxSingleDevReact=0;
           int maxSingleDevProcess=0;
           int maxSingleTestReact=0;
           int maxSingleTestProcess=0;
           for(Issue issue : issueList){
           if((issue.getIssueType().getName().equalsIgnoreCase("BUG")
           || issue.getIssueType().getName().equalsIgnoreCase("缺陷")
           || issue.getIssueType().getName().equalsIgnoreCase("故障")) && issue.getStatus().getName().equalsIgnoreCase("測試通過")){//只統計測試通過的
           log.info(">>> 添加需要統計耗時的缺陷:{}", issue.getKey());
           try {
           String sql="select timediff(changegroup.created,jiraissue.created) from changeitem,changegroup,jiraissue where \n" +
           "changeitem.field='status' and changeitem.groupid in (select id from changegroup where issueid=" + issue.getId() + ") and changeitem.groupid=changegroup.id \n" +
           "and changegroup.issueid=jiraissue.id and changeitem.newString='處理中';";
           int singleDevReact=timediffToMinutes(jiraJdbcTemplate.queryForObject(sql, String.class));
           devReact.add(singleDevReact);
           if(singleDevReact > maxSingleDevReact){
           maxSingleDevReact=singleDevReact;
           try {
           result[4]="<a href='" + IssueUtils.webUrl(issue) + "' target='_blank'>" + issue.getSummary() + "</a>";
           } catch (Exception e) {
           e.printStackTrace();
           }
           }
           sql="select timediff(changegroup.created,jiraissue.created) from changeitem,changegroup,jiraissue where \n" +
           "changeitem.field='status' and changeitem.groupid in (select id from changegroup where issueid=" + issue.getId() + ") and changeitem.groupid=changegroup.id \n" +
           "and changegroup.issueid=jiraissue.id and changeitem.newString='開發完成';";
           int singleDevProcess=timediffToMinutes(jiraJdbcTemplate.queryForObject(sql, String.class));
           devProcess.add(singleDevProcess - singleDevReact);
           if(singleDevProcess > maxSingleDevProcess){
           maxSingleDevProcess=singleDevProcess;
           try {
           result[5]="<a href='" + IssueUtils.webUrl(issue) + "' target='_blank'>" + issue.getSummary() + "</a>";
           } catch (Exception e) {
           e.printStackTrace();
           }
           }
           sql="select timediff(changegroup.created,jiraissue.created) from changeitem,changegroup,jiraissue where \n" +
           "changeitem.field='status' and changeitem.groupid in (select id from changegroup where issueid=" + issue.getId() + ") and changeitem.groupid=changegroup.id \n" +
           "and changegroup.issueid=jiraissue.id and changeitem.newString='測試進行中';";
           int singleTestReact=timediffToMinutes(jiraJdbcTemplate.queryForObject(sql, String.class));
           testReact.add(singleTestReact - singleDevProcess);
           if(singleTestReact > maxSingleTestReact){
           maxSingleTestReact=singleTestReact;
           try {
           result[6]="<a href='" + IssueUtils.webUrl(issue) + "' target='_blank'>" + issue.getSummary() + "</a>";
           } catch (Exception e) {
           e.printStackTrace();
           }
           }
           sql="select timediff(changegroup.created,jiraissue.created) from changeitem,changegroup,jiraissue where \n" +
           "changeitem.field='status' and changeitem.groupid in (select id from changegroup where issueid=" + issue.getId() + ") and changeitem.groupid=changegroup.id \n" +
           "and changegroup.issueid=jiraissue.id and changeitem.newString='測試通過';";
           int singleTestProcess=timediffToMinutes(jiraJdbcTemplate.queryForObject(sql, String.class));
           testProcess.add(singleTestProcess - singleTestReact);
           if(singleTestProcess > maxSingleTestProcess){
           maxSingleTestProcess=singleTestProcess;
           try {
           result[7]="<a href='" + IssueUtils.webUrl(issue) + "' target='_blank'>" + issue.getSummary() + "</a>";
           } catch (Exception e) {
           e.printStackTrace();
           }
           }
           } catch (DataAccessException e) {
           continue;
           }
           }
           }
           result[0]=String.valueOf(generateIssueDuration(devReact));
           result[1]=String.valueOf(generateIssueDuration(devProcess));
           result[2]=String.valueOf(generateIssueDuration(testReact));
           result[3]=String.valueOf(generateIssueDuration(testProcess));
           return result;
          }
          private double generateIssueDuration(List<Integer> list){
           DescriptiveStatistics descriptiveStatistics=new DescriptiveStatistics();
           for(int d : list){
           descriptiveStatistics.addValue(d / 60f);//按小時統計
           }
           double r=0;
           try{
           r=new BigDecimal(descriptiveStatistics.getMean()).setScale(BigDecimal.ROUND_HALF_UP, 2).doubleValue();
           }catch(Exception e){
           }
           return r;
          }
          /**
           * 生成報告
           * @param issueList
           * @throws Exception
           */
          private String[] generateReport(List<Issue> issueList) throws Exception{
           String[] issueReactProcessResult=issueReactProcess(issueList);
           Map<String, String> ldapUserMap=new HashMap<>();
           for(LdapUserEntity ldapUserEntity : ldapUserService.allUsers()){
           ldapUserMap.put(ldapUserEntity.getUserName(), ldapUserEntity.getDisplayName());
           }
           ldapUserMap.put("TBD", "TBD");
           String content=IOUtils.toString(new ClassPathResource("jira/report.md").getInputStream(), "utf-8");
           String content2=IOUtils.toString(new ClassPathResource("jira/jira_release_report.html").getInputStream(), "utf-8");
           content2=content2.replace("{{DEV_REACT}}", issueReactProcessResult[0]);
           content2=content2.replace("{{DEV_PROCESS}}", issueReactProcessResult[1]);
           content2=content2.replace("{{TEST_REACT}}", issueReactProcessResult[2]);
           content2=content2.replace("{{TEST_PROCESS}}", issueReactProcessResult[3]);
           content2=content2.replace("{{MAX_DEV_REACT}}", issueReactProcessResult[4]);
           content2=content2.replace("{{MAX_DEV_PROCESS}}", issueReactProcessResult[5]);
           content2=content2.replace("{{MAX_TEST_REACT}}", issueReactProcessResult[6]);
           content2=content2.replace("{{MAX_TEST_PROCESS}}", issueReactProcessResult[7]);
           Map<String, List<String>> releaseNote=new HashMap<>();
           Map<String, Integer> issuePriority=new HashMap<>();
           Map<String, Integer> issueAssignee=new HashMap<>();
           Map<String, Integer> issueReporter=new HashMap<>();
           Map<String, Integer> issueStatus=new HashMap<>();
           Map<String, Integer> component=new HashMap<>();
           Comparator<Map.Entry<String, Integer>> comparator=new Comparator<Map.Entry<String, Integer>>() {
           @Override
           public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
           if(o1.getKey().equalsIgnoreCase(o2.getKey())){
           return o2.getValue().compareTo(o1.getValue());
           }
           else{
           return o1.getKey().compareTo(o2.getKey());
           }
           }
           };
           for(Issue issue : issueList){
           //生成release note
           String type=issue.getIssueType().getName();
           if(!releaseNote.keySet().contains(type)){
           releaseNote.put(type, new ArrayList<String>());
           }
           releaseNote.get(type).add(issue.getKey() + ":" + issue.getSummary());
           if("bug".equalsIgnoreCase(type) || "故障".equalsIgnoreCase(type)) {
           //統計priority
           String priority=issue.getPriority().getName();
           if (!issuePriority.keySet().contains(priority)) {
           issuePriority.put(priority, 0);
           }
           issuePriority.put(priority, issuePriority.get(priority) + 1);
           //統計assignee
           String assignee="TBD";
           try {
           assignee=issue.getAssignee().getName();
           } catch (Exception e) {
           log.error(e.getMessage(), e);
           }
           if (!issueAssignee.keySet().contains(assignee)) {
           issueAssignee.put(assignee, 0);
           }
           issueAssignee.put(assignee, issueAssignee.get(assignee) + 1);
           //統計reporter
           String reporter="TBD";
           try {
           reporter=issue.getReporter().getName();
           } catch (Exception e) {
           log.error(e.getMessage(), e);
           }
           if (!issueReporter.keySet().contains(reporter)) {
           issueReporter.put(reporter, 0);
           }
           issueReporter.put(reporter, issueReporter.get(reporter) + 1);
           //統計issue status
           String status=issue.getStatus().getName();
           if (!issueStatus.keySet().contains(status)) {
           issueStatus.put(status, 0);
           }
           issueStatus.put(status, issueStatus.get(status) + 1);
           //統計component
           for (net.rcarz.jiraclient.Component c : issue.getComponents()) {
           String cc=c.getName();
           if (!component.keySet().contains(cc)) {
           component.put(cc, 0);
           }
           component.put(cc, component.get(cc) + 1);
           }
           }
           }
           //issuePriority排序
           List<Map.Entry<String, Integer>> list=new ArrayList<Map.Entry<String, Integer>>(issuePriority.entrySet());
           Collections.sort(list, comparator);
           StringBuilder sb=new StringBuilder();
           StringBuilder sb2=new StringBuilder();
           sb.append("| 級別 | 個數 |\n");
           sb.append("|:----|----:|\n");
           for(Map.Entry<String, Integer> entry : list){
           sb.append("| " + entry.getKey() + " | " + entry.getValue() + " |\n");
           sb2.append("<tr><td>" + entry.getKey() + "</td><td>" + entry.getValue() + "</td></tr>");
           }
           content=content.replace("{{ISSUE_PRIORITY}}", sb.toString());
           content2=content2.replace("{{ISSUE_PRIORITY}}", sb2.toString());
           //issueassignee
           list=new ArrayList<Map.Entry<String, Integer>>(issueAssignee.entrySet());
           Collections.sort(list, comparator);
           sb=new StringBuilder();
           sb2=new StringBuilder();
           sb.append("| 經辦人 | 個數 |\n");
           sb.append("|:----|----:|\n");
           for(Map.Entry<String, Integer> entry : list){
           sb.append("| " + entry.getKey() + " | " + entry.getValue() + " |\n");
           sb2.append("<tr><td>" + ldapUserMap.get(entry.getKey()) + "</td><td>" + entry.getValue() + "</td></tr>");
           }
           content=content.replace("{{ISSUE_ASSIGNEE}}", sb.toString());
           content2=content2.replace("{{ISSUE_ASSIGNEE}}", sb2.toString());
           //issue reporter
           list=new ArrayList<Map.Entry<String, Integer>>(issueReporter.entrySet());
           Collections.sort(list, comparator);
           sb=new StringBuilder();
           sb2=new StringBuilder();
           sb.append("| 報告人 | 個數 |\n");
           sb.append("|:----|----:|\n");
           for(Map.Entry<String, Integer> entry : list){
           sb.append("| " + entry.getKey() + " | " + entry.getValue() + " |\n");
           sb2.append("<tr><td>" + ldapUserMap.get(entry.getKey()) + "</td><td>" + entry.getValue() + "</td></tr>");
           }
           content=content.replace("{{ISSUE_REPORTER}}", sb.toString());
           content2=content2.replace("{{ISSUE_REPORTER}}", sb2.toString());
           //issuestatus
           list=new ArrayList<Map.Entry<String, Integer>>(issueStatus.entrySet());
           Collections.sort(list, comparator);
           sb=new StringBuilder();
           sb2=new StringBuilder();
           sb.append("| 狀態 | 個數 |\n");
           sb.append("|:----|----:|\n");
           for(Map.Entry<String, Integer> entry : list){
           sb.append("| " + entry.getKey() + " | " + entry.getValue() + " |\n");
           sb2.append("<tr><td>" + entry.getKey() + "</td><td>" + entry.getValue() + "</td></tr>");
           }
           content=content.replace("{{ISSUE_STATUS}}", sb.toString());
           content2=content2.replace("{{ISSUE_STATUS}}", sb2.toString());
           //issue component
           list=new ArrayList<Map.Entry<String, Integer>>(component.entrySet());
           Collections.sort(list, comparator);
           sb=new StringBuilder();
           sb2=new StringBuilder();
           sb.append("| 模塊 | 個數 |\n");
           sb.append("|:----|----:|\n");
           for(Map.Entry<String, Integer> entry : list){
           sb.append("| " + entry.getKey() + " | " + entry.getValue() + " |\n");
           sb2.append("<tr><td>" + entry.getKey() + "</td><td>" + entry.getValue() + "</td></tr>");
           }
           content=content.replace("{{ISSUE_COMPONENT}}", sb.toString());
           content2=content2.replace("{{ISSUE_COMPONENT}}", sb2.toString());
           //release note
           List<Map.Entry<String, List<String>>> list2=new ArrayList<>(releaseNote.entrySet());
           Collections.sort(list2, new Comparator<Map.Entry<String, List<String>>>() {
           @Override
           public int compare(Map.Entry<String, List<String>> o1, Map.Entry<String, List<String>> o2) {
           return o1.getKey().compareTo(o2.getKey());
           }
           });
           sb=new StringBuilder();
           sb2=new StringBuilder();
           Comparator<String> comparator2=new Comparator<String>() {
           @Override
           public int compare(String o1, String o2) {
           return o1.substring(0, o1.indexOf(":")).compareTo(o2.substring(0, o2.indexOf(":")));
           }
           };
           for(Map.Entry<String, List<String>> entry : list2){
           List<String> valueList=entry.getValue();
           Collections.sort(valueList, comparator2);
           for(String v : valueList) {
           sb.append("* [ " + entry.getKey() + " ] " + v + "\n");
           sb2.append("<tr><td>" + entry.getKey() + "</td><td><a href='" + jiraIssueBrowseUrl + v.substring(0, v.indexOf(":")) + "' target='_blank'>" + v + "</a></td></tr>");
           }
           }
           content=content.replace("{{RELEASE_NOTE}}", sb.toString());
           content2=content2.replace("{{RELEASE_NOTE}}", sb2.toString());
           return new String[]{content, content2};
          }
          private String jiraBugSummaryProcessUser(String user){
           String[] reporters=user.split(",");
           for(int i=0; i < reporters.length; i ++){
           reporters[i]="'" + reporters[i] + "'";
           }
           user=StringUtils.join(reporters, ",");
           return user;
          }
          /**
           *
           * @param type
           * @param user
           * @return
           */
          public List<String> jiraBugSummaryLabel(String type, String user){
           user=jiraBugSummaryProcessUser(user);
           String sql="";
           if("reporter".equalsIgnoreCase(type) || "depReporter".equalsIgnoreCase(type)){
           sql="select distinct(date_format(a.created, '%Y%m')) as sdate from jiraissue a where a.reporter in(" + user + ")";
           }
           else if("assignee".equalsIgnoreCase(type) || "depAssignee".equalsIgnoreCase(type)){
           sql="select distinct(date_format(a.created, '%Y%m')) as sdate from jiraissue a where a.assignee in(" + user + ")";
           }
           List<String> list=jiraJdbcTemplate.queryForList(sql, String.class);
           Collections.sort(list, new Comparator<String>() {
           @Override
           public int compare(String o1, String o2) {
           return o1.compareTo(o2);
           }
           });
           return list;
          }
          public List<JiraBugSummaryEntity> jiraBugSummary(String type, String user, String dep){
           user=jiraBugSummaryProcessUser(user);
           String sql="";
           if("reporter".equalsIgnoreCase(type)){
           sql="select count(1) as `count`, date_format(a.created, '%Y%m') as sdate, a.reporter as `user`";
           sql +=" from jiraissue a where a.issuetype in ('10004', '10207') and a.reporter in(" + user + ") group by sdate,user order by sdate";
           }
           else if("assignee".equalsIgnoreCase(type)){
           sql="select count(1) as `count`, date_format(a.created, '%Y%m') as sdate, a.assignee as `user`";
           sql +=" from jiraissue a where a.issuetype in ('10004', '10207') and a.assignee in(" + user + ") group by sdate,user order by sdate";
           }
           else if("depReporter".equalsIgnoreCase(type)){
           sql="select count(1) as `count`, date_format(a.created, '%Y%m') as sdate, '" + dep + "' as `user`";
           sql +=" from jiraissue a where a.issuetype in ('10004', '10207') and a.reporter in(" + user + ") group by sdate order by sdate";
           }
           else if("depAssignee".equalsIgnoreCase(type)){
           sql="select count(1) as `count`, date_format(a.created, '%Y%m') as sdate, '" + dep + "' as `user`";
           sql +=" from jiraissue a where a.issuetype in ('10004', '10207') and a.assignee in(" + user + ") group by sdate order by sdate";
           }
           List<JiraBugSummaryEntity> list=jiraJdbcTemplate.query(sql, new BeanPropertyRowMapper<JiraBugSummaryEntity>(JiraBugSummaryEntity.class));
           return list;
          }
          
          

          都是一些常規用法,就不解釋代碼了。

          年榮譽之路活動已經開啟了,和去年一樣,沒達到2級榮譽并沒有被鎖的玩家可以通過完成任務來恢復至2級榮譽,2級至4級的玩家可以提升一級榮譽,5級榮譽則可以領取魔法引擎。而恢復至2級榮譽并在單雙或靈活排位達到黃金及以上玩家能獲得勝利女神瑟莊妮,達到5級榮譽可以獲得三大榮譽瑪爾扎哈。


          活動時間:2022年11月25日-2023年1月8日

          活動鏈接:

          ?https://lol.qq.com/act/a20221125roadtohonor/index.html?e_code=508038&exchangeType=1


          目前有三種情況:

          ①恢復2級榮譽,適用0級和1級的,這個恢復也是用來給剛剛結束的S12賽季單雙或靈活排位達到黃金及以上段位的玩家領勝利女神瑟莊妮的。

          ②提升1檔榮譽,給2-4級的玩家用的,能提升一檔榮譽,因為每一級榮譽有三個里程碑點,我建議盡可能把等級升高點和剛升榮譽等級的時候用,比如剛升4級,再到這里申請就可以直達5級。可以用來領三大榮譽瑪爾扎哈的皮膚。

          ③榮譽5級獎勵。5級榮譽玩家可以領一個魔法引擎,這個引擎也沒啥好東西,就是一些鑰匙和守衛皮膚碎片等。


          要注意的是,這三個只能選擇一個參與。

          你選擇從3級升到4級,以后哪怕再達到5級也不能選擇領取魔法引擎。或者你恢復到了2級,也不能再選擇提升一檔榮譽了。

          所以這個活動可以晚點參與。


          游戲任務:

          ?都是登錄7天游戲并每天完成一場對局,由于時間比較長肯定做的完。



          恢復2級榮譽需要手打“我承諾...”也就是“認罪書”。對皮膚無所謂的也不用參加這玩意。


          關于獎勵

          ?之前的賽季結算公告有提過會在11.17以后發放獎勵,并在12月完成所有賬號發放。現在基本都還沒領到,慢慢等就行。



          ?當然瑪爾扎哈的皮膚只要在1月8日23:59之前達到5級榮譽都能獲得。

          最后覺得有用可以點個關注、分享啥的,有關英雄聯盟、云頂之弈的皮膚、資訊、活動、白嫖福利都會發。

          讀:任何原始格式的數據載入DataFrame后,都可以使用類似DataFrame.to_csv()的方法輸出到相應格式的文件或者目標系統里。本文將介紹一些常用的數據輸出目標格式。

          作者:李慶輝

          來源:華章科技

          01 CSV

          DataFrame.to_csv方法可以將DataFrame導出為CSV格式的文件,需要傳入一個CSV文件名。

          df.to_csv('done.csv')
          df.to_csv('data/done.csv') # 可以指定文件目錄路徑
          df.to_csv('done.csv', index=False) # 不要索引

          另外還可以使用sep參數指定分隔符,columns傳入一個序列指定列名,編碼用encoding傳入。如果不需要表頭,可以將header設為False。如果文件較大,可以使用compression進行壓縮:

          # 創建一個包含out.csv的壓縮文件out.zip
          compression_opts=dict(method='zip',
          archive_name='out.csv') 
          df.to_csv('out.zip', index=False,
          compression=compression_opts) 

          02 Excel

          將DataFrame導出為Excel格式也很方便,使用DataFrame.to_excel方法即可。要想把DataFrame對象導出,首先要指定一個文件名,這個文件名必須以.xlsx或.xls為擴展名,生成的文件標簽名也可以用sheet_name指定。

          如果要導出多個DataFrame到一個Excel,可以借助ExcelWriter對象來實現。

          # 導出,可以指定文件路徑
          df.to_excel('path_to_file.xlsx')
          # 指定sheet名,不要索引
          df.to_excel('path_to_file.xlsx', sheet_name='Sheet1', index=False)
          # 指定索引名,不合并單元格
          df.to_excel('path_to_file.xlsx', index_label='label', merge_cells=False)

          多個數據的導出如下:

          # 將多個df分不同sheet導入一個Excel文件中
          with pd.ExcelWriter('path_to_file.xlsx') as writer:
          df1.to_excel(writer, sheet_name='Sheet1')
          df2.to_excel(writer, sheet_name='Sheet2')

          使用指定的Excel導出引擎如下:

          # 指定操作引擎
          df.to_excel('path_to_file.xlsx', sheet_name='Sheet1', engine='xlsxwriter')
          # 在'engine'參數中設置ExcelWriter使用的引擎
          writer=pd.ExcelWriter('path_to_file.xlsx', engine='xlsxwriter')
          df.to_excel(writer)
          writer.save()
          
          # 設置系統引擎
          from pandas import options # noqa: E402
          options.io.excel.xlsx.writer='xlsxwriter'
          df.to_excel('path_to_file.xlsx', sheet_name='Sheet1')

          03 HTML

          DataFrame.to_html會將DataFrame中的數據組裝在HTML代碼的table標簽中,輸入一個字符串,這部分HTML代碼可以放在網頁中進行展示,也可以作為郵件正文。

          print(df.to_html())
          print(df.to_html(columns=[0])) # 輸出指定列
          print(df.to_html(bold_rows=False)) # 表頭不加粗
          # 表格指定樣式,支持多個
          print(df.to_html(classes=['class1', 'class2']))

          04 數據庫(SQL)

          將DataFrame中的數據保存到數據庫的對應表中:

          # 需要安裝SQLAlchemy庫
          from sqlalchemy import create_engine
          # 創建數據庫對象,SQLite內存模式
          engine=create_engine('sqlite:///:memory:')
          # 取出表名為data的表數據
          with engine.connect() as conn, conn.begin():
          data=pd.read_sql_table('data', conn)
          
          # data
          # 將數據寫入
          data.to_sql('data', engine)
          # 大量寫入
          data.to_sql('data_chunked', engine, chunksize=1000)
          # 使用SQL查詢
          pd.read_sql_query('SELECT * FROM data', engine)

          05 Markdown

          Markdown是一種常用的技術文檔編寫語言,Pandas支持輸出Markdown格式的字符串,如下:

          print(cdf.to_markdown())
          
          '''
          | | x | y | z |
          |:---|----:|----:|----:|
          | a | 1 | 2 | 3 |
          | b | 4 | 5 | 6 |
          | c | 7 | 8 | 9 |
          '''

          小結

          本文介紹了如何將DataFrame對象數據進行輸出,數據經輸出、持久化后會成為固定的數據資產,供我們進行歸檔和分析。

          關于作者:李慶輝,數據產品專家,某電商公司數據產品團隊負責人,擅長通過數據治理、數據分析、數據化運營提升公司的數據應用水平。精通Python數據科學及Python Web開發,曾獨立開發公司的自動化數據分析平臺,參與教育部“1+X”數據分析(Python)職業技能等級標準評審。中國人工智能學會會員,企業數字化、數據產品和數據分析講師,在個人網站“蓋若”上編寫的技術和產品教程廣受歡迎。

          本書摘編自《深入淺出Pandas:利用Python進行數據處理與分析》,機械工業出版社華章公司2021年出版。轉載請與我們取得授權。

          延伸閱讀《深入淺出Pandas》

          推薦語:這是一本全面覆蓋了Pandas使用者的普遍需求和痛點的著作,基于實用、易學的原則,從功能、使用、原理等多個維度對Pandas做了全方位的詳細講解,既是初學者系統學習Pandas難得的入門書,又是有經驗的Python工程師案頭必不可少的查詢手冊。《利用Python進行數據分析》學習伴侶,用好Python必備。


          主站蜘蛛池模板: 国产午夜精品一区二区三区漫画| 人妻无码视频一区二区三区| 国产乱码精品一区二区三区中文| 午夜性色一区二区三区免费不卡视频 | 97久久精品无码一区二区天美 | 一区二区三区在线观看视频| 亚洲视频一区网站| 日韩免费视频一区二区| 成人一区二区免费视频| 一区二区三区免费在线视频| 国产精品一区二区久久乐下载| 日韩国产免费一区二区三区| 日产亚洲一区二区三区| 亚洲国产精品乱码一区二区 | 久久国产香蕉一区精品| 四虎一区二区成人免费影院网址| 国内精自品线一区91| 精品国产一区二区三区av片| 国产精品无码一区二区三区免费| 亚洲午夜精品一区二区麻豆| 亚洲综合在线一区二区三区| 国内自拍视频一区二区三区| 日韩一区二区超清视频| 午夜DV内射一区区| 丝袜美腿高跟呻吟高潮一区| 亚洲av片一区二区三区| 成人无码AV一区二区| 久久国产午夜一区二区福利| 亚洲AV美女一区二区三区| 亚洲美女视频一区| 性色av闺蜜一区二区三区| 成人精品一区久久久久| av在线亚洲欧洲日产一区二区| 成人区人妻精品一区二区不卡视频 | 欧美日韩精品一区二区在线观看| 成人精品视频一区二区| 变态调教一区二区三区| 亚洲乱码国产一区三区| 影音先锋中文无码一区| 视频在线一区二区| 久久久精品人妻一区亚美研究所 |