我们这里需要设计两张表,一个是期刊表,还有一个是文章表;
首先是期刊表的字段:发行年份、第几期、请求地址、记录日期;
然后是文章表的字段:文章code、期刊id、文章类型、请求地址、文章标题、作者、内容、记录日期;
- CREATE TABLE `t_lemon1234_scraper_periodical` (
- `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键id',
- `issuanceYear` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '发行年份',
- `period` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '期',
- `requestUrl` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求地址',
- `recordDt` datetime NULL DEFAULT NULL COMMENT '记录日期',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-
- CREATE INDEX index_t_lemon1234_scraper_periodical_001 ON t_lemon1234_scraper_periodical(issuanceYear, period);
-
- CREATE TABLE `t_lemon1234_scraper_article` (
- `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
- `articleCode` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文章编号',
- `periodicalId` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '期刊id',
- `typeName` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文章类型',
- `requestUrl` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文章请求地址',
- `title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文章标题',
- `author` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '作者名称',
- `context` mediumtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '文章内容',
- `recordDt` datetime NULL DEFAULT NULL COMMENT '记录日期',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-
- CREATE INDEX index_t_lemon1234_scraper_article_001 ON t_lemon1234_scraper_article(articleCode);
爬虫代码实现和之前的爬博客代码类似,所以我会省略很多代码,详细代码可以去我的 Git 仓库拉取。
我们博客爬取是每 20 分钟爬取一次,但是这个文章却不用,它每半个月才会去更新一次~,这里我是让他每天凌晨 2 点更新一次。
@Scheduled(cron = "0 0 2 * * ?")
或者可以一周更新一次,这个大家到时候自己看
这里我们第一步肯定是要去爬取期刊,所以我们需要识别到主页面的所有期刊信息,来看一下:
- HtmlPage page = webClient.getPage(taskUrl);
- List<DomElement> tableTrDom = page.getByXPath("//table[@class='booklist']/tbody/tr/td[@class='time']");
这里主要来看一下 XPath 的路径信息:

然后拿到 List 之后,我们需要通过遍历这些数据,顺便简单的处理一下,就可以拿到期刊信息了。
- for(DomElement x : tableTrDom) {
- Document indexDocument = Jsoup.parse(x.asXml());
- Element element = indexDocument.select("a").first();
-
- String periodicals = element.text();
- String requestUrl = taskUrl + "/" + element.attr("href");
-
- Periodical periodical = createPeriodical(periodicals, requestUrl);
- String periodicalId = periodicalService.findPeriodicalId(periodical);
- if(StrUtil.isEmpty(periodicalId)) {
- periodicalService.savePeriodical(periodical);
- periodicalId = periodical.getId();
- }
- logger.info("========== 当前爬取第 {}-{} 期 ========================================", periodical.getIssuanceYear(), periodical.getPeriod());
- }
findPeriodicalId() 方法是通过年份、第几期进行查看是否存在,如果存在就跳过,如果不存在就做保存。
createPeriodical 方法:
- public Periodical createPeriodical(String periodicals, String requestUrl) {
- String yearPeriod = periodicals.replace("年第", "-").replace("期", "");
- String[] split = yearPeriod.split("-");
- return new Periodical(split[0], split[1], requestUrl);
- }
上面我们获取到了每一期文章的请求地址,接下来我们就要通过请求,拿到每一期文章里面的数据。
- String requestUrl = taskUrl + "/" + element.attr("href");
- ......
- HtmlPage periodPage = webClient.getPage(requestUrl);
- List<DomElement> dlDom = periodPage.getByXPath("//dl");
- for(DomElement y : dlDom) {
- }
这里我们是获取的所有的 dl,来看一下 dl 里面有什么

可以看到,到时候我们通过获取 dt 能拿到文章的类型名称,通过 dd 拿到具体的文章~
- for(DomElement y : dlDom) {
- Document menuDocument = Jsoup.parse(y.asXml());
- Element dtElement = menuDocument.select("dt").first();
-
- Element dt_span = dtElement.select("span").first();
- String typeName = dt_span.text();
-
- Elements ddElements = menuDocument.select("dd a[href]");
- for(Element a : ddElements) {
-
- }
- }
这里解释一下,dd 里面可能有多个文章,所以说我们这里获取的一定是一个 Elements。
最后,我们可以通过获取 dd 里面的 a 标签,拿到 href 属性,这样就可以通过 url 拿到文章的连接了。
- for(Element a : ddElements) {
- String url = a.attr("href");
- String title = a.text();
-
- String articleCode = getArticleCode(url);
-
- if(!articleService.existsArticle(articleCode)) {
- Article article = new Article();
- article.setArticleCode(articleCode);
- article.setPeriodicalId(periodicalId);
- article.setTypeName(typeName);
- article.setRequestUrl(taskUrl + "/" + issuanceYear + "/yl" + issuanceYear + period + "/" + url);
- article.setTitle(title);
- parseArticle(webClient, article);
- articleService.saveArticle(article);
-
- count++;
-
- logger.info("========== 文章《{}》爬取完成 ========================================", title);
- } else {
- logger.info("========== 跳过文章《{}》 ========================================", title);
- }
- }
getArticleCode() 方法:
- public String getArticleCode(String url) {
- return url.replace(".html", "");
- }
existsArticle() 方法:
这个就是根据上面 getArticleCode 解析出来的 code 去查有没有这个文章,有就跳过,没有就进行爬取更新~
parseArticle() 方法:
- public void parseArticle(WebClient webClient, Article article) throws Exception {
- HtmlPage page = webClient.getPage(article.getRequestUrl());
- List<DomElement> p = page.getByXPath("//div[@class='blkContainerSblkCon']/p");
- Integer size = p.size();
- StringBuffer context = new StringBuffer();
- for(int i = 0; i < size; i++) {
- Document pDocument = Jsoup.parse(p.get(i).asXml());
- Element dtElement = pDocument.select("p").first();
- if(i == 0) {
- if(dtElement.text().length() > 10) {
- article.setAuthor("佚名");
- context.append(dtElement.text());
- } else {
- article.setAuthor(dtElement.text());
- }
- }
- if(i != size) {
- context.append(dtElement.text());
- }
- }
- article.setContext(context.toString());
- }
这个方法就是再通过文章的请求地址,在进行获取文章的内容,还有文章的作者。

可以发现,这文章都是在 class 为 blkContainerSblkCon 的 div 下面的 p 标签中,所以我们只需要去得到这一堆 p 标签即可。
但是,这文章的第一个 p 一般都是作者的名称,所以说我们这里需要去特殊的处理一下,如果第一个 p 标签中文字超过 10 个字,那么就说明是:“佚名”,反之就是作者名称。
代码写完之后,我们来测试一波

这里在爬取的时候一定会出现 404 问题,但是我们不能因为 404 就不去爬取了,所以这里我们需要绕过这种的文章;
- HtmlPage page = null;
- try {
- page = webClient.getPage(article.getRequestUrl());
- } catch (FailingHttpStatusCodeException e) {
- logger.info("========== 文章《{}》爬取过程中出现异常:{} ========================================", article.getTitle(), e.getMessage());
- return;
- }
- parseArticle(webClient, article);
- if(StrUtil.isEmpty(article.getAuthor()) || StrUtil.isEmpty(article.getContext())) {
- logger.info("========== 跳过文章《{}》 ========================================", title);
- continue;
- }
来看报错

这是我再爬取的时候发现的一个很头疼的问题,就是他这个 url 的构成是有变化的,之前的期刊组成不是这样的~

可以看到,他之前的一些文章 url 是这样的

再看一下最近几期的,所以说我们如果想真的爬取这些数据,还得根据年份判断他们文章 url 组成的方式,这里我就不修复这个 bug 了~。
我这里当时写代码的时候在家里,所以老是碰到一个 http 连接超时,这个的解决办法就是增加 Thread.sleep 进行处理;
要增加的地方有两个,一个是:

还有一个是:
到这里位置,我们的爬虫项目就完结了。
从我们这两个爬虫项目中,我们可以发现,不管爬什么项目,核心就是我们要爬取的网页的结构,这个我们一定要很了解,然后就是测试、优化。
好比我们上面的这个文章爬取,最后的一个问题,就是因为它网页的 url 结构做了改变,导致我们无法爬取,所以一定要有大量的测试来去纠正我们的项目代码。
还有一点就是优化,随着我们爬取的数量越来越多,数据库压力也会慢慢增加,所以我们需要通过优化我们的项目,来让项目运行的更加流畅。
这一讲就讲到这里,有问题可以联系我:QQ 2100363119,欢迎大家访问我的个人网站:https://www.lemon1234.com