写点什么

【Java】Jsoup 解析 HTML 报告

  • 2024-08-05
    福建
  • 本文字数:3442 字

    阅读完需:约 11 分钟

一、需求背景


有好几种报告文件,目前是人肉找报告信息填到 Excel 上生成统计信息

跟用户交流了下需求和提供的几个文件,发现都是 html 文件

其实所谓的报告的文件,就是一些本地可打开的静态资源,里面也有 js、img 等等


二、方案选型


前面老板一直说是文档解析,我寻思这不就是写爬虫吗....

因为是在现有系统上加新功能实现,现有系统还是 Java 做后端服务,所以之前学的 Python 就不想用了

写 Python 还需要单独起个服务部署起来,Java 有 JSOUP 能用,没 Python 那么好用就是...


三、落地实现


1、JSOUP 依赖坐标:

<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup --><dependency>    <groupId>org.jsoup</groupId>    <artifactId>jsoup</artifactId>    <version>1.18.1</version></dependency>
复制代码


2、文件读取问题


我发现每种类型的报告文件的存放方式都不一样


第一种单 HTML 文件:

这种相对简单,只需要读取路径后直接访问文件内容即可

String reportFilePath = "C:/Users/Administrator/Desktop/report-type/xxx.html";String htmlContent = new String(Files.readAllBytes(Paths.get(reportFilePath)), StandardCharsets.UTF_8);Document doc = Jsoup.parse(htmlContent); 
复制代码


第二种单 Zip 压缩文件:



单层压缩,可以通过 zipFile 的 API 访问,取出压缩条目一个个用条目名称进行判断

再通过 zipFile 打开读取流对该条目进行读取

String targetFile = "index.html";ZipEntry targetEntry = null;String reportFilePath = "C:/Users/Administrator/Desktop/report-type/xxxhtml.zip";ZipFile zipFile = isWinSys() ? new ZipFile(new File(reportFilePath), ZipFile.OPEN_READ, Charset.forName("GBK")) : new ZipFile(reportFilePath);Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();while (zipEntries.hasMoreElements()) {    ZipEntry zipEntry = zipEntries.nextElement();    boolean isDirectory = zipEntry.isDirectory();    if (isDirectory) continue;    String name = zipEntry.getName();    if (targetFile.equals(name)) {        targetEntry = zipEntry;        break;    }}boolean hasFind = Objects.nonNull(targetEntry);if (!hasFind) return; /* 没有可读取的目标文件 */InputStream inputStream = zipFile.getInputStream(targetEntry);String htmlCode = IoUtil.readUtf8(inputStream);Document doc = Jsoup.parse(htmlCode);
复制代码


执行完成后记得要释放资源:

/* 资源释放 */inputStream.close();zipFile.close();
复制代码


第三种多 Zip 嵌套压缩文件:


文件被压缩了两次,要解压两边才可以访问


1、读取内嵌的 Zip 文件时发现 MALFORM 报错,需要根据操作系统设置读取编码...

	https://blog.csdn.net/qq_25112523/article/details/136060946 
复制代码


然后在创建 ZipFile 对象的 API 加了一个操作系统的判断

public static boolean isWinSys() {    String property = System.getProperty("os.name");    return property.contains("win") || property.contains("Win");}
复制代码


2、ZipFile 只对单层压缩有用,如果是嵌套的压缩文件就不支持了


这个报告文件的情况是第一层只有一个条目,所以上传上来的文件我只关心里面只有一个内嵌的压缩文件就行



当匹配这个条件交给 ZipFile 读取输入流,转换成 Zip 输入流,否则不处理

可以在下面代码看到,对被压缩的文件进行 inputStream 读取后,要改用 ZipInputStream 读取

zipInputStream 等效 zipFile + zipEntries 的合体,包含了条目迭代信息

但是只有一个 getNextEntry 方法,只能写 While 循环不断判断下一个条目是否还存在

文件名叫 report.html,判断条目名是否匹配后结束循环

再利用 IO 工具类直接读取 ZipInputStream 即可 (getNextEntry 方法就是让 ZipInputStream 不断切换到当前条目的引用)

如果要处理复杂情况要在 While 里面才能实现的,建议每个条目结束之后调用 closeEntry 方法

String targetSuffix = ".zip";String targetFile = "report.html";String reportFilePath = "C:/Users/Administrator/Desktop/report-type/xx_20240729153751.zip";ZipFile zipFile = isWinSys() ? new ZipFile(new File(reportFilePath), ZipFile.OPEN_READ, Charset.forName("GBK")) : new ZipFile(reportFilePath);Enumeration<? extends ZipEntry> enumeration = zipFile.entries();/* 转换成集合条目,迭代条目不能判断size */List<ZipEntry> zipEntrieList = new ArrayList<>();while (enumeration.hasMoreElements()) {    ZipEntry zipEntry = enumeration.nextElement();    zipEntrieList.add(zipEntry);}/* 只有1个zip压缩文件时才处理 */if (CollectionUtils.isEmpty(zipEntrieList)) return;boolean isOnlyOneEntry = zipEntrieList.size() == 1;boolean anyMatch = zipEntrieList.stream().anyMatch(ze -> ze.getName().endsWith(targetSuffix));if (!isOnlyOneEntry || !anyMatch) return;ZipEntry zipEntry = zipEntrieList.get(0);/* 通过ZipInputStream不断切换条目找到目标文件 */InputStream inputStream = zipFile.getInputStream(zipEntry);ZipInputStream zipInputStream = new ZipInputStream(inputStream);/* 在内层中寻找目标文件 */ZipEntry reportEntry = zipInputStream.getNextEntry();while (Objects.nonNull(reportEntry)) {    String name = reportEntry.getName();    if (targetFile.equals(name)) break;    reportEntry = zipInputStream.getNextEntry();}String htmlCode = IoUtil.readUtf8(zipInputStream);Document doc = Jsoup.parse(htmlCode);
复制代码


同样这里也需要释放资源:

/* 资源释放 */zipInputStream.close();inputStream.close();zipFile.close();
复制代码


3、常见查询 API 使用


一、常见 API 方法


下班到家才反应过来 ownText 是元素自己的文本内容,过滤掉其他嵌套的元素文本



也可以直接使用 cssQuery

doc.select("table.y-report-ui-report-info-grid")
复制代码


二、使用兄弟元素查找对应关系


有一个特殊的情况就是有些元素按文档结构应该是一个逐层关联的结构

先有 A,然后 B 在 A 里面,C 又在 B 里面这样

但是这个是摊开来的结构,A -> B -> C -> D,元素 id 和类名也没用直接关系,这样是很难构建关联的

只能通过元素的顺序推断结构:

1、获取当前 ip 标题元素和下一个 ip 标题元素的兄弟元素下标值

2、将 idp 元素的兄弟元素下标值取出

3、比较 idp 元素是否在两者之间,如果为是表示 idp 元素属于第一个 ip 标题元素



三、父子元素操作获取兄弟元素


报告明细列表,发现标题是 xx 名称,xx 等级摘要信息,点击详情是把下一行展示出来

然后在下一行的 tr 中列出 xx 的全部信息

使用 siblingIndex 不准确,元素是动态的,可以第一张表 10 个,第二章表 20 个这样

所以在表格读取的时候改用 parent() + child()方式读取

在选取表格所有摘要行后,通过父元素的 indexOf 方法获取当前摘要行的下标

再加一就是下一个明细行的下表了

同样还可以通过当前元素的 child 方法直接去第 N 个子元素

这个方式相比 select 方法不用从元素集合中获取,确定是唯一的一个元素

/* 2、读取【漏洞分布】信息 */Element vulnTable = doc.getElementById("vuln_distribution");Element vulnTableBody = vulnTable.child(1);Elements allTrList = vulnTableBody.children();Elements vulnTitleTrList = vulnTable.select("tr[style='cursor:pointer;']");for (Element vrTr : vulnTitleTrList) {    /* 2-1、漏洞名称 */    String vt = vrTr.child(1).text();    int vrTrIdx = allTrList.indexOf(vrTr);    Element vrDetailTr = allTrList.get(vrTrIdx + 1);    Element vrDetailTableBody = vrDetailTr.child(1).child(0).child(0);    /* 2-2、漏洞主机 */    String ipHosts = vrDetailTableBody.child(0).child(1).text();    ipHosts = ipHosts.replaceAll("&nbsp", "").replaceAll(" 点击查看详情;", "");    /* 2-3、漏洞描述 */    String vulnDesc = vrDetailTableBody.child(1).child(1).text();    /* 2-4、威胁分值 */    String vulnTag = vrDetailTableBody.child(3).child(1).text();    String format = StrFormatter.format("reportTime: {}, ip: {}, name: {}, tag: {} desc: {}, ", date, ipHosts, vt, vulnTag, vulnDesc);    System.out.println(format);}
复制代码


文章转载自:emdzz

原文链接:https://www.cnblogs.com/mindzone/p/18339177

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
【Java】Jsoup 解析HTML报告_Java_不在线第一只蜗牛_InfoQ写作社区