Merge remote-tracking branch 'origin/后台运营yy' into 后台运营yy

后台运营yy
yangyang 8 months ago
commit c140b86d46

@ -1,23 +1,44 @@
package com.tamguo;
import org.junit.Test;
// 引入JUnit框架中的@Test注解用于标记一个方法是测试方法JUnit在运行测试时会执行被该注解标记的方法
// 并根据方法内的逻辑判断测试是否通过。
import org.junit.runner.RunWith;
// 用于指定JUnit运行测试时使用的运行器Runner不同的运行器可以提供不同的测试执行环境和功能。
import org.springframework.beans.factory.annotation.Autowired;
// Spring框架的注解用于自动装配依赖注入Spring会根据类型等规则自动查找并注入相应的Bean实例到标注该注解的变量中。
import org.springframework.boot.test.context.SpringBootTest;
// 这个注解用于标记一个测试类它会启动一个完整的Spring Boot应用上下文以便在测试中可以加载整个应用的配置
// 并且能够像在实际运行环境中一样使用各种Spring管理的组件如自动注入的Bean等方便进行集成测试等操作。
import org.springframework.test.context.junit4.SpringRunner;
// Spring提供的用于JUnit 4的运行器结合@RunWith(SpringRunner.class)使用可以让JUnit测试运行在Spring的测试环境下
// 从而支持Spring相关的功能如依赖注入、事务管理等在测试中的应用。
import com.tamguo.service.IBookService;
// 引入自定义的服务层接口IBookService从名称推测该接口可能定义了与书籍相关的业务操作方法
// 在本测试类中会通过依赖注入获取其实现类的实例来调用具体的业务方法进行测试。
// BookCrawler类是一个用于测试的类它使用了Spring Boot和JUnit相关的注解来构建测试环境
// 目的是对与书籍相关的业务逻辑通过IBookService实现进行测试。
@RunWith(SpringRunner.class)
@SpringBootTest
public class BookCrawler {
// 使用@Autowired注解自动注入一个实现了IBookService接口的Bean实例到bookService变量中
// 这样在测试方法中就可以直接使用这个服务层实例来调用相应的业务方法了。
@Autowired
IBookService bookService;
// 用@Test注解标记的测试方法方法名为crawlerBook用于测试书籍爬取相关的业务逻辑。
// 该方法声明可能抛出Exception异常意味着在执行过程中如果出现了未处理的异常情况JUnit会相应地记录测试失败并输出异常信息。
@Test
public void crawlerBook() throws Exception {
// 调用bookService实例的crawlerBook方法该方法具体实现应该位于IBookService接口的实现类中
// 推测其功能是执行书籍爬取的操作,比如从网络上获取书籍信息等,这里通过调用该方法来验证相关业务逻辑是否正确执行。
this.bookService.crawlerBook();
}
}
}

@ -1,23 +1,48 @@
package com.tamguo;
import org.junit.Test;
// 引入JUnit框架中的@Test注解它的作用是标记当前类中的某个方法为测试方法。
// 在运行测试时JUnit会识别并执行被该注解标记的方法然后依据方法内部的逻辑以及最终执行结果等来判断测试是否通过。
import org.junit.runner.RunWith;
// 此注解用于指定JUnit运行测试时所采用的运行器Runner
// 不同的运行器能为测试提供不同的执行环境与功能支持,以此满足多样化的测试需求。
import org.springframework.beans.factory.annotation.Autowired;
// Spring框架提供的注解主要功能是实现自动装配依赖注入
// Spring容器会按照一定的规则比如根据类型等自动查找匹配的Bean实例并将其注入到标注了该注解的变量中方便在代码中直接使用对应的组件。
import org.springframework.boot.test.context.SpringBootTest;
// 该注解用于标记一个测试类它会启动完整的Spring Boot应用上下文。
// 这意味着在测试过程中,能够加载整个应用程序所配置的各种组件、配置信息等,如同应用在实际运行环境中一样,方便进行集成测试等相关操作。
import org.springframework.test.context.junit4.SpringRunner;
// Spring为JUnit 4提供的运行器类结合@RunWith(SpringRunner.class)这种使用方式,
// 可以让JUnit测试运行在Spring所构建的测试环境之下进而使Spring相关的诸多特性如依赖注入、事务管理等能够在测试过程中得以运用。
import com.tamguo.service.IChapterService;
// 引入自定义的服务层接口IChapterService从接口名称推测其应该定义了与章节相关的一系列业务操作方法
// 在本测试类中,将会通过依赖注入获取该接口实现类的实例,进而调用相应的业务方法来进行测试操作。
// ChapterCrawler类是一个专门用于测试的类它借助Spring Boot和JUnit相关的注解搭建起测试环境
// 旨在对和章节相关的业务逻辑通过IChapterService实现进行测试验证。
@RunWith(SpringRunner.class)
@SpringBootTest
public class ChapterCrawler {
@Autowired
IChapterService iChapterService;
// 使用@Autowired注解让Spring容器自动将实现了IChapterService接口的Bean实例注入到iChapterService变量中。
// 如此一来,在后续的测试方法里就能直接利用这个注入的服务实例去调用相应的章节相关业务方法了。
@Autowired
IChapterService iChapterService;
// 使用@Test注解标记的测试方法名为crawlerChapter其主要作用是测试章节爬取相关的业务逻辑是否正确。
// 该方法声明了可能抛出Exception异常意味着如果在方法执行期间出现了未被处理的异常情况JUnit会将此次测试标记为失败并输出相应的异常信息。
@Test
public void crawlerChapter() throws Exception {
iChapterService.crawlerChapter();
// 调用iChapterService实例的crawlerChapter方法该方法的具体实现应该位于IChapterService接口的实现类当中。
// 从方法名推测,其功能大概率是执行章节爬取的操作,比如从网络或者其他数据源获取章节相关的信息等,
// 通过在这里调用该方法来检验与之相关的业务逻辑能否按照预期正常执行。
iChapterService.crawlerChapter();
}
}
}

@ -1,22 +1,48 @@
package com.tamguo;
import com.tamguo.service.ICrawlerBookService;
// 引入自定义的服务层接口ICrawlerBookService从接口名称可以推测它应该定义了与书籍爬取相关的业务操作方法
// 这些方法会在具体的实现类中实现相应的逻辑,而本测试类会通过依赖注入获取其实现类的实例来进行测试操作。
import org.junit.Test;
// 该注解来自JUnit测试框架用于标识下面的方法是一个测试方法。在运行测试时JUnit会执行被此注解标记的方法
// 并根据方法内的逻辑执行情况以及可能出现的异常等来判断该测试是否通过。
import org.junit.runner.RunWith;
// 此注解用于指定JUnit测试运行时所采用的运行器Runner。不同的运行器能提供不同的测试执行环境和功能特性
// 这里通过指定运行器来确保测试能在符合要求的Spring相关环境下进行。
import org.springframework.beans.factory.annotation.Autowired;
// 这是Spring框架提供的用于依赖注入自动装配的注解。Spring容器会依据类型等规则自动查找并将合适的Bean实例注入到标注该注解的变量中
// 使得代码可以方便地获取和使用其他组件在这里就是将实现了ICrawlerBookService接口的实例注入进来。
import org.springframework.boot.test.context.SpringBootTest;
// 该注解用于标记这是一个Spring Boot应用的测试类它会启动完整的Spring Boot应用上下文加载整个应用的配置信息、组件等
// 就如同应用在实际运行环境中一样从而便于进行集成测试等操作让测试能够使用到所有已配置好的Spring相关资源。
import org.springframework.test.context.junit4.SpringRunner;
// Spring为JUnit 4提供的运行器结合@RunWith(SpringRunner.class)的使用能让JUnit测试运行在Spring构建的测试环境下
// 进而可以利用Spring的各种特性比如依赖注入、事务管理等在测试过程中发挥作用。
// CrawlerBookCrawler类是一个基于Spring Boot和JUnit的测试类其主要目的是对与书籍爬取相关的业务逻辑进行测试
// 通过使用相关注解搭建起合适的测试环境,并借助依赖注入获取对应的服务实例来执行具体的测试操作。
@RunWith(SpringRunner.class)
@SpringBootTest
public class CrawlerBookCrawler {
@Autowired
// 使用@Autowired注解让Spring容器自动查找并注入一个实现了ICrawlerBookService接口的Bean实例到crawlerBookService变量中。
// 这样在后续的测试方法里,就能直接利用这个服务实例去调用与书籍爬取相关的业务方法了。
@Autowired
ICrawlerBookService crawlerBookService;
// 使用@Test注解标记的测试方法名为crawlerBook其功能是测试书籍爬取相关的具体业务逻辑。
// 该方法声明抛出Exception异常表示如果在方法执行过程中出现了未处理的异常情况JUnit会将此次测试判定为失败并输出相应的异常信息。
@Test
public void crawlerBook() throws Exception {
// 调用crawlerBookService实例的crawlerBook方法这个方法的具体实现应该在ICrawlerBookService接口的实现类中定义
// 从方法名推测,其主要作用是执行实际的书籍爬取操作,例如从网络上抓取书籍的相关信息等,
// 通过调用该方法来验证相关的书籍爬取业务逻辑能否正确执行,达到预期的效果。
crawlerBookService.crawlerBook();
}
}
}

@ -1,16 +1,33 @@
package com.tamguo;
import org.junit.Test;
// 引入JUnit框架中的@Test注解用于标记下方的方法为测试方法。JUnit在执行测试任务时会识别并执行被该注解标记的方法
// 随后依据方法内部的代码逻辑执行情况(例如是否抛出异常、返回值是否符合预期等)来判定该测试是否通过。
import org.junit.runner.RunWith;
// 此注解用于指定JUnit运行测试时所采用的运行器Runner。不同的运行器能为测试提供不同的执行环境与功能支持
// 这里配置的运行器可使测试运行在符合Spring框架要求的特定环境中方便与Spring相关功能进行集成测试。
import org.springframework.beans.factory.annotation.Autowired;
// Spring框架提供的这个注解用于实现自动装配依赖注入功能。Spring容器会按照一定规则如根据类型、名称等自动查找并注入匹配的Bean实例
// 到标注了该注解的变量中从而让代码能够方便地获取和使用其他Spring管理的组件在此处就是将实现IChapterService接口的Bean注入进来。
import org.springframework.boot.test.context.SpringBootTest;
// 该注解用于标记当前类是一个针对Spring Boot应用的测试类。它会启动完整的Spring Boot应用上下文加载整个应用配置的各类资源
// 模拟应用在实际运行时的环境方便在测试中使用各种已配置好的Spring组件进行集成测试、功能验证等操作。
import org.springframework.test.context.junit4.SpringRunner;
// 这是Spring为JUnit 4提供的运行器类当与@RunWith(SpringRunner.class)配合使用时能让JUnit测试运行在Spring构建的测试环境之下
// 进而可以充分利用Spring框架的诸多特性如依赖注入、事务管理等来辅助完成测试工作。
import com.tamguo.service.IChapterService;
// 引入自定义的服务层接口IChapterService从接口名称推测其定义了与章节相关的一系列业务操作方法
// 在本测试类中,会通过依赖注入获取该接口的实现类实例,然后调用相应的业务方法来验证相关业务逻辑的正确性。
/**
* Num -
*
* ModifyChpaterQuestionNum
* Spring BootJUnit
*
* @author tamguo
*
*/
@ -18,12 +35,19 @@ import com.tamguo.service.IChapterService;
@SpringBootTest
public class ModifyChpaterQuestionNum {
@Autowired
IChapterService iChapterService;
// 使用@Autowired注解让Spring容器自动将实现了IChapterService接口的Bean实例注入到iChapterService变量中。
// 如此一来,在后续的测试方法里就能直接使用这个服务实例去调用和章节相关的业务方法,比如此处涉及的修改题目数量的方法。
@Autowired
IChapterService iChapterService;
// 使用@Test注解标记的测试方法名为crawlerSubject这里方法名可能不太准确从功能上推测或许叫modifyQuestionNum更合适不过要结合具体业务情况
// 该方法主要用于测试修改章节题目数量相关的业务逻辑声明抛出Exception异常表示如果在方法执行期间出现了未处理的异常情况
// JUnit会将此次测试判定为失败并输出相应的异常信息便于开发人员排查问题。
@Test
public void crawlerSubject() throws Exception {
iChapterService.modifyQuestionNum();
// 调用iChapterService实例的modifyQuestionNum方法该方法的具体实现应该位于IChapterService接口的实现类中。
// 从方法名可以推断其功能是执行修改章节题目数量的实际操作,通过在这里调用该方法来检验与之相关的业务逻辑能否按照预期正常执行。
iChapterService.modifyQuestionNum();
}
}
}

@ -2,54 +2,104 @@ package com.tamguo;
import com.baomidou.mybatisplus.mapper.Condition;
import com.baomidou.mybatisplus.plugins.Page;
// 引入MyBatis-Plus框架中的相关类Page类用于进行分页相关操作比如设置每页数据量、获取当前页码等方便实现数据的分页查询与处理
// Condition类用于构建查询条件例如可以指定排序规则、筛选条件等在这里用于构建查询数据库的相关条件语句。
import com.tamguo.model.QuestionEntity;
// 引入自定义的QuestionEntity类它应该是对应数据库中题目相关表的实体类包含了题目各个属性的定义如题目内容、答案、解析等用于在代码中承载和操作题目相关的数据。
import com.tamguo.service.IQuestionService;
// 引入自定义的服务层接口IQuestionService从接口名称推测它定义了与题目相关的一系列业务操作方法例如查询题目、更新题目等
// 在本类中会通过依赖注入获取其实现类的实例来调用这些业务方法进行具体的数据处理操作。
import org.junit.Test;
// 引入JUnit框架中的@Test注解用于标记下面的方法为测试方法JUnit在运行测试时会执行被该注解标记的方法
// 并根据方法内的逻辑以及是否抛出异常等情况来判断测试是否通过。
import org.junit.runner.RunWith;
// 用于指定JUnit运行测试时所采用的运行器Runner通过配置合适的运行器可以让测试运行在特定的环境中这里是为了让测试运行在Spring相关的环境下。
import org.springframework.beans.factory.annotation.Autowired;
// Spring框架提供的注解用于自动装配依赖注入Spring容器会根据类型等规则自动查找并注入相应的Bean实例到标注该注解的变量中
// 方便在代码中直接使用对应的服务层组件在这里就是将实现IQuestionService接口的Bean实例注入进来。
import org.springframework.boot.test.context.SpringBootTest;
// 该注解用于标记当前类是一个针对Spring Boot应用的测试类它会启动完整的Spring Boot应用上下文加载整个应用的配置信息和组件
// 模拟应用实际运行的环境,便于进行集成测试等操作,使得测试能够使用到应用中已配置好的各种资源。
import org.springframework.test.context.junit4.SpringRunner;
// Spring为JUnit 4提供的运行器结合@RunWith(SpringRunner.class)使用可以让JUnit测试运行在Spring构建的测试环境下
// 进而支持Spring相关的功能如依赖注入、事务管理等在测试中的应用。
import java.util.Arrays;
// 引入Java标准库中的Arrays类用于操作数组在这里主要是配合Condition类来指定排序字段的列表方便构建按指定字段排序的查询条件。
/**
* Test -
*
* ModifyQuestionImage
* Spring BootJUnit
*
* @author tamguo
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class ModifyQuestionImage {
// 使用@Autowired注解让Spring容器自动将实现了IQuestionService接口的Bean实例注入到iQuestionService变量中。
// 这样在后续的测试方法里就能直接利用这个服务实例去调用与题目相关的业务方法,比如查询题目、更新题目等操作。
@Autowired
private IQuestionService iQuestionService;
// 使用@Test注解标记的测试方法名为modify该方法实现了对题目数据的批量处理逻辑主要是对题目中的文件路径相关字符串进行替换操作并更新到数据库中。
// @SuppressWarnings("unchecked")注解用于抑制编译器的unchecked警告这里可能是因为代码中存在一些泛型相关的操作编译器会产生警告通过此注解告知编译器忽略这些警告。
@SuppressWarnings("unchecked")
@Test
public void modify() {
Integer current = 1 ;
// 当前页码初始化为1表示从第一页数据开始处理用于在分页查询中指定要获取的页码。
Integer current = 1;
// 每页数据量设置为100表示每次分页查询时获取100条题目数据进行处理可根据实际数据量和性能等因素进行调整。
Integer size = 100;
while(true) {
Page<QuestionEntity> page = new Page<QuestionEntity>(current , size);
Page<QuestionEntity> entitys = iQuestionService.selectPage(page , Condition.create().orderAsc(Arrays.asList("id")));
if(entitys.getCurrent() > 759) {
// 使用while循环来实现对所有题目数据的分页遍历处理只要满足循环条件就会一直循环下去直到处理完所有符合条件的数据为止。
while (true) {
// 创建一个Page对象用于分页查询传入当前页码current和每页数据量size作为参数
// 这样就可以通过后续的服务层方法根据这个分页设置去数据库中获取相应的数据。
Page<QuestionEntity> page = new Page<QuestionEntity>(current, size);
// 调用iQuestionService的selectPage方法进行分页查询传入构建好的分页对象page以及查询条件对象。
// 查询条件通过Condition.create().orderAsc(Arrays.asList("id"))构建,意思是按照题目实体类中的"id"字段进行升序排序来获取题目数据,
// 并将查询结果封装到另一个Page对象entitys中该对象包含了当前页的题目数据以及分页相关的其他信息如总页数、总记录数等
Page<QuestionEntity> entitys = iQuestionService.selectPage(page, Condition.create().orderAsc(Arrays.asList("id")));
// 判断当前页码是否大于759如果大于则表示已经处理完了计划处理的数据量这里759可能是根据总数据量和每页数量估算出来的一个边界值具体要结合实际情况
// 如果满足这个条件就跳出while循环结束数据处理流程。
if (entitys.getCurrent() > 759) {
break;
}
// 处理数据
for(int i=0 ; i<entitys.getSize() ; i++) {
// 遍历当前页获取到的题目数据列表,开始对每条题目数据进行处理,这里通过循环依次获取每条题目记录进行相关操作。
for (int i = 0; i < entitys.getSize(); i++) {
// 从当前页的题目数据列表中获取第i条题目记录封装为QuestionEntity对象方便后续对该条题目各个属性进行操作。
QuestionEntity question = entitys.getRecords().get(i);
// 对题目解析内容analysis属性进行字符串替换操作将其中的"files/"替换为"/files/"
// 可能是为了统一文件路径的格式,确保文件能够正确被访问等业务需求,具体取决于实际的文件存储和访问逻辑。
question.setAnalysis(question.getAnalysis().replaceAll("files/", "/files/"));
// 对题目内容content属性也进行同样的字符串替换操作将"files/"替换为"/files/",理由与上述对题目解析的处理类似,都是为了规范文件路径格式。
question.setContent(question.getContent().replaceAll("files/", "/files/"));
if(question.getAnswer() == null) {
// 判断题目答案answer属性是否为null如果是null则将其设置为空字符串
// 这样可以避免后续对null值进行字符串操作时可能出现的空指针异常等问题保证数据的完整性和后续处理的一致性。
if (question.getAnswer() == null) {
question.setAnswer("");
}
// 对题目答案answer属性同样进行字符串替换操作将其中的"files/"替换为"/files/",目的也是规范文件路径相关的字符串内容。
question.setAnswer(question.getAnswer().replaceAll("files/", "/files/"));
}
// 调用iQuestionService的updateAllColumnBatchById方法将处理后的当前页所有题目记录批量更新到数据库中
// 确保对题目数据所做的修改(文件路径字符串替换等操作)能够持久化保存到数据库里,实现数据的更新。
iQuestionService.updateAllColumnBatchById(entitys.getRecords());
// 将当前页码加1准备处理下一页的数据实现分页数据的依次处理不断循环这个过程直到处理完所有符合条件的数据页。
current = current + 1;
// 在控制台打印当前页码信息,方便在程序运行过程中查看数据处理的进度情况,便于调试和监控整个处理流程。
System.out.println("当前Current" + current);
}
}
}
}

@ -1,94 +1,181 @@
package com.tamguo;
import com.xuxueli.crawler.loader.strategy.HtmlUnitPageLoader;
// 引入XxlCrawler框架中的HtmlUnitPageLoader类用于加载HTML页面它基于HtmlUnit库实现能够模拟浏览器行为来获取网页内容是进行网页爬取的重要组件。
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
// 引入Jsoup库中的Document和Element类Jsoup用于解析HTML文档Document代表整个HTML文档Element则用于表示HTML文档中的元素如标签等
// 在网页爬取后解析页面内容时会用到这两个类来提取和操作相关数据。
import org.junit.Test;
// 引入JUnit框架中的@Test注解用于标记下面的方法为测试方法JUnit在运行测试时会执行被该注解标记的方法
// 并根据方法内的逻辑执行情况以及是否抛出异常等来判断测试是否通过。
import org.junit.runner.RunWith;
// 用于指定JUnit运行测试时所采用的运行器Runner通过配置合适的运行器可以让测试运行在特定的环境中这里是为了让测试运行在Spring相关的环境下。
import org.springframework.beans.factory.annotation.Autowired;
// Spring框架提供的注解用于自动装配依赖注入Spring容器会根据类型等规则自动查找并注入相应的Bean实例到标注该注解的变量中
// 方便在代码中直接使用对应的组件在这里就是将相关的数据访问层Mapper接口的实现类实例注入进来。
import org.springframework.boot.test.context.SpringBootTest;
// 该注解用于标记当前类是一个针对Spring Boot应用的测试类它会启动完整的Spring Boot应用上下文加载整个应用的配置信息和组件
// 模拟应用实际运行的环境,便于进行集成测试等操作,使得测试能够使用到应用中已配置好的各种资源。
import org.springframework.test.context.junit4.SpringRunner;
// Spring为JUnit 4提供的运行器结合@RunWith(SpringRunner.class)使用可以让JUnit测试运行在Spring构建的测试环境下
// 进而支持Spring相关的功能如依赖注入、事务管理等在测试中的应用。
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
// 引入阿里巴巴的FastJSON库中的JSONArray和JSONObject类用于处理JSON格式的数据方便在代码中进行JSON数据的构建、解析以及操作
// 例如将对象转换为JSON字符串或者从JSON字符串解析出对象等在这里用于对爬取到的试卷相关数据进行处理和存储。
import com.tamguo.dao.CrawlerPaperMapper;
import com.tamguo.dao.PaperMapper;
// 引入自定义的数据库访问层接口CrawlerPaperMapper和PaperMapper从名称推测它们分别定义了与爬取试卷信息以及试卷基本信息相关的数据库操作方法
// 如插入、查询等操作,在代码中会通过依赖注入获取其实现类实例来与数据库进行交互,实现数据的持久化。
import com.tamguo.model.CrawlerPaperEntity;
import com.tamguo.model.PaperEntity;
// 引入自定义的实体类CrawlerPaperEntity和PaperEntity分别对应数据库中与爬取试卷相关表以及试卷基本信息表的实体对象
// 包含了各自表中字段对应的属性,用于在代码中承载和传递相关数据,方便进行数据的处理和存储操作。
import com.tamguo.model.enums.QuestionType;
// 引入自定义的枚举类QuestionType从名称推测它定义了不同类型的题目类型枚举值用于在代码中明确区分和表示题目所属的类型便于业务逻辑处理。
import com.tamguo.model.vo.PaperVo;
// 引入自定义的视图对象类PaperVo它可能是用于在网页爬取过程中临时封装和传递试卷相关数据的对象包含了如试卷名称、题目信息、URL等属性
// 方便在解析网页和后续数据处理过程中进行数据的传递和操作。
import com.xuxueli.crawler.XxlCrawler;
import com.xuxueli.crawler.parser.PageParser;
import com.xuxueli.crawler.rundata.RunData;
// 引入XxlCrawler框架中的核心类XxlCrawler用于构建和配置爬虫实例PageParser用于定义如何解析爬取到的页面内容RunData用于管理爬虫运行时的数据
// 例如记录已访问的URL、待访问的URL等这些类协同工作来实现网页爬取及相关数据处理的功能。
// 北京模拟试卷,真题试卷已经爬取完毕
// 该类的注释表示北京模拟试卷、真题试卷已经爬取完毕但从代码功能看可能此处在进行其他类型试卷从START_URL及相关逻辑推测的爬取及处理操作
// 不过具体还需结合整体业务情况确定。此为一个基于Spring Boot和JUnit的测试类主要功能是利用XxlCrawler框架进行试卷相关信息的爬取并将数据持久化到数据库中。
@RunWith(SpringRunner.class)
@SpringBootTest
public class PaperCrawler {
// 高考
// 定义高考的学科ID从代码中看它是一个固定的标识字符串用于区分不同学科这里表示高考相关的学科具体对应关系取决于业务逻辑设定。
private final String SUBJECT_ID = "gaokao";
// 科目
// 定义具体的科目ID这里是“生物”科目同样是一个固定标识用于准确标识所爬取试卷所属的具体科目便于后续数据分类、存储等操作。
private final String COURSE_ID = "shengwu";
// 定义区域ID这里的代码注释列出了多个地区的区域ID示例当前设置的是“江西360000”的区域ID用于表明试卷所属的地理区域
// 在数据库存储以及后续按区域筛选、统计试卷等业务场景中会用到该标识。
// 110000 北京 | 310000 上海 | 500000 重庆 | 120000 天津 | 370000 山东 | 410000 河南 | 420000 湖北 | 320000 江苏 | 330000 浙江
// 140000 山西 | 350000 福建 | 340000 安徽 | 220000 吉林 | 150000 内蒙古 | 640000 宁夏 | 650000 新疆 | 广西 450000 | 210000 辽宁
// 230000 黑龙江 | 610000 陕西 | 360000 江西 | 440000 广东 | 430000 湖南 | 460000 海南 | 530000 云南 | 510000 四川 | 630000 青海
// 620000 甘肃 | 130000 河北 | 540000 西藏 | 贵州 520000
private final String AREA_ID = "360000";
// 年份
// 定义年份固定为“2016”用于标识试卷对应的年份在按年份对试卷进行分类、查询以及数据统计等业务场景中会用到该属性。
private final String YEAR = "2016";
// 真题试卷 类型(1:真题试卷,2:模拟试卷,3:押题预测,4:名校精品)
// 定义试卷类型这里设置为“4”根据代码注释中的类型说明1:真题试卷,2:模拟试卷,3:押题预测,4:名校精品),
// 表示当前要爬取和处理的是名校精品类型的试卷,方便后续根据试卷类型进行分类展示、筛选等操作。
private final String PAPER_TYPE = "4";
// 开始采集的URL
// 定义开始采集的URL即爬虫开始爬取试卷信息的初始网页地址这里指向百度题库中特定的试卷列表页面
// 爬虫会从这个URL出发按照设定的规则去获取相关试卷的详细信息以及其他关联数据。
private final String START_URL = "https://tiku.baidu.com/tikupc/paperlist/1bfd700abb68a98271fefa04-20-7-2016-1360-1-download";
// 用于存储爬虫运行时的数据对象通过XxlCrawler框架管理爬虫在运行过程中的各种相关数据如已访问的URL、待访问的URL等
// 在后续的爬虫启动以及数据处理过程中会不断更新和使用这个对象中的数据。
private RunData runData;
// 使用@Autowired注解自动注入PaperMapper接口的实现类实例用于操作试卷基本信息相关的数据库操作
// 比如将爬取到并处理好的试卷基本信息插入到数据库对应的表中,实现数据持久化存储。
@Autowired
private PaperMapper paperMapper;
// 使用@Autowired注解自动注入CrawlerPaperMapper接口的实现类实例用于处理与爬取试卷相关的数据持久化操作
// 例如将爬取过程中涉及的试卷与题目URL等关联信息插入到对应的数据库表中保存爬取过程中的详细数据。
@Autowired
private CrawlerPaperMapper crawlerPaperMapper;
// 使用@Test注解标记的测试方法名为crawler该方法实现了利用XxlCrawler框架进行试卷信息爬取以及将爬取到的数据存储到数据库的完整逻辑。
@Test
public void crawler() {
// 创建一个XxlCrawler实例的构建器通过链式调用一系列方法来配置爬虫的各项参数和行为后续调用build方法即可构建出最终的爬虫实例。
XxlCrawler crawler = new XxlCrawler.Builder()
.setUrls(START_URL)
.setAllowSpread(false)
.setFailRetryCount(5)
.setThreadCount(1)
.setPageLoader(new HtmlUnitPageLoader())
.setPageParser(new PageParser<PaperVo>() {
@Override
public void parse(Document html, Element pageVoElement, PaperVo paperVo) {
// 解析封装 PageVo 对象
String pageUrl = html.baseUri();
if(pageUrl.contains("https://tiku.baidu.com/tikupc/paperdetail")) {
System.out.println(paperVo.getPaperName());
PaperEntity paper = new PaperEntity();
paper.setSubjectId(SUBJECT_ID);
paper.setCourseId(COURSE_ID);
paper.setSchoolId("");
paper.setAreaId(AREA_ID);
paper.setCreaterId("system");
paper.setName(paperVo.getPaperName());
paper.setYear(YEAR);
paper.setFree("0");
paper.setSeoTitle(paperVo.getPaperName());
paper.setSeoKeywords("");
paper.setSeoDescription("");
paper.setType(PAPER_TYPE);
JSONArray entitys = new JSONArray();
// 处理类型问题
for(int i=0 ; i<paperVo.getQuestionInfoTypes().size() ; i++) {
JSONObject entity = new JSONObject();
entity.put("id", i+1);
entity.put("name", paperVo.getQuestionInfoTypes().get(i));
entity.put("title", paperVo.getQuestionInfoTitles().get(i));
QuestionType questionType = QuestionType.getQuestionType(paperVo.getQuestionInfoTypes().get(i));
entity.put("type", questionType.getValue());
// 设置爬虫要爬取的URL列表这里只传入了START_URL表示从这个初始URL开始进行试卷相关信息的爬取
// 如果需要爬取多个初始URL可以传入包含多个URL的集合等数据结构具体根据框架要求
.setUrls(START_URL)
// 设置是否允许爬虫自动扩展爬取链接设置为false表示只按照设置的初始URL进行爬取不会根据页面中的链接自动扩展爬取范围
// 如果设置为true则爬虫会自动根据页面中的超链接等继续去爬取更多相关页面需根据具体业务需求谨慎设置避免过度爬取
.setAllowSpread(false)
// 设置当爬取某个页面失败时的重试次数这里设置为5次表示如果在爬取某个URL对应的页面出现失败情况会尝试重新爬取最多5次
// 以此来提高爬取成功率,确保尽可能获取到完整的试卷信息。
.setFailRetryCount(5)
// 设置爬虫运行时使用的线程数量这里设置为1表示采用单线程方式进行爬取可根据实际情况调整线程数量来提高爬取效率
// 但要注意线程数量过多可能会给目标网站带来较大压力以及可能遇到反爬虫限制等问题。
.setThreadCount(1)
// 设置页面加载器为HtmlUnitPageLoader即使用基于HtmlUnit的方式来加载网页内容模拟浏览器行为获取页面的HTML文档
// 以便后续进行页面解析和数据提取操作。
.setPageLoader(new HtmlUnitPageLoader())
// 设置页面解析器通过匿名内部类实现PageParser接口并重写parse方法来定义如何解析爬取到的页面内容
// 根据页面内容的不同情况如是否是试卷详情页、试卷列表页等进行相应的数据提取和处理操作下面的parse方法中就是具体的解析逻辑实现。
.setPageParser(new PageParser<PaperVo>() {
@Override
public void parse(Document html, Element pageVoElement, PaperVo paperVo) {
// 获取当前爬取页面的URL地址html.baseUri()方法通过解析HTML文档获取其基础URL用于后续判断页面类型等操作。
String pageUrl = html.baseUri();
// 判断页面URL是否包含“https://tiku.baidu.com/tikupc/paperdetail”如果包含则表示当前页面是试卷详情页
// 需要进行试卷详细信息的提取和处理,并将数据存储到数据库中,下面的代码块就是针对试卷详情页的处理逻辑。
if (pageUrl.contains("https://tiku.baidu.com/tikupc/paperdetail")) {
// 在控制台打印试卷名称,这里主要是为了方便在爬取过程中查看当前正在处理的试卷信息,便于调试和监控爬取进度,
// paperVo.getPaperName()方法用于从PaperVo对象中获取试卷名称属性值PaperVo对象应该是在页面解析前期已经封装好了部分试卷相关数据。
System.out.println(paperVo.getPaperName());
// 创建一个PaperEntity对象用于封装要插入到数据库中的试卷基本信息后续会将这个对象中的数据持久化到数据库对应的试卷表中。
PaperEntity paper = new PaperEntity();
// 设置试卷所属的学科ID将之前定义的固定学科ID“gaokao”赋值给PaperEntity对象的相应属性用于标识试卷所属学科。
paper.setSubjectId(SUBJECT_ID);
// 设置试卷所属的科目ID把“shengwu”赋值给相应属性明确试卷属于生物科目方便后续按科目对试卷进行分类、查询等操作。
paper.setCourseId(COURSE_ID);
// 设置试卷所属学校ID为空字符串可能表示当前试卷与特定学校无关联或者暂时未获取到学校相关信息具体需结合业务逻辑确定。
paper.setSchoolId("");
// 设置试卷所属的区域ID使用之前定义的“360000”区域ID用于标识试卷来自江西地区便于后续按区域管理试卷数据。
paper.setAreaId(AREA_ID);
// 设置试卷创建者ID为“system”这里可能表示试卷是由系统自动爬取添加的并非用户手动创建用于记录试卷的创建来源信息。
paper.setCreaterId("system");
// 设置试卷名称从PaperVo对象中获取试卷名称并赋值给PaperEntity对象的相应属性确保试卷在数据库中的名称与爬取到的一致。
paper.setName(paperVo.getPaperName());
// 设置试卷年份将固定的“2016”年份赋值给相应属性方便后续按年份统计、查询试卷等操作。
paper.setYear(YEAR);
// 设置试卷是否免费的标识这里设置为“0”具体含义可能表示试卷不是免费的需结合业务中对该字段的定义确定
// 用于记录试卷的收费相关属性,在涉及试卷购买、展示等业务场景中会用到该信息。
paper.setFree("0");
// 设置试卷的SEO标题这里直接使用试卷名称作为SEO标题方便搜索引擎对试卷页面进行索引和展示提高试卷在搜索结果中的曝光度。
paper.setSeoTitle(paperVo.getPaperName());
// 设置试卷的SEO关键词为空字符串可能表示暂时未获取到合适的关键词或者该试卷不需要特定关键词进行搜索引擎优化
// 具体可根据实际业务需求进行补充完善。
paper.setSeoKeywords("");
// 设置试卷的SEO描述为空字符串同样可能是暂时未确定描述内容或者不需要特定描述进行搜索引擎优化后续可根据实际情况完善。
paper.setSeoDescription("");
// 设置试卷类型将之前定义的“4”名校精品类型赋值给相应属性便于后续按试卷类型对试卷进行分类管理和展示等操作。
paper.setType(PAPER_TYPE);
// 创建一个JSONArray对象用于存储试卷的题目信息题目信息会以JSON格式进行封装后续会将这个JSONArray转换为字符串后存入数据库。
JSONArray entitys = new JSONArray();
// 循环处理题目类型相关信息遍历paperVo对象中存储的题目类型列表对每个题目类型进行相应的数据封装和处理操作
// 这里paperVo.getQuestionInfoTypes()方法应该返回一个包含题目类型名称的列表,通过循环依次处理每个题目类型。
for (int i = 0; i < paperVo.getQuestionInfoTypes().size(); i++) {
// 创建一个JSONObject对象用于封装单个题目的相关信息每个题目会被构建成一个独立的JSONObject然后添加到JSONArray中。
JSONObject entity = new JSONObject();
// 设置题目的唯一标识这里简单地以序号i + 1作为题目ID方便后续对题目进行区分和索引等操作。
entity.put("id", i + 1);
// 设置题目的名称从paperVo对象中获取题目类型名称并赋值给相应属性用于展示题目相关信息。
entity.put("name", paperVo.getQuestionInfoTypes().get(i));
// 设置题目的标题同样从paperVo对象中获取对应信息赋值给相应属性这里题目的标题可能是更详细的题目描述等内容具体取决于业务逻辑。
entity.put("title", paperVo.getQuestionInfoTitles().get(i));
// 通过QuestionType枚举类的静态方法获取题目类型对应的枚举值根据题目类型名称从枚举中查找对应的类型值
// 并将其赋值给entity对象的“type”属性用于明确题目在业务逻辑中的具体类型分类。
QuestionType questionType = QuestionType.getQuestionType(paperVo.getQuestionInfoTypes().get(i));
entity.put("type", questionType.getValue());
// 将封装好的单个题目信息的JSONObject对象添加到JSONArray中这样
entitys.add(entity);
}

@ -2,237 +2,408 @@ package com.tamguo;
import com.baomidou.mybatisplus.mapper.Condition;
import com.baomidou.mybatisplus.plugins.Page;
// 引入MyBatis-Plus框架中的相关类Condition类用于构建数据库查询条件方便按照特定规则筛选、排序等操作
// Page类用于实现分页查询相关功能可指定每页数据量、当前页码等信息便于对大量数据进行分页处理。
import com.tamguo.config.redis.CacheService;
// 引入自定义的基于Redis的缓存服务类推测其提供了如缓存数据读写、计数等与Redis缓存交互的功能在后续代码中可能用于辅助数据处理比如生成唯一标识等操作。
import com.tamguo.dao.*;
// 引入多个自定义的数据访问层DAO接口这些接口分别对应不同实体如Question、CrawlerQuestion、CrawlerPaper等在数据库中的操作
// 其实现类会具体实现像数据的插入、查询、更新等数据库交互逻辑,通过依赖注入获取相应实现类实例后就能操作数据库了。
import com.tamguo.model.*;
import com.tamguo.model.enums.QuestionType;
import com.tamguo.model.vo.QuestionVo;
// 引入自定义的实体类、枚举类以及视图对象类。实体类对应数据库表结构包含各表字段对应的属性QuestionType枚举类用于明确题目类型的不同取值情况便于业务逻辑区分
// QuestionVo这类视图对象类通常用于在不同方法间临时封装和传递数据比如在网页爬取解析过程中传递题目相关信息。
import com.xuxueli.crawler.XxlCrawler;
import com.xuxueli.crawler.conf.XxlCrawlerConf;
import com.xuxueli.crawler.loader.strategy.HtmlUnitPageLoader;
import com.xuxueli.crawler.parser.PageParser;
import com.xuxueli.crawler.rundata.RunData;
import com.xuxueli.crawler.util.FileUtil;
// 引入XxlCrawler框架相关类XxlCrawler用于构建和配置爬虫实例XxlCrawlerConf可能包含爬虫的一些默认配置参数等HtmlUnitPageLoader用于加载网页内容
// PageParser用于定义如何解析爬取到的页面内容RunData用于管理爬虫运行时的数据FileUtil提供文件下载等文件相关操作的功能这些类协同实现网页爬取及后续数据处理。
import org.apache.commons.lang3.StringUtils;
// 引入Apache Commons Lang3库中的StringUtils类用于方便地进行字符串相关操作比如判断字符串是否为空、拼接字符串等在代码中多处用于对各种文本属性的处理。
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
// 引入Jsoup库中的Document和Element类用于解析HTML文档Document代表整个HTML页面文档Element则用于表示文档中的具体元素
// 在爬取网页后解析页面内容提取数据时会用到它们。
import org.junit.Test;
import org.junit.runner.RunWith;
// 引入JUnit测试框架中的注解@Test用于标记下面的方法为测试方法JUnit运行时会执行被标记的方法来进行测试@RunWith用于指定测试运行的运行器
// 这里配置使其能运行在符合Spring框架要求的环境下便于结合Spring相关功能开展测试。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
// Spring框架相关注解@Autowired用于实现自动装配依赖注入让Spring容器把对应的Bean实例注入到标注的变量中@SpringBootTest表明这是一个Spring Boot应用的测试类
// 会启动完整的应用上下文SpringRunner是Spring为JUnit 4提供的运行器结合@RunWith使用能让测试运行在Spring构建的测试环境里。
import java.io.File;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.*;
// 引入Java标准库中的常用类用于文件操作、日期格式化、数字格式化以及集合操作等满足代码中诸如创建文件目录、格式化日期、生成编号以及处理数据集合等需求。
// PaperQuestionCrawler类是一个基于Spring Boot和JUnit框架构建的测试类其主要功能是利用XxlCrawler框架爬取试卷题目相关信息
// 对爬取到的数据进行多方面处理(如关联其他实体信息、处理题目内容中的图片等)后,将题目数据存储到数据库中。
@RunWith(SpringRunner.class)
@SpringBootTest
public class PaperQuestionCrawler {
// 使用@Autowired注解自动注入QuestionMapper接口的实现类实例通过该实例可执行与题目Question相关的数据库操作
// 例如将整理好的题目信息插入到数据库对应的题目表中,实现数据的持久化存储。
@Autowired
QuestionMapper questionMapper;
// 自动注入CrawlerQuestionMapper接口的实现类实例用于处理与爬取题目相关的数据库操作比如可能涉及保存爬取题目过程中的一些额外信息等辅助整个题目爬取及存储流程。
@Autowired
CrawlerQuestionMapper crawlerQuestionMapper;
// 注入CrawlerPaperMapper接口实现类实例借助它能操作与爬取试卷相关的数据比如依据题目所在的URL去查找对应的试卷信息等方便建立题目与试卷之间的关联关系。
@Autowired
CrawlerPaperMapper crawlerPaperMapper;
// 注入ChapterMapper接口实现类实例虽然在当前展示的代码中未明确体现其详细使用场景但从名称推测它用于执行与章节Chapter相关的数据库操作
// 或许在题目与章节关联等业务逻辑处理中会用到。
@Autowired
ChapterMapper chapterMapper;
// 注入CourseMapper接口实现类实例用于操作课程Course相关的数据库操作例如通过试卷关联获取题目所属课程信息进而准确设置题目实体中的课程相关属性。
@Autowired
CourseMapper courseMapper;
// 注入SubjectMapper接口实现类实例用于操作学科Subject相关的数据库操作同样是为了获取题目所属学科的相关信息完善题目实体对象中关于学科的属性设置。
@Autowired
SubjectMapper subjectMapper;
// 注入PaperMapper接口实现类实例用于执行试卷Paper相关的数据库操作像根据试卷ID获取试卷详细信息等辅助题目数据与试卷数据之间的关联处理及相关属性赋值。
@Autowired
PaperMapper paperMapper;
// 注入自定义的缓存服务类实例用于与Redis缓存进行交互在后续代码中可能用于对部分数据进行缓存以提升性能或者实现如文件编号自增等特定的数据处理功能。
@Autowired
CacheService cacheService;
// 定义文件编号的格式模板是一个固定长度9位的用0占位的数字字符串用于后续按照一定规则生成文件编号确保编号格式的一致性和规范性。
private static final String FILES_NO_FORMAT = "000000000";
// 定义文件路径的前缀字符串此处为“shengwu”用于构建完整的文件存储路径可能表示文件所属的分类或主题具体含义取决于业务逻辑设定。
private static final String FILES_PREFIX = "shengwu";
// 定义课程ID固定为“shengwu”用于标识题目所属的课程方便在数据处理过程中关联题目与课程以及在构建文件路径等操作中作为关键标识使用。
private static final String COURSE_ID = "shengwu";
// 用于存储爬虫运行时的数据对象通过XxlCrawler框架管理爬虫运行过程中的各种相关数据例如已访问的URL、待访问的URL等
// 在爬虫启动以及后续的数据处理过程中,这个对象中的数据会不断更新和被使用。
private RunData runData;
// 使用@Test注解标记的测试方法名为crawlerQuestion该方法实现了利用XxlCrawler框架进行题目信息爬取、对爬取到的数据进行全面处理以及最终将处理好的数据存储到数据库的完整逻辑
// 同时涵盖了对题目内容中图片的下载以及相关URL替换等复杂操作。
@SuppressWarnings("unchecked")
@Test
public void crawlerQuestion() {
// 创建一个XxlCrawler实例的构建器通过链式调用一系列方法来配置爬虫的各个参数和行为完成配置后调用build方法就能得到最终可用的爬虫实例。
XxlCrawler crawler = new XxlCrawler.Builder()
.setAllowSpread(false)
.setThreadCount(1)
.setFailRetryCount(5)
.setPageLoader(new HtmlUnitPageLoader())
.setPageParser(new PageParser<QuestionVo>() {
@Override
public void parse(Document html, Element pageVoElement, QuestionVo questionVo) {
if(StringUtils.isEmpty(questionVo.getContent())) {
runData.addUrl(html.baseUri());
return;
}
CrawlerPaperEntity condition = new CrawlerPaperEntity();
condition.setQuestionUrl(html.baseUri());
System.out.println(html.baseUri());
CrawlerPaperEntity crawlerPaper = crawlerPaperMapper.selectOne(condition);
PaperEntity paper = paperMapper.selectById(crawlerPaper.getPaperId());
CourseEntity course = courseMapper.selectById(paper.getCourseId());
SubjectEntity subject = subjectMapper.selectById(paper.getSubjectId());
QuestionType questionType = QuestionType.getQuestionType(questionVo.getQuestionType());
QuestionEntity question = new QuestionEntity();
if(questionType == QuestionType.DANXUANTI) {
if(!StringUtils.isEmpty(questionVo.getQueoptions())) {
question.setContent(questionVo.getContent() + questionVo.getQueoptions());
}else {
question.setContent(questionVo.getContent());
}
}else {
question.setContent(questionVo.getContent());
}
question.setAnalysis(questionVo.getAnalysis());
if(StringUtils.isEmpty(question.getAnalysis())) {
question.setAnalysis("<p> <span> 略 </span> <br> </p>");
}
question.setAnswer(questionVo.getAnswer());
question.setAuditStatus("1");
question.setChapterId("");
question.setCourseId(course.getId());
question.setPaperId(paper.getId());
question.setQuestionType(questionType.getValue().toString());
if(questionVo.getReviewPoint() != null && questionVo.getReviewPoint().size() > 0) {
question.setReviewPoint(StringUtils.join(questionVo.getReviewPoint().toArray(), ","));
}
// 处理分数
if(questionVo.getScore() != null) {
if(questionVo.getScore().contains("分")) {
question.setScore(questionVo.getScore());
}
if(questionVo.getScore().contains("年")) {
question.setYear(questionVo.getScore());
}
}
if(questionVo.getYear() != null) {
if(questionVo.getYear().contains("年")) {
question.setYear(questionVo.getYear());
}
}
question.setSubjectId(subject.getId());
if (questionVo.getAnswerImages()!=null && questionVo.getAnswerImages().size() > 0) {
Set<String> imagesSet = new HashSet<String>(questionVo.getAnswerImages());
for (String img: imagesSet) {
// 下载图片文件
String fileName = getFileName(img);
String filePath = getFilePath();
String fileDatePath = getFileDatePath();
File dir = new File(filePath +fileDatePath+ "/");
if (!dir.exists())
dir.mkdirs();
boolean ret = FileUtil.downFile(img, XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT, filePath +fileDatePath+ "/", fileName);
System.out.println("down images " + (ret?"success":"fail") + "" + img);
// 替换URL
question.setAnswer(question.getAnswer().replace(img, "/files/paper/" + COURSE_ID + '/' + fileDatePath + "/" + fileName));
}
question.setAnswer(question.getAnswer());
}
if (questionVo.getAnalysisImages()!=null && questionVo.getAnalysisImages().size() > 0) {
Set<String> imagesSet = new HashSet<String>(questionVo.getAnalysisImages());
for (String img: imagesSet) {
// 下载图片文件
String fileName = getFileName(img);
String filePath = getFilePath();
String fileDatePath = getFileDatePath();
File dir = new File(filePath +fileDatePath+ "/");
if (!dir.exists())
dir.mkdirs();
boolean ret = FileUtil.downFile(img, XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT, filePath +fileDatePath+ "/", fileName);
System.out.println("down images " + (ret?"success":"fail") + "" + img);
// 替换URL
question.setAnalysis(question.getAnalysis().replace(img, "/files/paper/" + COURSE_ID + '/' + fileDatePath + "/" + fileName));
}
question.setAnalysis(question.getAnalysis());
}
if (questionVo.getContentImages()!=null && questionVo.getContentImages().size() > 0) {
Set<String> imagesSet = new HashSet<String>(questionVo.getContentImages());
for (String img: imagesSet) {
// 下载图片文件
String fileName = getFileName(img);
String filePath = getFilePath();
String fileDatePath = getFileDatePath();
File dir = new File(filePath +fileDatePath+ "/");
if (!dir.exists()) {
dir.mkdirs();
}
boolean ret = FileUtil.downFile(img, XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT, filePath +fileDatePath+ "/", fileName);
System.out.println("down images " + (ret?"success":"fail") + "" + img);
// 替换URL
question.setContent(question.getContent().replace(img, "/files/paper/" + COURSE_ID + '/' + fileDatePath + "/" + fileName));
}
question.setContent(question.getContent());
}
// 处理图片
question.setSourceType("baidu");
question.setSourceUrl(html.baseUri());
questionMapper.insert(question);
}
public String getFileName(String img) {
return getFileNo() + img.substring(img.lastIndexOf("."));
}
private String getFilePath() {
return "/home/webdata/files/paper/" + COURSE_ID + "/";
}
private String getFileDatePath() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHH");
String format = sdf.format(new Date());
return format;
}
private String getFileNo() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHH");
String format = sdf.format(new Date());
DecimalFormat df = new DecimalFormat(FILES_NO_FORMAT);
String key = FILES_PREFIX + format;
Long incr = cacheService.incr(key);
String avatorNo = FILES_PREFIX + df.format(incr);
return avatorNo;
}
}).build();
runData = crawler.getRunData();
int page = 1;
int pageSize = 1000;
while(true) {
Page<CrawlerPaperEntity> questionPage = new Page<CrawlerPaperEntity>(page , pageSize);
List<CrawlerPaperEntity> questionList = crawlerPaperMapper.selectPage(questionPage, Condition.create().orderAsc(Arrays.asList("paper_id" , "queindex")));
for(int i=0 ;i<questionList.size() ; i++) {
runData.addUrl(questionList.get(i).getQuestionUrl());
}
page++;
if(questionList.size() < 1000) {
break;
// 设置是否允许爬虫自动根据页面中的链接扩展爬取范围设置为false表示仅按照预先设定的初始URL进行爬取不会主动去爬取页面中发现的其他链接指向的页面
// 若设为true则爬虫会自动依据页面内的超链接等去爬取更多相关页面但需谨慎使用以免出现过度爬取或不符合预期的情况。
.setAllowSpread(false)
// 设置爬虫运行时使用的线程数量这里配置为1表示采用单线程方式来执行爬取操作可根据实际情况如目标网站的负载能力、爬取效率需求等调整线程数量
// 不过线程过多可能会给目标网站带来较大压力,甚至触发反爬虫机制等问题。
.setThreadCount(1)
// 设置当爬取某个页面失败时的重试次数此处设定为5次意味着如果在尝试爬取某个URL对应的页面出现失败情况时爬虫会自动再次尝试爬取该页面最多重试5次
// 这样能在一定程度上提高爬取的成功率,尽量保证可以完整获取到需要的题目相关信息。
.setFailRetryCount(5)
// 设置页面加载器为HtmlUnitPageLoader即利用基于HtmlUnit的方式来加载网页内容它能够模拟浏览器的行为获取页面的HTML文档
// 以便后续可以顺利地对获取到的页面进行解析以及提取相关数据等操作。
.setPageLoader(new HtmlUnitPageLoader())
// 设置页面解析器通过匿名内部类实现PageParser接口并重新实现parse方法以此来定义针对爬取到的页面内容具体的解析逻辑
// 根据页面的不同情况如包含的元素、文本内容等进行相应的数据提取、整理以及后续处理操作下面的parse方法内就是具体的解析逻辑实现部分。
.setPageParser(new PageParser<QuestionVo>() {
@Override
public void parse(Document html, Element pageVoElement, QuestionVo questionVo) {
// 判断题目Vo对象中的题目内容questionVo.getContent())是否为空字符串,如果为空,说明当前页面可能未正确获取到题目内容,
// 则将当前页面的URL添加到待访问的URL列表中通过runData.addUrl方法以便后续可能再次尝试爬取该URL对应的页面然后直接返回不再进行后续的题目数据处理流程。
if (StringUtils.isEmpty(questionVo.getContent())) {
runData.addUrl(html.baseUri());
return;
}
// 创建一个CrawlerPaperEntity对象用于构建查询数据库的条件这里以题目所在页面的URLhtml.baseUri())作为关键条件,
// 目的是通过这个URL去数据库中查找对应的爬取试卷相关的实体信息以便后续建立题目与试卷之间的关联关系。
CrawlerPaperEntity condition = new CrawlerPaperEntity();
condition.setQuestionUrl(html.baseUri());
// 在控制台打印当前页面的URL可能是为了方便在爬取过程中查看正在处理的页面情况有助于调试和监控爬取的进度以及了解数据来源等情况。
System.out.println(html.baseUri());
// 使用注入的crawlerPaperMapperCrawlerPaperMapper接口的实现类实例的selectOne方法根据前面构建的查询条件condition
// 从数据库中查询并获取对应的CrawlerPaperEntity对象也就是得到该题目所属的爬取试卷的相关详细信息。
CrawlerPaperEntity crawlerPaper = crawlerPaperMapper.selectOne(condition);
// 通过注入的paperMapperPaperMapper接口的实现类实例的selectById方法依据从前面获取到的爬取试卷的ID从crawlerPaper对象中获取
// 从数据库中查询出对应的PaperEntity对象从而得到完整的试卷信息为后续设置题目与试卷相关的属性做准备。
PaperEntity paper = paperMapper.selectById(crawlerPaper.getPaperId());
// 同样地利用注入的courseMapperCourseMapper接口的实现类实例的selectById方法按照试卷所属的课程ID从paper对象中获取
// 从数据库中查询出对应的CourseEntity对象获取题目所属的课程相关信息用于后续准确设置题目实体中的课程属性。
CourseEntity course = courseMapper.selectById(paper.getCourseId());
// 按照类似的逻辑使用注入的subjectMapperSubjectMapper接口的实现类实例的selectById方法根据试卷所属的学科ID从paper对象中获取
// 从数据库中查询出SubjectEntity对象以此得到题目所属的学科信息进一步完善题目实体对象的属性赋值。
SubjectEntity subject = subjectMapper.selectById(paper.getSubjectId());
// 通过QuestionType枚举类的静态方法getQuestionType依据题目Vo对象中获取的题目类型名称questionVo.getQuestionType()
// 获取对应的QuestionType枚举值这样就能在业务逻辑中明确区分题目具体属于哪种类型方便后续根据不同类型题目进行差异化的处理逻辑。
QuestionType questionType = QuestionType.getQuestionType(questionVo.getQuestionType());
// 创建一个QuestionEntity对象用于封装要插入到数据库中的题目信息后续会把这个对象中的各项数据持久化存储到数据库对应的题目表中。
QuestionEntity question = new QuestionEntity();
// 根据题目类型来设置题目内容如果题目类型是单选题通过判断questionType是否等于QuestionType.DANXUANTI
// 并且题目Vo对象中的题目选项questionVo.getQueoptions())不为空字符串,那么将题目内容和题目选项拼接起来作为最终的题目内容;
// 若题目选项为空则直接使用题目Vo中的题目内容作为最终题目内容对于非单选题类型则直接使用题目Vo中的题目内容设置即可。
if (questionType == QuestionType.DANXUANTI) {
if (!StringUtils.isEmpty(questionVo.getQueoptions())) {
question.setContent(questionVo.getContent() + questionVo.getQueoptions());
} else {
question.setContent(questionVo.getContent());
}
} else {
question.setContent(questionVo.getContent());
}
// 设置题目解析内容从题目Vo对象中获取解析内容赋值给QuestionEntity对象的相应属性如果获取到的解析内容为空字符串
// 则设置一个默认的简略解析内容(<p> <span> 略 </span> <br> </p>),以保证题目解析信息在数据库中具有一定的完整性。
question.setAnalysis(questionVo.getAnalysis());
if (StringUtils.isEmpty(question.getAnalysis())) {
question.setAnalysis("<p> <span> 略 </span> <br> </p>");
}
// 设置题目答案直接从题目Vo对象中获取答案内容赋值给QuestionEntity对象的相应属性。
question.setAnswer(questionVo.getAnswer());
// 设置题目审核状态为“1”这里“1”所代表的具体审核状态含义需要结合整个业务逻辑来确定可能表示已审核通过或者其他特定的审核情况
// 用于在数据库中记录题目在审核方面的状态信息。
question.setAuditStatus("1");
// 设置题目所属章节ID为空字符串这可能意味着当前题目暂时没有关联到具体的章节或者还未获取到章节相关信息具体要根据业务场景来判断。
question.setChapterId("");
// 设置题目所属课程ID将前面获取到的CourseEntity对象中的课程ID赋值给题目实体的相应属性确保题目与正确的课程建立关联关系。
question.setCourseId(course.getId());
// 设置题目所属试卷ID从前面获取的PaperEntity对象中获取试卷ID赋值给题目实体的相应属性以此建立题目与试卷之间的明确关联关系。
question.setPaperId(paper.getId());
// 设置题目类型把QuestionType枚举值转换为字符串通过getValue().toString()方法)后赋值给题目实体的相应属性,
// 这样在数据库中就能准确记录题目具体的类型信息了。
question.setQuestionType(questionType.getValue().toString());
// 判断题目Vo对象中的复习要点questionVo.getReviewPoint())是否不为空且包含元素,如果满足条件,
// 则使用StringUtils的join方法将复习要点列表转换为以逗号分隔的字符串并赋值给题目实体的复习要点属性方便在数据库中以合适的格式存储该信息。
if (questionVo.getReviewPoint()!= null && questionVo.getReviewPoint().size() > 0) {
question.setReviewPoint(StringUtils.join(questionVo.getReviewPoint().toArray(), ","));
}
// 处理题目分数相关信息如果题目Vo对象中的分数questionVo.getScore())不为空,并且这个分数字符串中包含“分”字,
// 则将该分数内容赋值给题目实体的分数属性,用于准确记录题目对应的分值情况,符合常规的分数记录逻辑。
if (questionVo.getScore()!= null) {
if (questionVo.getScore().contains("分")) {
question.setScore(questionVo.getScore());
}
// 如果题目Vo对象中的分数字符串包含“年”字这里可能存在特殊的业务逻辑处理情况比如在特定场景下分数字段复用为年份相关信息的记录
// 则将该分数内容赋值给题目实体的年份属性,具体含义需结合
if (questionVo.getScore()!= null) {
// 判断题目Vo对象中的分数questionVo.getScore())字符串是否包含“年”字,如果包含,说明在当前业务逻辑下,
// 这个分数字段可能有特殊含义也许是复用该字段来表示年份相关信息则将该分数内容赋值给题目实体的年份属性question.setYear(questionVo.getScore()))。
if (questionVo.getScore().contains("年")) {
question.setYear(questionVo.getScore());
}
}
// 再次判断题目Vo对象中的年份questionVo.getYear())是否不为空,进行这一步判断是为了确保年份信息的准确设置,
// 因为前面可能只是基于分数字段中是否包含“年”字来设置年份属性,这里再单独判断年份字段本身的情况。
if (questionVo.getYear()!= null) {
// 如果题目Vo对象中的年份字符串包含“年”字那么将该年份内容赋值给题目实体的年份属性以保证题目实体中年份信息的准确性和完整性
// 符合业务逻辑中对题目年份信息记录的要求。
if (questionVo.getYear().contains("年")) {
question.setYear(questionVo.getYear());
}
}
// 设置题目所属学科ID将从前面通过subjectMapper查询获取到的SubjectEntity对象中的学科ID赋值给题目实体的相应属性question.setSubjectId(subject.getId())
// 以此明确题目所属的学科,建立题目与学科之间的正确关联关系,便于后续按照学科维度对题目进行分类、查询等业务操作。
question.setSubjectId(subject.getId());
// 判断题目Vo对象中的答案图片列表questionVo.getAnswerImages())是否不为空且包含元素,即检查题目答案中是否存在图片,
// 如果存在图片则需要进行图片相关的处理操作包括图片下载以及在题目答案文本中替换图片URL等操作。
if (questionVo.getAnswerImages()!= null && questionVo.getAnswerImages().size() > 0) {
// 将题目Vo对象中的答案图片列表转换为一个HashSet集合使用HashSet可以去除重复的图片URL确保每个图片只处理一次提高处理效率
// 同时方便后续的遍历操作因为Set集合不允许重复元素符合处理图片URL这种唯一性要求较高的场景
Set<String> imagesSet = new HashSet<String>(questionVo.getAnswerImages());
// 遍历答案图片集合对每一张图片进行下载以及URL替换操作。
for (String img : imagesSet) {
// 调用getFileName方法获取要下载的图片文件的文件名该方法会根据图片URL以及一些业务规则生成文件名
// 文件名的生成逻辑在getFileName方法中具体实现通常会考虑文件后缀、编号等因素来保证文件名的唯一性和规范性
String fileName = getFileName(img);
// 调用getFilePath方法获取文件存储的基础路径该路径是预先定义好的用于确定文件在服务器上大致的存储位置
// 具体路径可能根据业务需求和项目配置来设定,比如按照课程、学科等分类来划分不同的文件存储目录。
String filePath = getFilePath();
// 调用getFileDatePath方法获取基于当前日期时间生成的文件路径部分通常用于按照时间来进一步细分文件存储目录
// 例如每天的文件存放在不同的以日期时间命名的子目录下,方便文件管理和查找,该方法内部会利用日期格式化来生成相应的路径字符串。
String fileDatePath = getFileDatePath();
// 根据获取到的文件路径和日期路径创建一个File对象表示要创建的文件目录对象如果该目录不存在
// 则通过mkdirs方法创建该目录及其所有必要的父目录确保文件下载时有对应的存储目录存在避免出现文件保存失败的情况。
File dir = new File(filePath + fileDatePath + "/");
if (!dir.exists())
dir.mkdirs();
// 使用FileUtil工具类的downFile方法下载图片文件传入图片URL、下载超时时间这里使用XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT作为默认超时时间
// 文件存储路径以及文件名等参数,该方法内部实现了具体的文件下载逻辑,比如建立网络连接、读取文件流并保存到本地等操作,
// 并返回一个布尔值表示文件下载是否成功。
boolean ret = FileUtil.downFile(img, XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT, filePath + fileDatePath + "/", fileName);
// 在控制台打印图片下载的结果信息方便在运行程序时查看图片下载情况便于调试和监控如果下载成功则显示“success”失败则显示“fail”
// 同时打印出当前正在处理的图片URL让开发人员能清楚知道具体是哪张图片的下载情况。
System.out.println("down images " + (ret? "success" : "fail") + "" + img);
// 在题目答案文本中替换图片的原始URL为新的本地文件路径对应的URL通过调用String的replace方法将题目答案中原来的图片URLimg
// 替换为新的格式(/files/paper/ + COURSE_ID + '/' + fileDatePath + "/" + fileName使其指向下载到本地后的图片文件位置
// 这样在后续展示题目答案等相关操作时,就能正确加载本地存储的图片了。
question.setAnswer(question.getAnswer().replace(img, "/files/paper/" + COURSE_ID + '/' + fileDatePath + "/" + fileName));
}
// 将处理后的题目答案重新赋值给题目实体的答案属性,虽然看起来这一步有点多余(因为前面已经在原答案属性上进行了替换操作),
// 但从代码的清晰性和完整性角度考虑,这样做可以更明确地表示答案属性经过一系列处理后最终的赋值情况,避免其他地方误修改该属性导致逻辑混乱。
question.setAnswer(question.getAnswer());
}
// 判断题目Vo对象中的解析图片列表questionVo.getAnalysisImages())是否不为空且包含元素,即检查题目解析内容中是否存在图片,
// 如果存在图片同样需要进行图片下载以及在题目解析文本中替换图片URL等相关操作流程与处理答案图片类似。
if (questionVo.getAnalysisImages()!= null && questionVo.getAnalysisImages().size() > 0) {
Set<String> imagesSet = new HashSet<String>(questionVo.getAnalysisImages());
for (String img : imagesSet) {
String fileName = getFileName(img);
String filePath = getFilePath();
String fileDatePath = getFileDatePath();
File dir = new File(filePath + fileDatePath + "/");
if (!dir.exists())
dir.mkdirs();
boolean ret = FileUtil.downFile(img, XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT, filePath + fileDatePath + "/", fileName);
System.out.println("down images " + (ret? "success" : "fail") + "" + img);
// 在题目解析文本中替换图片的原始URL为新的本地文件路径对应的URL使题目解析在展示时能正确加载本地存储的图片
// 替换逻辑与答案图片的URL替换类似只是针对的是题目解析内容中的图片URL替换。
question.setAnalysis(question.getAnalysis().replace(img, "/files/paper/" + COURSE_ID + '/' + fileDatePath + "/" + fileName));
}
// 同样将处理后的题目解析内容重新赋值给题目实体的解析属性,确保解析属性的最终状态符合预期,保证数据的准确性和完整性。
question.setAnalysis(question.getAnalysis());
}
// 判断题目Vo对象中的题目内容图片列表questionVo.getContentImages())是否不为空且包含元素,即检查题目内容本身是否包含图片,
// 如果有图片也要进行相应的图片下载以及在题目内容文本中替换图片URL等操作操作流程与前面处理答案图片、解析图片基本一致。
if (questionVo.getContentImages()!= null && questionVo.getContentImages().size() > 0) {
Set<String> imagesSet = new HashSet<String>(questionVo.getContentImages());
for (String img : imagesSet) {
String fileName = getFileName(img);
String filePath = getFilePath();
String fileDatePath = getFileDatePath();
File dir = new File(filePath + fileDatePath + "/");
if (!dir.exists()) {
dir.mkdirs();
}
boolean ret = FileUtil.downFile(img, XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT, filePath + fileDatePath + "/", fileName);
System.out.println("down images " + (ret? "success" : "fail") + "" + img);
// 在题目内容文本中替换图片的原始URL为新的本地文件路径对应的URL使得题目内容在展示时能够正确加载本地存储的图片
// 保证题目内容的完整性和正确性,符合业务逻辑中对题目包含图片情况的处理要求。
question.setContent(question.getContent().replace(img, "/files/paper/" + COURSE_ID + '/' + fileDatePath + "/" + fileName));
}
// 将处理后的题目内容重新赋值给题目实体的内容属性,明确题目内容属性经过图片相关处理后的最终状态,便于后续将题目数据准确存储到数据库中。
question.setContent(question.getContent());
}
// 设置题目图片的来源类型为“baidu”这里明确表示题目中的图片是从百度相关来源获取的可能是从百度题库等地方爬取的
// 具体的来源类型设定取决于业务需求和数据溯源的要求,方便后续对图片来源进行统计、分析等操作。
question.setSourceType("baidu");
// 设置题目图片的来源URL将当前爬取页面的URLhtml.baseUri()赋值给题目实体的来源URL属性记录题目图片最初是从哪个页面获取的
// 有助于数据溯源以及在需要查看原始图片来源等情况下使用,保证数据的完整性和可追溯性。
question.setSourceUrl(html.baseUri());
// 使用注入的questionMapperQuestionMapper接口的实现类实例的insert方法将处理好的题目实体对象question中的数据插入到数据库对应的题目表中
// 实现题目数据的持久化存储,完成整个题目爬取、处理以及存储的流程,确保题目信息能够正确保存到数据库里,供后续业务使用。
questionMapper.insert(question);
// 定义一个名为getFileName的私有方法它接收一个表示图片URL的字符串参数img其作用是根据给定的图片URL生成一个合适的文件名
// 这个文件名会用于后续图片文件的保存操作,确保文件名具有一定的规范性和唯一性(结合业务规则生成)。
public String getFileName(String img) {
// 通过调用getFileNo方法获取一个文件编号部分这个编号可能基于当前时间、业务标识等因素生成用于保证文件名的唯一性避免重复文件名冲突
// 然后拼接上图片URL中提取出的文件后缀部分通过img.substring(img.lastIndexOf("."))获取即从图片URL中获取最后一个“.”之后的内容作为后缀),
// 最终组成完整的文件名并返回。
return getFileNo() + img.substring(img.lastIndexOf("."));
}
// 定义一个私有的方法getFilePath用于获取文件存储的基础路径该路径是预先配置好的固定路径按照业务逻辑设定了文件大致的存储位置
// 这里返回的路径表明文件将存储在“/home/webdata/files/paper/”目录下并结合课程IDCOURSE_ID当前固定为“shengwu”进一步明确子目录
// 方便对不同课程的相关文件进行分类存储管理。
private String getFilePath() {
return "/home/webdata/files/paper/" + COURSE_ID + "/";
}
// 定义一个私有的方法getFileDatePath其目的是根据当前日期时间生成一个用于文件路径的时间相关部分
// 通过使用SimpleDateFormat按照“yyyyMMddHH”格式对当前日期时间进行格式化生成类似“2024121510”这样的字符串代表具体的年、月、日、小时
// 以此作为文件存储路径的一部分,便于按照时间维度对文件进行分类存储,例如每天的文件存放在以当天日期时间命名的子目录下,方便后续查找和管理文件。
private String getFileDatePath() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHH");
String format = sdf.format(new Date());
return format;
}
// 定义一个私有的方法getFileNo用于生成文件编号该编号在整个文件管理逻辑中起到保证文件唯一性以及便于排序等作用具体取决于业务需求
private String getFileNo() {
// 首先使用SimpleDateFormat按照“yyyyMMddHH”格式获取当前日期时间的格式化字符串例如“2024121510”这一步和getFileDatePath方法中获取时间格式的逻辑类似
// 是基于当前时间来生成编号的一部分,确保不同时间生成的编号有一定区分度,同时也便于后续按时间维度对文件进行管理和查找等操作。
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHH");
String format = sdf.format(new Date());
// 创建一个DecimalFormat对象使用之前定义的FILES_NO_FORMAT固定格式为“000000000”作为格式化模板
// 目的是将后续生成的自增编号按照这个固定长度的数字格式进行格式化,保证编号长度统一且具有一定的顺序性和可读性。
DecimalFormat df = new DecimalFormat(FILES_NO_FORMAT);
// 构建一个用于在缓存中作为键的字符串它由FILES_PREFIX当前固定为“shengwu”和前面获取的日期时间格式化字符串format拼接而成
// 这个键用于在缓存服务cacheService中标识一个特定的计数项方便后续基于业务场景对不同的文件编号进行区分和管理比如不同课程、不同业务模块等可以有不同的计数规则
String key = FILES_PREFIX + format;
// 调用cacheService注入的基于Redis的缓存服务类实例的incr方法对前面构建的键key对应的值进行自增操作
// Redis的自增操作是原子性的能保证在分布式环境下或者多线程环境中计数的准确性和一致性这里实现了文件编号的自增功能每次调用该方法都会使编号加1。
Long incr = cacheService.incr(key);
// 使用DecimalFormat对象将自增后的编号incr按照之前定义的固定格式FILES_NO_FORMAT进行格式化得到一个规范的文件编号字符串
// 例如如果自增后编号为1格式化后可能就是“shengwu000000001”然后在前面再加上FILES_PREFIX“shengwu”最终返回完整的文件编号
// 这个文件编号会作为文件名的一部分,用于保证文件名在整个业务场景下的唯一性和规范性。
String avatorNo = FILES_PREFIX + df.format(incr);
return avatorNo;
}
// 通过调用build方法构建出前面配置好的XxlCrawler实例完成爬虫对象的创建这个爬虫实例已经按照之前设置的各项参数如页面加载策略、解析逻辑、线程数量等进行了配置
// 后续可以使用这个实例来启动爬虫进行网页爬取以及相关的数据处理操作。
}).build();
// 获取前面构建好的XxlCrawler实例中的运行数据对象RunData这个对象用于管理爬虫在运行过程中的各种相关数据
// 例如已经访问过的URL、还需要访问的URL等信息在后续的爬取流程以及数据处理过程中会不断对这个对象中的数据进行更新和使用起到协调整个爬虫工作流程的作用。
runData = crawler.getRunData();
// 初始化页码变量page为1表示从第一页开始进行分页查询操作这里的分页是针对要爬取的题目相关数据进行的目的是分批处理大量的题目数据便于管理和提高效率。
int page = 1;
// 设置每页的数据量为1000即每次查询数据库获取1000条题目相关的数据记录这个数量可以根据实际情况如数据库性能、业务需求等进行调整
// 合理的每页数据量设置有助于平衡内存使用、查询效率以及数据处理的便捷性等方面的因素。
int pageSize = 1000;
// 开启一个无限循环,通过不断分页查询题目数据,直到满足特定条件(即查询到的数据量小于每页数据量,表示已经处理完所有数据)时才会跳出循环,
// 以此实现对所有相关题目数据的遍历处理,确保不遗漏任何需要处理的数据。
while (true) {
// 创建一个Page对象用于表示分页查询的相关参数传入当前页码page和每页数据量pageSize
// 这个对象会被传递给后续的查询方法,以便数据库查询操作按照指定的分页规则获取相应的数据记录。
Page<CrawlerPaperEntity> questionPage = new Page<CrawlerPaperEntity>(page, pageSize);
// 使用注入的crawlerPaperMapperCrawlerPaperMapper接口的实现类实例的selectPage方法进行分页查询操作
// 传入前面构建的分页参数对象questionPage以及查询条件通过Condition.create().orderAsc(Arrays.asList("paper_id", "queindex"))构建),
// 这里构建的查询条件表示按照“paper_id”和“queindex”字段升序排列进行查询获取符合条件的CrawlerPaperEntity类型的数据列表
// 也就是获取到一批与爬取试卷题目相关的实体数据便于后续对这些题目数据进行进一步的处理如添加URL到待爬取列表等操作
List<CrawlerPaperEntity> questionList = crawlerPaperMapper.selectPage(questionPage, Condition.create().orderAsc(Arrays.asList("paper_id", "queindex")));
// 遍历查询到的题目数据列表对于每一条题目数据记录CrawlerPaperEntity对象获取其对应的题目URL通过getQuestionUrl方法
// 然后将这个URL添加到爬虫的运行数据对象runData中的待访问URL列表中这样爬虫后续就会按照顺序去访问这些题目对应的页面进行数据爬取和处理操作。
for (int i = 0; i < questionList.size(); i++) {
runData.addUrl(questionList.get(i).getQuestionUrl());
}
// 页码自增1表示准备查询下一页的数据继续下一轮的分页查询和数据处理流程直到处理完所有满足条件的数据页为止。
page++;
// 判断当前查询到的数据列表的大小questionList.size()是否小于每页数据量1000如果小于表示已经查询完所有数据没有更多的数据页了
// 此时跳出循环结束分页查询和数据添加URL的操作准备启动爬虫进行实际的爬取和后续处理工作。
if (questionList.size() < 1000) {
break;
}
}
// 启动前面配置好的XxlCrawler实例传入参数true表示以某种特定的启动模式具体取决于XxlCrawler框架的内部实现逻辑启动爬虫
// 启动后爬虫会按照预先设置的参数(如爬取范围、页面解析逻辑等)开始进行网页爬取、数据提取以及后续的数据处理和存储等操作,
// 整个流程围绕着获取试卷题目相关信息并进行处理后保存到数据库中展开同时涉及图片文件下载及相关URL替换等相关复杂操作在前面的代码逻辑中已定义
// 获取科目(这里“获取科目”从代码逻辑上看不太明确具体含义,可能在爬虫启动后有进一步获取科目相关信息用于后续业务处理的逻辑,不过需要结合更多上下文代码来准确判断)。
crawler.start(true);}
}
}
// 获取科目
crawler.start(true);
}
}

@ -1,24 +1,49 @@
package com.tamguo;
import org.junit.Test;
// 引入JUnit测试框架中的@Test注解其作用是标记当前类中的某个方法为测试方法。
// 在运行测试时JUnit框架会识别并执行被该注解标记的方法然后根据方法内部的代码逻辑执行情况例如是否正常结束、有无抛出异常等来判断该测试是否通过。
import org.junit.runner.RunWith;
// 此注解用于指定JUnit运行测试时所采用的运行器Runner。不同的运行器能为测试提供不同的执行环境和功能特性
// 这里通过使用特定的运行器使得测试能够运行在符合Spring框架要求的环境下便于将Spring相关功能与测试进行整合。
import org.springframework.beans.factory.annotation.Autowired;
// Spring框架提供的用于自动装配依赖注入的注解。Spring容器会依据一定的规则如按照类型、名称等自动查找并注入合适的Bean实例到标注了该注解的变量中
// 这样代码就能方便地获取和使用其他由Spring管理的组件在这段代码里就是将实现IQuestionService接口的Bean实例注入进来。
import org.springframework.boot.test.context.SpringBootTest;
// 该注解用于表明这是一个针对Spring Boot应用的测试类它会启动完整的Spring Boot应用上下文加载整个应用配置的各种资源包含组件、配置信息等
// 模拟应用实际运行的环境,方便在测试中使用已配置好的资源进行集成测试等操作,确保测试可以利用到应用中所有相关的配置和组件功能。
import org.springframework.test.context.junit4.SpringRunner;
// Spring为JUnit 4提供的运行器结合@RunWith(SpringRunner.class)的使用能让JUnit测试运行在Spring构建的测试环境下
// 进而可以利用Spring框架的诸多特性如依赖注入、事务管理等来辅助完成测试工作确保测试过程与Spring应用的良好集成。
import com.tamguo.service.IQuestionService;
// 引入自定义的服务层接口IQuestionService从接口名称推测它定义了与题目Question相关的一系列业务操作方法
// 比如题目数据的查询、更新、爬取等功能。在本测试类中,会通过依赖注入获取该接口的实现类实例,然后调用相应的业务方法来验证与题目相关业务逻辑的正确性。
// QuestionCrawler类是一个基于Spring Boot和JUnit框架搭建的测试类其主要目的是对题目相关的业务逻辑进行测试验证
// 具体是通过调用IQuestionService接口实现类中的crawlerQuestion方法来检测题目数据爬取相关的业务逻辑是否能够正确执行。
@RunWith(SpringRunner.class)
@SpringBootTest
public class QuestionCrawler {
@Autowired
IQuestionService iQuestionService;
// 使用@Autowired注解让Spring容器自动将实现了IQuestionService接口的Bean实例注入到iQuestionService变量中。
// 如此一来在后续的测试方法里就能直接使用这个服务实例去调用和题目相关的业务方法例如这里要调用的crawlerQuestion方法用于执行题目爬取操作。
@Autowired
IQuestionService iQuestionService;
// 使用@Test注解标记的测试方法名为crawlerSubject此处方法名可能不太准确从功能上看或许叫crawlerQuestion更合适不过需结合具体业务情况
// 该方法主要用于测试题目数据爬取相关的业务逻辑。方法声明抛出Exception异常意味着如果在方法执行期间出现了未处理的异常情况
// JUnit会将此次测试判定为失败并输出相应的异常信息便于开发人员后续根据异常提示去排查问题确定是代码逻辑错误还是其他原因导致的异常。
@Test
public void crawlerSubject() throws Exception {
iQuestionService.crawlerQuestion();
// 调用iQuestionService实例的crawlerQuestion方法该方法的具体实现位于IQuestionService接口的实现类中。
// 从方法名推测,其功能是执行实际的题目数据爬取操作,例如从网络上获取题目相关的信息等,
// 通过在此处调用该方法,来验证与之相关的题目爬取业务逻辑能否按照预期正常执行,保障题目爬取功能的准确性和稳定性。
iQuestionService.crawlerQuestion();
}
}
}

@ -1,23 +1,41 @@
package com.tamguo;
import com.baomidou.mybatisplus.plugins.Page;
// 用于分页操作相关的类
import com.tamguo.config.redis.CacheService;
// 自定义Redis缓存服务类
import com.tamguo.dao.*;
// 多个数据访问层接口
import com.tamguo.model.*;
import com.tamguo.model.vo.QuestionVo;
// 实体类与题目相关视图对象类
import com.xuxueli.crawler.XxlCrawler;
import com.xuxueli.crawler.conf.XxlCrawlerConf;
import com.xuxueli.crawler.loader.strategy.HtmlUnitPageLoader;
import com.xuxueli.crawler.parser.PageParser;
import com.xuxueli.crawler.rundata.RunData;
import com.xuxueli.crawler.util.FileUtil;
// XxlCrawler框架相关类用于爬虫及相关操作
import org.apache.commons.lang3.StringUtils;
// 字符串操作工具类
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
// 用于解析HTML文档的类
import org.junit.Test;
import org.junit.runner.RunWith;
// JUnit测试相关注解
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
// Spring框架的依赖注入与属性值注入注解
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@ -29,163 +47,181 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
// 基于Spring Boot和JUnit的测试类用于爬取单个题目相关信息并处理存储
@RunWith(SpringRunner.class)
@SpringBootTest
public class SingleQuestionCrawler {
// 存储爬虫运行时数据
private RunData runData;
// 自动注入爬取题目相关的数据访问层实例
@Autowired
CrawlerQuestionMapper crawlerQuestionMapper;
// 章节数据访问层实例
@Autowired
ChapterMapper chapterMapper;
// 课程数据访问层实例
@Autowired
CourseMapper courseMapper;
// 学科数据访问层实例
@Autowired
SubjectMapper subjectMapper;
// 缓存服务实例
@Autowired
CacheService cacheService;
// 题目数据访问层实例
@Autowired
QuestionMapper questionMapper;
// 文件编号格式模板
private static final String FILES_NO_FORMAT = "000000";
// 文件路径前缀
private static final String FILES_PREFIX = "FP";
@Value(value="${domain.name}")
// 注入配置文件中的域名属性值
@Value(value = "${domain.name}")
public String DOMAIN;
@Test
public void crawlerSubject() throws Exception {
XxlCrawler crawler = new XxlCrawler.Builder()
.setAllowSpread(false)
.setThreadCount(20)
.setPageLoader(new HtmlUnitPageLoader())
.setPageParser(new PageParser<QuestionVo>() {
// 测试方法,实现题目爬取、处理及存储逻辑
@Test
public void crawlerSubject() throws Exception {
// 构建XxlCrawler实例并配置相关参数
XxlCrawler crawler = new XxlCrawler.Builder()
.setAllowSpread(false) // 不允许自动扩展爬取范围
.setThreadCount(20) // 设置20个线程进行爬取
.setPageLoader(new HtmlUnitPageLoader()) // 设置页面加载器
.setPageParser(new PageParser<QuestionVo>() { // 设置页面解析逻辑
@Override
public void parse(Document html, Element pageVoElement, QuestionVo questionVo) {
// 根据题目URL查询爬取题目相关信息
CrawlerQuestionEntity condition = new CrawlerQuestionEntity();
condition.setQuestionUrl(html.baseUri());
CrawlerQuestionEntity crawlerQuestion = crawlerQuestionMapper.selectOne(condition);
ChapterEntity chapter = chapterMapper.selectById(crawlerQuestion.getChapterId());
CourseEntity course = courseMapper.selectById(chapter.getCourseId());
SubjectEntity subject = subjectMapper.selectById(course.getSubjectId());
QuestionEntity question = new QuestionEntity();
question.setAnalysis(questionVo.getAnalysis());
question.setAnswer(questionVo.getAnswer());
question.setAuditStatus("1");
question.setChapterId(chapter.getId());
question.setContent(questionVo.getContent());
question.setCourseId(course.getId());
question.setPaperId(null);
question.setQuestionType("1");
if(questionVo.getReviewPoint() != null && questionVo.getReviewPoint().size() > 0) {
question.setReviewPoint(StringUtils.join(questionVo.getReviewPoint().toArray(), ","));
}
question.setScore(questionVo.getScore());
question.setSubjectId(subject.getId());
question.setYear(questionVo.getYear());
if (questionVo.getAnswerImages()!=null && questionVo.getAnswerImages().size() > 0) {
Set<String> imagesSet = new HashSet<String>(questionVo.getAnswerImages());
for (String img: imagesSet) {
// 下载图片文件
String fileName = getFileName(img);
File dir = new File(getFilePath());
if (!dir.exists())
dir.mkdirs();
boolean ret = FileUtil.downFile(img, XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT, getFilePath(), fileName);
System.out.println("down images " + (ret?"success":"fail") + "" + img);
// 替换URL
questionVo.setAnswer(questionVo.getAnswer().replace(img, DOMAIN + getFilePaths() + fileName));
}
question.setAnswer(questionVo.getAnswer());
}
if (questionVo.getAnalysisImages()!=null && questionVo.getAnalysisImages().size() > 0) {
Set<String> imagesSet = new HashSet<String>(questionVo.getAnalysisImages());
for (String img: imagesSet) {
// 下载图片文件
String fileName = getFileName(img);
File dir = new File(getFilePath());
if (!dir.exists())
dir.mkdirs();
boolean ret = FileUtil.downFile(img, XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT, getFilePath(), fileName);
System.out.println("down images " + (ret?"success":"fail") + "" + img);
// 替换URL
questionVo.setAnalysis(questionVo.getAnalysis().replace(img, DOMAIN + getFilePaths() + fileName));
}
}
question.setAnalysis(questionVo.getAnalysis());
if (questionVo.getContentImages()!=null && questionVo.getContentImages().size() > 0) {
Set<String> imagesSet = new HashSet<String>(questionVo.getContentImages());
for (String img: imagesSet) {
// 下载图片文件
String fileName = getFileName(img);
File dir = new File(getFilePath());
if (!dir.exists())
dir.mkdirs();
boolean ret = FileUtil.downFile(img, XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT, getFilePath(), fileName);
System.out.println("down images " + (ret?"success":"fail") + "" + img);
// 替换URL
questionVo.setContent(questionVo.getContent().replace(img, DOMAIN + getFilePaths() + fileName));
}
}
question.setContent(questionVo.getContent());
questionMapper.insert(question);
condition.setQuestionUrl(html.baseUri());
CrawlerQuestionEntity crawlerQuestion = crawlerQuestionMapper.selectOne(condition);
// 获取题目所属章节、课程、学科等相关实体信息
ChapterEntity chapter = chapterMapper.selectById(crawlerQuestion.getChapterId());
CourseEntity course = courseMapper.selectById(chapter.getCourseId());
SubjectEntity subject = subjectMapper.selectById(course.getSubjectId());
// 创建题目实体对象并设置各项属性
QuestionEntity question = new QuestionEntity();
question.setAnalysis(questionVo.getAnalysis());
question.setAnswer(questionVo.getAnswer());
question.setAuditStatus("1");
question.setChapterId(chapter.getId());
question.setContent(questionVo.getContent());
question.setCourseId(course.getId());
question.setPaperId(null);
question.setQuestionType("1");
if (questionVo.getReviewPoint()!= null && questionVo.getReviewPoint().size() > 0) {
question.setReviewPoint(StringUtils.join(questionVo.getReviewPoint().toArray(), ","));
}
question.setScore(questionVo.getScore());
question.setSubjectId(subject.getId());
question.setYear(questionVo.getYear());
// 处理答案图片下载、替换URL
if (questionVo.getAnswerImages()!= null && questionVo.getAnswerImages().size() > 0) {
Set<String> imagesSet = new HashSet<String>(questionVo.getAnswerImages());
for (String img : imagesSet) {
String fileName = getFileName(img);
File dir = new File(getFilePath());
if (!dir.exists()) {
dir.mkdirs();
}
boolean ret = FileUtil.downFile(img, XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT, getFilePath(), fileName);
System.out.println("down images " + (ret? "success" : "fail") + "" + img);
questionVo.setAnswer(questionVo.getAnswer().replace(img, DOMAIN + getFilePaths() + fileName));
}
question.setAnswer(questionVo.getAnswer());
}
// 处理解析图片下载、替换URL
if (questionVo.getAnalysisImages()!= null && questionVo.getAnalysisImages().size() > 0) {
Set<String> imagesSet = new HashSet<String>(questionVo.getAnalysisImages());
for (String img : imagesSet) {
String fileName = getFileName(img);
File dir = new File(getFilePath());
if (!dir.exists()) {
dir.mkdirs();
}
boolean ret = FileUtil.downFile(img, XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT, getFilePath(), fileName);
System.out.println("down images " + (ret? "success" : "fail") + "" + img);
questionVo.setAnalysis(questionVo.getAnalysis().replace(img, DOMAIN + getFilePaths() + fileName));
}
question.setAnalysis(questionVo.getAnalysis());
}
// 处理题目内容图片下载、替换URL
if (questionVo.getContentImages()!= null && questionVo.getContentImages().size() > 0) {
Set<String> imagesSet = new HashSet<String>(questionVo.getContentImages());
for (String img : imagesSet) {
String fileName = getFileName(img);
File dir = new File(getFilePath());
if (!dir.exists()) {
dir.mkdirs();
}
boolean ret = FileUtil.downFile(img, XxlCrawlerConf.TIMEOUT_MILLIS_DEFAULT, getFilePath(), fileName);
System.out.println("down images " + (ret? "success" : "fail") + "" + img);
questionVo.setContent(questionVo.getContent().replace(img, DOMAIN + getFilePaths() + fileName));
}
question.setContent(questionVo.getContent());
}
// 将题目信息插入数据库
questionMapper.insert(question);
}
// 根据图片URL生成文件名
public String getFileName(String img) {
return getFileNo() + img.substring(img.lastIndexOf("."));
}
private String getFilePath() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String format = sdf.format(new Date());
return "/home/webdata/files/" + format + "/";
}
private String getFilePaths() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String format = sdf.format(new Date());
return "/files/" + format + "/";
}
private String getFileNo() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String format = sdf.format(new Date());
DecimalFormat df = new DecimalFormat(FILES_NO_FORMAT);
String key = FILES_PREFIX + format;
Long incr = cacheService.incr(key);
String avatorNo = FILES_PREFIX + df.format(incr);
return avatorNo;
}
}).build();
return getFileNo() + img.substring(img.lastIndexOf("."));
}
// 获取文件存储路径(按日期生成)
private String getFilePath() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String format = sdf.format(new Date());
return "/home/webdata/files/" + format + "/";
}
// 获取用于替换URL的文件路径按日期生成
private String getFilePaths() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String format = sdf.format(new Date());
return "/files/" + format + "/";
}
// 生成文件编号
private String getFileNo() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String format = sdf.format(new Date());
DecimalFormat df = new DecimalFormat(FILES_NO_FORMAT);
String key = FILES_PREFIX + format;
Long incr = cacheService.incr(key);
String avatorNo = FILES_PREFIX + df.format(incr);
return avatorNo;
}
}).build();
// 获取爬虫运行数据对象
runData = crawler.getRunData();
int page = 1;
int pageSize = 100;
while(true) {
Page<CrawlerQuestionEntity> questionPage = new Page<CrawlerQuestionEntity>(page , pageSize);
// 分页循环获取爬取题目列表添加题目URL到待爬取列表
while (true) {
Page<CrawlerQuestionEntity> questionPage = new Page<CrawlerQuestionEntity>(page, pageSize);
List<CrawlerQuestionEntity> questionList = crawlerQuestionMapper.queryPageOrderUid(questionPage);
for(int i=0 ;i<questionList.size() ; i++) {
for (int i = 0; i < questionList.size(); i++) {
runData.addUrl(questionList.get(i).getQuestionUrl());
}
page++;
if(questionList.size() < 100) {
if (questionList.size() < 100) {
break;
}
}
crawler.start(true);
}
}
// 启动爬虫开始爬取
crawler.start(true);
}
}

@ -1,23 +1,52 @@
package com.tamguo;
import org.junit.Test;
// 引入JUnit框架中的@Test注解用于标记下方的方法为测试方法。JUnit在运行测试时会专门执行被该注解标记的方法
// 并依据方法内部的逻辑执行情况(例如是否正常返回预期结果、是否抛出异常等)来判断该测试是否通过。
import org.junit.runner.RunWith;
// 此注解用于指定JUnit运行测试时所采用的运行器Runner。不同的运行器能为测试营造不同的执行环境以及提供相应的功能支持
// 这里配置的运行器旨在让测试能够运行在符合Spring框架要求的特定环境之中方便后续整合Spring相关的功能进行测试。
import org.springframework.beans.factory.annotation.Autowired;
// Spring框架提供的这个注解用于实现自动装配依赖注入功能。Spring容器会按照既定的规则比如依据类型、名称等条件
// 自动查找并将匹配的Bean实例注入到标注了该注解的变量中从而使代码可以便捷地获取并使用其他由Spring管理的组件。
// 在本代码中就是利用该注解将实现ISubjectService接口的Bean实例注入进来。
import org.springframework.boot.test.context.SpringBootTest;
// 该注解用于表明当前类是一个针对Spring Boot应用而设计的测试类。它会促使Spring Boot启动完整的应用上下文
// 加载整个应用所配置的各类资源(包含各种组件、配置信息等),模拟应用在实际运行时的真实环境,便于开展集成测试等相关操作,
// 确保测试过程能够顺利使用到应用中已经配置好的所有资源。
import org.springframework.test.context.junit4.SpringRunner;
// 这是Spring专门为JUnit 4提供的运行器类当与@RunWith(SpringRunner.class)配合使用时,
// 可以让JUnit测试运行在由Spring构建的测试环境之下进而充分利用Spring框架所具备的诸多特性比如依赖注入、事务管理等功能来辅助完成测试工作。
import com.tamguo.service.ISubjectService;
// 引入自定义的服务层接口ISubjectService从接口名称推测它应该定义了与学科Subject相关的一系列业务操作方法
// 例如学科数据的获取、学科信息的更新或者学科相关资源的爬取等操作。在本测试类中,会通过依赖注入获取该接口的实现类实例,
// 进而调用对应的业务方法来验证相关业务逻辑是否正确执行。
// SubjectCrawler类是一个基于Spring Boot和JUnit框架构建的测试类其核心功能在于对学科相关业务逻辑进行测试
// 具体而言是通过调用ISubjectService接口实现类中的方法此处为crawlerSubject方法来测试学科数据爬取相关的业务逻辑是否符合预期。
@RunWith(SpringRunner.class)
@SpringBootTest
public class SubjectCrawler {
@Autowired
ISubjectService iSubjectService;
// 使用@Autowired注解让Spring容器自动查找并注入一个实现了ISubjectService接口的Bean实例到iSubjectService变量中。
// 如此一来后续在测试方法里就能直接利用这个服务实例去调用和学科相关的业务方法了例如在这个类中调用的crawlerSubject方法。
@Autowired
ISubjectService iSubjectService;
// 使用@Test注解标记的测试方法名为crawlerSubject该方法主要用于测试学科数据爬取相关的具体业务逻辑。
// 方法声明抛出Exception异常表示如果在方法执行过程中出现了未被处理的异常情况JUnit会将此次测试判定为失败
// 并输出相应的异常信息,方便开发人员排查问题所在,进而分析和修复可能存在的代码错误或者业务逻辑问题。
@Test
public void crawlerSubject() throws Exception {
// 调用iSubjectService实例的crawlerSubject方法该方法的具体实现应该位于ISubjectService接口的实现类中。
// 从方法名推测,其主要功能是执行实际的学科数据爬取操作,比如从网络上抓取学科相关的信息等,
// 通过在这里调用该方法来检验与之相关的业务逻辑能否按照预期正常执行,确保学科数据爬取功能的正确性和稳定性。
iSubjectService.crawlerSubject();
}
}
}
Loading…
Cancel
Save