在本地 idea 中自动生成牛客刷题列表
前言
前几天使用牛客 “题库” 刷题时,尽管牛客本身的代码提示足够智能,但发现还是更喜欢本地 idea 进行编译调试。
尤其是带有 链表 ListNode 或者二叉树 TreeNode 的题目。本地开发调试会更加方便。
同时,将所有刷题记录都保存在一个工程中,也更方便后续的复习查看。
但是,如果在本地一个个新建题目类和写测试类,又显得特别的累赘繁琐。
有没有办法 自动生成 这部分内容呢?
思路分析
以剑指offer 这个专题为例。https://www.nowcoder.com/exam/oj/ta?tpId=13
通过 F12 查看可以发现,题目列表的 JSON 都保存在如下请求中:
https://www.nowcoder.com/api/questiontraining/coding/getTopicQuestion?pageSize=100&title=&topicId=265&page=1&_=166****285152
在请求结果 JSON 中,可以看到,题目难度保存在 difficultyArray 中,而题目列表保存在 questions 中。
从而,我们可以分析该 JSON,来获取生成相应的 Java 类。
为了后续生成 测试类方便,我们可以同时生成对应的题目 Markdown 文档,最终从 Markdown 中,生成对应的测试类。
生成 Java 类
查看其中一个题目的 JSON。如下所示:
{ "difficulty": 2, "topicUrl": "/ta/coding-interviews-all", "tpId": 265, "questionId": 1375279, "questionTitle": "数组中重复的数字", "acceptRate": 54.5677, "questionNo": "JZ3", "isNew": false, "questionUUid": "6fe361ede7e54db1b84adc81d09d8524", "tqId": 39207, "tags": [ { "name": "数组", "id": 578 } ] }
我们可以在 questionTitle 中得到题目名称,从 questionUUid 得到题目的 UUID,进而得到题目的 URL。
如 "6fe361ede7e54db1b84adc81d09d8524" 这个 UUID,可以链接到对应的题目:https://www.nowcoder.com/practice/6fe361ede7e54db1b84adc81d09d8524
生成类名
可以看到,questionTitle 是中文,我们如果想作为 Java 类名的话,需要转换为英文。
查询可以发现开源工具 https://github.com/houbb/pinyin 可以将中文转换为对应的拼音,从而我们可以转换得到类名。
编写对应的转换方法,如下所示:
public static String toPinyin(String info) { if (StringUtils.isBlank(info)) { return null; } if (info.contains("(")) { info = info.split("(")[0]; } if (info.contains("(")) { info = info.split("\\(")[0]; } String pinyin = PinyinHelper.toPinyin(info, PinyinStyleEnum.NORMAL); List<String> list = new LinkedList<>(); for (String t : pinyin.split(" ")) { list.add(StringUtils.capitalize(t)); } return String.join("", list).replaceAll("[^a-zA-Z0-9]", ""); }
可以发现,questionTitle 的中文中,存在部分括号、和标点,这部分我们进行过滤去除。
同时,为了检索方便,我们将 questionNo 也放到类名中。
为了排序方便,JZ3 我们也转换为 JZ03,加0补齐。
最终生成的类名如:JZ03_ShuZuZhongChongFuDeShuZi
获取题目列表
通过 JSON 遍历,得到所有题目的 QuestionInfo 对象 。
public static List<QuestionInfo> getQuestionInfoList(String html) { JSONObject contentJson = JSONObject.parseObject(html); JSONObject dataJson = contentJson.getJSONObject("data"); JSONArray questionsArray = dataJson.getJSONArray("questions"); JSONArray difficultyArray = dataJson.getJSONArray("difficultyArray"); Map<Integer, String> difficultyMap = new HashMap<>(); for (int i = 0; i < difficultyArray.size(); i++) { JSONObject jsonObject = difficultyArray.getJSONObject(i); difficultyMap.put(jsonObject.getInteger("id"), jsonObject.getString("title")); } List<QuestionInfo> questionInfos = JSONObject.parseArray(questionsArray.toString(), QuestionInfo.class); for (QuestionInfo info : questionInfos) { info.setDifficultyInfo(difficultyMap.getOrDefault(info.getDifficulty(), String.valueOf(info.getDifficulty()))); } return questionInfos; } public static List<QuestionInfo> getQuestionInfoList() { String content = LoadTestCaseData.loadInfo(CODE_PATH); List<QuestionInfo> questionInfos = getQuestionInfoList(content); return questionInfos; }
在上述方法中,我们将题目的“难度”也获取了出来。
生成Java 类
对每个题目,生成Java类:
public static void createJavaName(QuestionInfo questionInfo, String type) { try { File file = new File(getJavaPath(questionInfo)); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } if (!file.exists()) { file.createNewFile(); } else { System.out.println(questionInfo.getJavaName() + " is exists."); return; } FileOutputStream out = new FileOutputStream(file, true); StringBuffer sb = new StringBuffer(); sb.append("package com.jueee.nowcoder." + type + ";\n\n\n"); sb.append(getClassAnnotation(questionInfo)); sb.append("public class " + questionInfo.getJavaName() + " {\n\n\n\n}"); out.write(sb.toString().getBytes("utf-8")); out.close(); } catch (Exception e) { e.printStackTrace(); } return; }
其中,getClassAnnotation 方法表示注释,由于测试类中也有使用,我们将其独立出来。
public static String getClassAnnotation(QuestionInfo questionInfo) { StringBuffer buffer = new StringBuffer(); buffer.append("/**\n"); buffer.append(" * " + questionInfo.getQuestionNo() + ":" + questionInfo.getQuestionTitle() + "\n"); buffer.append(" * 链接:https://www.nowcoder.com/practice/" + questionInfo.getQuestionUUid() + "\n"); buffer.append(" * 难度:" + questionInfo.getDifficultyInfo() + "\n"); buffer.append(" * 描述:" + questionInfo.getMarkDownName() + ".md\n"); buffer.append(" * " + questionInfo.getTagInfo() + "\n"); buffer.append(" */\n"); return buffer.toString(); }
生成Markdown 文件
同理,我们生成MarkDown的文档。
public static void createMarkDown(QuestionInfo questionInfo) { try { File file = new File(getMarkPath(questionInfo)); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } if (!file.exists()) { file.createNewFile(); } else { System.out.println(questionInfo.getMarkDownName() + " is exists."); return; } FileOutputStream out = new FileOutputStream(file, true); StringBuffer buffer = new StringBuffer(); buffer.append("## " + questionInfo.getQuestionTitle() + " \r"); buffer.append("链接:[" + questionInfo.getQuestionNo() + "](" + questionInfo.getTitleUrl() + ") \r"); buffer.append("难度:**" + questionInfo.getDifficultyInfo() + "** \r"); buffer.append("实现:" + questionInfo.getJavaName() + ".java \r"); buffer.append("" + questionInfo.getTagInfo() + " \r"); buffer.append(" \r\r"); out.write(buffer.toString().getBytes("utf-8")); out.close(); } catch (Exception e) { e.printStackTrace(); } }
最终效果
类文件和MarkDown文件:
对于每个Java类,内容如下所示:
对于每个MarkDown文件,内容如下所示:
关于测试类和测试方法的生成,会比较复制,我们下次再聊。
如果有其他问题,可以在本贴下面留言交流。
#刷题##笔试##题库##网易##Java#