使用jsoup获取maven仓库所有版本信息

iBit程序猿 2021年11月14日 2,653次浏览

最新项目需要获取maven仓库中开源的组件版本信息,原以为使用wget命令,就可以从 Maven Repo 轻松获取。可惜,理想很丰满,现实很有骨感。既然wget获取不到,那就自己简单实现个爬虫获取吧。

分析过程

打开页面

打开仓库页面:https://repo.maven.apache.org/maven2/

页面上都是以目录和文件的方式展示的。

查看页面源码

可以轻易的发现目录和文件的内容都是在id为“contents”下的a标签中。

版本信息查看(在maven-metadata.xml)

不断深入某个目录,可以轻易的发现组件的版本信息都在maven-metadata.xml中进行描述。eg:

https://repo.maven.apache.org/maven2/tech/ibit/sql-builder/maven-metadata.xml 的内容

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <metadata>
  3. <groupId>tech.ibit</groupId>
  4. <artifactId>sql-builder</artifactId>
  5. <versioning>
  6. <latest>2.0</latest>
  7. <release>2.0</release>
  8. <versions>
  9. <version>1.0</version>
  10. <version>1.1</version>
  11. <version>2.0</version>
  12. </versions>
  13. <lastUpdated>20201130115230</lastUpdated>
  14. </versioning>
  15. </metadata>

maven-metadata.xml中包含groupIdartifactIdversion信息。

综合上述过程,获取maven所有版本信息,可以做以下操作

  • 遍历 maven repo 所有目录信息,并获取 maven-metadata.xml 文件
  • 解析 maven-metadata.xml,获取 groupIdartifactIdversion

示例代码:

爬取所有的 maven-metadata.xml文件和目录

  1. package tech.ibit.crawler;

  2. import org.apache.commons.lang.StringUtils;
  3. import org.jsoup.Jsoup;
  4. import org.jsoup.nodes.Document;
  5. import org.jsoup.nodes.Element;
  6. import org.jsoup.select.Elements;

  7. import java.io.File;
  8. import java.io.FileWriter;
  9. import java.io.IOException;
  10. import java.util.Scanner;

  11. /**
  12. * Maven爬虫
  13. *
  14. * @author IBIT程序猿
  15. */
  16. public class MavenCrawler {

  17. /**
  18. * 爬取跟目录
  19. */
  20. private static final String ROOT = "https://repo.maven.apache.org/";

  21. /**
  22. * maven-metadata.xml文件名
  23. */
  24. private static final String MAVEN_METADATA_XML_FILENAME = "maven-metadata.xml";

  25. public static void main(String[] args) {

  26. // 参数说明
  27. // args[0]: 爬取目录
  28. // args[1]: sleep毫秒数
  29. // args[2]: 开始层级(可选)
  30. // args[3]: 开始行(可选)

  31. String dirPath = args[0];

  32. File dir = new File(dirPath);
  33. if (!dir.exists() || !dir.isDirectory()) {
  34. System.err.println("爬取目录不存在,dir: " + dirPath);
  35. System.exit(1);
  36. }

  37. int sleepMillis = Integer.parseInt(args[1]);

  38. int level = 0;
  39. if (args.length > 2) {
  40. level = Integer.parseInt(args[2]);
  41. }

  42. String beginLine = null;
  43. if (args.length > 3) {
  44. beginLine = args[3];
  45. }


  46. File urlFile;
  47. boolean begin = null == beginLine;

  48. while ((urlFile = getLevelFile(dir, level)).exists()) {
  49. level++;
  50. boolean fileEmpty = true;
  51. File subFile = getLevelFile(dir, level);
  52. try (Scanner scanner = new Scanner(urlFile);
  53. FileWriter writer = new FileWriter(subFile)) {
  54. while (scanner.hasNext()) {
  55. String line = scanner.nextLine();
  56. if (StringUtils.isNotBlank(line)) {
  57. fileEmpty = false;
  58. if (!begin && line.equals(beginLine)) {
  59. begin = true;
  60. }
  61. if (begin) {
  62. String url = ROOT + line;
  63. findSubUrl(url, sleepMillis, writer);
  64. }
  65. }
  66. }

  67. } catch (IOException e) {
  68. e.printStackTrace();
  69. }
  70. if (fileEmpty) {
  71. urlFile.deleteOnExit();
  72. subFile.deleteOnExit();
  73. break;
  74. }
  75. }

  76. }

  77. /**
  78. * 获取文件
  79. *
  80. * @param dir 目录
  81. * @param level 等级
  82. * @return 文件
  83. */
  84. private static File getLevelFile(File dir, int level) {
  85. return new File(dir.getAbsolutePath() + File.separator + "level_" + level + ".txt");
  86. }


  87. /**
  88. * 查询子url
  89. *
  90. * @param url 当前url
  91. * @param sleepMillis 睡眠毫秒数
  92. * @param writer writer
  93. */
  94. private static void findSubUrl(String url, int sleepMillis, FileWriter writer) {
  95. try {
  96. if (url.endsWith(MAVEN_METADATA_XML_FILENAME)) {
  97. return;
  98. }
  99. Thread.sleep(sleepMillis);
  100. Document doc = Jsoup.connect(url).get();
  101. Elements links = doc.select("#contents a");
  102. for (Element link : links) {
  103. String absUrl = link.absUrl("href");
  104. // 非子目录
  105. if (!absUrl.contains(url) || url.equals(absUrl)) {
  106. continue;
  107. }
  108. String relativePath = absUrl.substring(url.length());
  109. if (MAVEN_METADATA_XML_FILENAME.equals(relativePath) || !relativePath.contains(".")) {
  110. String path = absUrl.substring(ROOT.length());
  111. writer.write(path + "\n");
  112. writer.flush();
  113. System.out.println(path);
  114. }
  115. }
  116. } catch (IOException | InterruptedException e) {
  117. e.printStackTrace();
  118. }

  119. }

  120. }

说明:

  1. 需要在保存的文件夹中新建level_0.txt文件,并将初始url https://repo.maven.apache.org/maven2/ 放置于文件中。执行过程中,会按照遍历目录的深度,生成level_1.txt, level_2.txt等。。
  2. 当前示例代码使用单线程,并设置睡眠时间(避免ip被封),如果需要改为多线程,自行设计。

解析 maven-metadata.xml 示例代码

  1. package tech.ibit.crawler;


  2. import org.apache.commons.collections4.CollectionUtils;
  3. import org.apache.commons.io.IOUtils;
  4. import org.apache.commons.lang.StringUtils;
  5. import org.w3c.dom.Document;
  6. import org.w3c.dom.Node;
  7. import org.w3c.dom.NodeList;

  8. import javax.xml.parsers.DocumentBuilder;
  9. import javax.xml.parsers.DocumentBuilderFactory;
  10. import java.io.ByteArrayInputStream;
  11. import java.io.File;
  12. import java.io.FileWriter;
  13. import java.io.IOException;
  14. import java.net.URL;
  15. import java.nio.charset.StandardCharsets;
  16. import java.util.LinkedHashSet;
  17. import java.util.Scanner;
  18. import java.util.Set;

  19. /**
  20. * Maven meta
  21. *
  22. * @author IBIT程序猿
  23. */
  24. public class MavenMetaDataParser {

  25. /**
  26. * 爬取跟目录
  27. */
  28. private static final String ROOT = "https://repo.maven.apache.org/";

  29. /**
  30. * maven-metadata.xml文件名
  31. */
  32. private static final String MAVEN_METADATA_XML_FILENAME = "maven-metadata.xml";


  33. public static void main(String[] args) {

  34. // 参数说明
  35. // args[0]: 爬取目录
  36. // args[1]: sleep毫秒数
  37. // args[2]: 开始层级
  38. // args[3]: 结束层级
  39. // args[4]: 开始行(可选)
  40. if (args.length < 4) {
  41. System.err.println("参数:爬取目录 sleep毫秒数 开始层级 结束层级 开始行(可选)");
  42. System.exit(1);
  43. }

  44. String dirPath = args[0];

  45. File dir = new File(dirPath);
  46. if (!dir.exists() || !dir.isDirectory()) {
  47. System.err.println("爬取目录不存在,dir: " + dirPath);
  48. System.exit(1);
  49. }


  50. int sleepMillis = Integer.parseInt(args[1]);
  51. int beginLevel = Integer.parseInt(args[2]);
  52. int endLevel = Integer.parseInt(args[3]);


  53. String beginLine = null;

  54. if (args.length > 4) {
  55. beginLine = args[4];
  56. }

  57. boolean begin = null == beginLine;
  58. for (int i = beginLevel; i <= endLevel; i++) {
  59. File urlFile = getLevelFile(dir, i);
  60. if (!urlFile.exists()) {
  61. break;
  62. }

  63. try (Scanner scanner = new Scanner(urlFile);
  64. FileWriter writer = new FileWriter(getVersionLevelFile(dir, i))) {
  65. while (scanner.hasNext()) {
  66. String line = scanner.nextLine();
  67. if (StringUtils.isNotBlank(line)) {
  68. if (!begin && line.equals(beginLine)) {
  69. begin = true;
  70. }
  71. if (begin && line.endsWith(MAVEN_METADATA_XML_FILENAME)) {
  72. String url = ROOT + line;
  73. appendVersions(url, sleepMillis, writer);
  74. }
  75. }
  76. }
  77. } catch (IOException e) {
  78. e.printStackTrace();
  79. }

  80. }

  81. }

  82. /**
  83. * 生成版本
  84. *
  85. * @param url url
  86. * @param sleepMillis 睡眠毫秒数
  87. * @param writer writer
  88. */
  89. private static void appendVersions(String url, int sleepMillis, FileWriter writer) {
  90. try {
  91. Thread.sleep(sleepMillis);
  92. String xmlContent = IOUtils.toString(new URL(url), StandardCharsets.UTF_8);
  93. DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
  94. DocumentBuilder builder = factory.newDocumentBuilder();
  95. try (ByteArrayInputStream in = new ByteArrayInputStream(xmlContent.getBytes(StandardCharsets.UTF_8))) {
  96. Document doc = builder.parse(in);

  97. String groupId = getSingleValue(doc, "groupId");
  98. if (StringUtils.isBlank(groupId)) {
  99. return;
  100. }

  101. String artifactId = getSingleValue(doc, "artifactId");
  102. if (StringUtils.isBlank(artifactId)) {
  103. return;
  104. }

  105. Set<String> versions = getMultiValues(doc, "version");
  106. if (CollectionUtils.isEmpty(versions)) {
  107. return;
  108. }

  109. String versionLine = groupId + ":" + artifactId + ":" + StringUtils.join(versions, ",");
  110. writer.write(versionLine + "\n");
  111. writer.flush();
  112. System.out.println(versionLine);
  113. }


  114. } catch (Exception e) {
  115. e.printStackTrace();
  116. }

  117. }

  118. /**
  119. * 获取文件
  120. *
  121. * @param dir 目录
  122. * @param level 等级
  123. * @return 文件
  124. */
  125. private static File getLevelFile(File dir, int level) {
  126. return new File(dir.getAbsolutePath() + File.separator + "level_" + level + ".txt");
  127. }

  128. /**
  129. * 获取文件
  130. *
  131. * @param dir 目录
  132. * @param level 等级
  133. * @return 文件
  134. */
  135. private static File getVersionLevelFile(File dir, int level) {
  136. return new File(dir.getAbsolutePath() + File.separator + "version_level_" + level + ".txt");
  137. }

  138. /**
  139. * 获取单个值
  140. *
  141. * @param document 文档
  142. * @param tagName 标签名称
  143. * @return 单个值
  144. */
  145. private static String getSingleValue(Document document, String tagName) {
  146. NodeList nodeList = document.getElementsByTagName(tagName);
  147. if (nodeList.getLength() == 0) {
  148. return null;
  149. }
  150. return getNodeValue(nodeList.item(0));
  151. }


  152. /**
  153. * 获取多个值
  154. *
  155. * @param document 文档
  156. * @param tagName 标签名称
  157. * @return 值集合
  158. */
  159. private static Set<String> getMultiValues(Document document, String tagName) {
  160. Set<String> values = new LinkedHashSet<>();
  161. NodeList nodeList = document.getElementsByTagName(tagName);
  162. for (int i = 0; i < nodeList.getLength(); i++) {
  163. String value = getNodeValue(nodeList.item(i));
  164. if (null != value) {
  165. values.add(value);
  166. }
  167. }
  168. return values;
  169. }

  170. /**
  171. * 获取节点值
  172. *
  173. * @param node 节点
  174. * @return 节点值
  175. */
  176. private static String getNodeValue(Node node) {
  177. if (null == node) {
  178. return null;
  179. }
  180. return node.getFirstChild().getNodeValue();
  181. }
  182. }

说明

  1. 该示例代码就是读取爬虫生成的level_x.txt文件中的maven-metadata.xml文件,并解析出对应的groupId, artifactId, version
  2. 当前示例代码使用单线程,并设置睡眠时间(避免ip被封),如果需要改为多线程,自行设计。

其他说明,pom.xml引入依赖说明

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.jsoup</groupId>
  4. <artifactId>jsoup</artifactId>
  5. <version>1.14.3</version>
  6. </dependency>


  7. <dependency>
  8. <groupId>commons-lang</groupId>
  9. <artifactId>commons-lang</artifactId>
  10. <version>2.6</version>
  11. </dependency>

  12. <dependency>
  13. <groupId>org.apache.commons</groupId>
  14. <artifactId>commons-collections4</artifactId>
  15. <version>4.4</version>
  16. </dependency>

  17. <dependency>
  18. <groupId>commons-io</groupId>
  19. <artifactId>commons-io</artifactId>
  20. <version>2.11.0</version>
  21. </dependency>
  22. </dependencies>