coding 笔记、点滴记录,以后的文章也会同步到公众号(Coding Insight)中,希望大家关注^_^
最近想将自己 LeetCode 账号的题目,按照题号顺序将题解整理到 GitHub 上,但是现在 LeetCode 有 1500 道题目(其实我也就做了400+),手动工作量太大,研究了一下午写了爬虫程序,结尾附源码。
首先打开官网:https://leetcode-cn.com/problemset/algorithms/
打开控制台,细心能发现题解的链接:
可以看到这个接口每道题目都有,不过是倒叙排列,题号最大的在最上面。这里比较重要的有下面几个参数:
数据类定义如下:
@Data
public class TitleVo {
private String user_name;
private String category_slug;
private List<StatStatusPairsBean> stat_status_pairs;
@Data
public static class StatStatusPairsBean {
private StatBean stat;
private boolean paid_only;
private boolean is_favor;
private int frequency;
private int progress;
@Data
public static class StatBean {
private int question_id;
private String question__title;
private String question__title_slug;
private boolean question__hide;
private String frontend_question_id;
}
}
}
这个接口不需要登录态,仅一个 get 请求就能够获取所有题目。使用Java爬取代码如下:
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://leetcode-cn.com/api/problems/algorithms/").method("GET", null)
.build();
Response response = client.newCall(request).execute();
String body = response.body().string();
TitleVo titleVo = JSON.parseObject(body, TitleVo.class);
为了以后方便查找和复习,希望将文件命名成 L0010_Regular_Expression_Matching
这样的格式,注意题目的 title 可能包含 -
、空格、英文括号等,需要特殊处理,否则是不合法的 Java 文件名称(有几个类似于 「L1000056_拿硬币」 的竞赛题目,没有英文,暂不处理)。
代码如下:
// 1. 生成 title
String title = "L" + String.format("%04d", bean.getStat().getQuestion_id())
+ "_"
+ bean.getStat().getQuestion__title().replaceAll(" ", "_")
.replaceAll("\\(", "").replaceAll("\\)", "")
.replaceAll("-", "_").replaceAll(",", "");
以第一题为例,点击代码编辑区域上访的 {},获取最近一次提交的代码。
在控制台能够看到 https://leetcode-cn.com/submissions/latest/?qid=1&lang=java 接口,即为自己提交的代码。
这个接口需要登录态,使用代码请求的时候需要带上 cookie,下面的代码 cookie 为空,在使用的时候需要填入自己的 cookie。注意有些题目爬取获取不到最近提交的代码,比如我在测试时前20题的第6题和第13题一直获取不到题解,或者400道之后的题目我都没有提交过题目,所以需要直接获取原题目代码。原题目代码的类是 QuestionVo
,不详细叙述,直接贴代码。
private String getRecentSubmitCode(int qid, String titleSlug) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(String.format("https://leetcode-cn.com/submissions/latest/?qid=%d&lang=java",
qid))
.method("GET", null)
.addHeader("Cookie", "")
.build();
try {
Response response = client.newCall(request).execute();
CodeVo codeVo = JSON.parseObject(response.body().string(), CodeVo.class);
if (codeVo != null && StringUtils.isNotEmpty(codeVo.getCode())) {
return codeVo.getCode();
}
} catch (IOException e) {
e.printStackTrace();
}
QuestionVo questionVo = getQuestionVo(titleSlug);
if (questionVo != null) {
QuestionVo.DataBean.QuestionBean.CodeSnippetsBean javaCode = questionVo.getData()
.getQuestion().getCodeSnippets().stream()
.filter(q -> Objects.equals(q.getLang(), "Java")).findFirst().orElse(null);
return javaCode.getCode();
}
return "";
}
CodeVo 类:
@Data
public class CodeVo {
private String code;
}
QuestionVo 类(在 CodeVo 为空时使用):
@Data
public class QuestionVo {
@SerializedName("data")
private DataBean data;
public static class DataBean {
@SerializedName("question")
private QuestionBean question;
public QuestionBean getQuestion() {
return question;
}
public void setQuestion(QuestionBean question) {
this.question = question;
}
@Data
public static class QuestionBean {
@SerializedName("questionId")
private String questionId;
private String questionFrontendId;
private int boundTopicId;
private String title;
private String titleSlug;
private String content;
private String translatedTitle;
private String translatedContent;
private Object isLiked;
private String stats;
private SolutionBean solution;
private String status;
private String metaData;
private boolean judgerAvailable;
private String judgeType;
private boolean enableRunCode;
private String envInfo;
private String dailyRecordStatus;
private String style;
private String __typename;
private List<TopicTagsBean> topicTags;
private List<CodeSnippetsBean> codeSnippets;
@Data
public static class SolutionBean {
private String id;
private boolean canSeeDetail;
private String __typename;
}
@Data
public static class TopicTagsBean {
private String name;
private String slug;
private String translatedName;
private String __typename;
}
@Data
public static class CodeSnippetsBean {
private String lang;
private String langSlug;
private String code;
private String __typename;
}
}
}
}
总结了下基本就是 HashMap、Arrays 之类的 JDK 基础类,在上面获取到的题解代码中如果存在某些关键字,就直接在题解代码最上面加入对应的 import。代码如下:
private StringBuilder getImports(TitleVo.StatStatusPairsBean bean, String recentSubmitCode) {
StringBuilder importSb = new StringBuilder();
if (recentSubmitCode.contains("HashMap")) {
importSb.append("import java.util.HashMap;\n");
}
if (recentSubmitCode.contains("Map<")) {
importSb.append("import java.util.Map;\n");
}
if (recentSubmitCode.contains("ListNode")) {
importSb.append("import common.ListNode;\n");
}
if (recentSubmitCode.contains("Node")) {
importSb.append("import common.Node;\n");
}
if (recentSubmitCode.contains("Arrays")) {
importSb.append("import java.util.Arrays;\n");
}
if (recentSubmitCode.contains("List<")) {
importSb.append("import java.util.List;\n");
}
if (recentSubmitCode.contains("ArrayList")) {
importSb.append("import java.util.ArrayList;\n");
}
if (recentSubmitCode.contains("HashSet")) {
importSb.append("import java.util.HashSet;\n");
}
if (recentSubmitCode.contains("Set<")) {
importSb.append("import java.util.Set;\n");
}
if (recentSubmitCode.contains("Stack<")) {
importSb.append("import java.util.Stack;\n");
}
if (recentSubmitCode.contains("TreeNode")) {
importSb.append("import common.TreeNode;\n");
}
importSb.append(String.format("\n// https://leetcode-cn.com/problems/%s/\n",
bean.getStat().getQuestion__title_slug()));
return importSb;
}
使用的是 org.apache.commons.io.FileUtils
,将上述代码转换并写入文件即可。
FileUtils.writeByteArrayToFile(new File("./" + title + ".java"),
recentSubmitCode.getBytes());
@Test
public void test() throws IOException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://leetcode-cn.com/api/problems/algorithms/").method("GET", null)
.build();
Response response = client.newCall(request).execute();
String body = response.body().string();
TitleVo titleVo = JSON.parseObject(body, TitleVo.class);
titleVo.getStat_status_pairs().stream()
.filter(bean -> bean.getStat().getQuestion_id() < 1500)
.forEach(bean -> {
try {
// 1. 生成 title
String title = "L" + String.format("%04d", bean.getStat().getQuestion_id())
+ "_"
+ bean.getStat().getQuestion__title().replaceAll(" ", "_")
.replaceAll("\\(", "").replaceAll("\\)", "")
.replaceAll("-", "_").replaceAll(",", "");
System.out.println(title);
// 2. 填入代码
String recentSubmitCode = getRecentSubmitCode(
bean.getStat().getQuestion_id(),
bean.getStat().getQuestion__title_slug());
if (StringUtils.isNotEmpty(recentSubmitCode)) {
recentSubmitCode = recentSubmitCode.replaceFirst("class Solution",
"class " + title);
// System.out.println(recentSubmitCode);
}
// 3. 若存在 Arrays、TreeNode 等类,增加 import
recentSubmitCode = getImports(bean, recentSubmitCode) + recentSubmitCode;
// 4. 生成 java 文件
FileUtils.writeByteArrayToFile(new File("./" + title + ".java"),
recentSubmitCode.getBytes());
TimeUnit.MILLISECONDS.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
});
}
最终生成的文件如下,同时增加了题目的中文官网链接,方便刷题、测试: