整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

使用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必备。