• 手把手Java爬虫教学 - 8. 项目2 - 数据库表设计 & 爬虫代码实现


    一、数据库表设计

    我们这里需要设计两张表,一个是期刊表,还有一个是文章表;

    首先是期刊表的字段:发行年份、第几期、请求地址、记录日期;

    然后是文章表的字段:文章code、期刊id、文章类型、请求地址、文章标题、作者、内容、记录日期;

    1. CREATE TABLE `t_lemon1234_scraper_periodical` (
    2. `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键id',
    3. `issuanceYear` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '发行年份',
    4. `period` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '期',
    5. `requestUrl` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求地址',
    6. `recordDt` datetime NULL DEFAULT NULL COMMENT '记录日期',
    7. PRIMARY KEY (`id`) USING BTREE
    8. ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    9. CREATE INDEX index_t_lemon1234_scraper_periodical_001 ON t_lemon1234_scraper_periodical(issuanceYear, period);
    10. CREATE TABLE `t_lemon1234_scraper_article` (
    11. `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    12. `articleCode` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文章编号',
    13. `periodicalId` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '期刊id',
    14. `typeName` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文章类型',
    15. `requestUrl` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文章请求地址',
    16. `title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文章标题',
    17. `author` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '作者名称',
    18. `context` mediumtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '文章内容',
    19. `recordDt` datetime NULL DEFAULT NULL COMMENT '记录日期',
    20. PRIMARY KEY (`id`) USING BTREE
    21. ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    22. CREATE INDEX index_t_lemon1234_scraper_article_001 ON t_lemon1234_scraper_article(articleCode);

    二、爬虫代码实现

    爬虫代码实现和之前的爬博客代码类似,所以我会省略很多代码,详细代码可以去我的 Git 仓库拉取。

    1. 爬取时间

    我们博客爬取是每 20 分钟爬取一次,但是这个文章却不用,它每半个月才会去更新一次~,这里我是让他每天凌晨 2 点更新一次。

    @Scheduled(cron = "0 0 2 * * ?")

    或者可以一周更新一次,这个大家到时候自己看

    2. 爬取期刊信息

    这里我们第一步肯定是要去爬取期刊,所以我们需要识别到主页面的所有期刊信息,来看一下:

    1. HtmlPage page = webClient.getPage(taskUrl);
    2. List<DomElement> tableTrDom = page.getByXPath("//table[@class='booklist']/tbody/tr/td[@class='time']");

    这里主要来看一下 XPath 的路径信息:

    然后拿到 List 之后,我们需要通过遍历这些数据,顺便简单的处理一下,就可以拿到期刊信息了。

    1. for(DomElement x : tableTrDom) {
    2. Document indexDocument = Jsoup.parse(x.asXml());
    3. Element element = indexDocument.select("a").first();
    4. String periodicals = element.text();
    5. String requestUrl = taskUrl + "/" + element.attr("href");
    6. Periodical periodical = createPeriodical(periodicals, requestUrl);
    7. String periodicalId = periodicalService.findPeriodicalId(periodical);
    8. if(StrUtil.isEmpty(periodicalId)) {
    9. periodicalService.savePeriodical(periodical);
    10. periodicalId = periodical.getId();
    11. }
    12. logger.info("========== 当前爬取第 {}-{} 期 ========================================", periodical.getIssuanceYear(), periodical.getPeriod());
    13. }

    findPeriodicalId() 方法是通过年份、第几期进行查看是否存在,如果存在就跳过,如果不存在就做保存。

    createPeriodical 方法:

    1. public Periodical createPeriodical(String periodicals, String requestUrl) {
    2. String yearPeriod = periodicals.replace("年第", "-").replace("期", "");
    3. String[] split = yearPeriod.split("-");
    4. return new Periodical(split[0], split[1], requestUrl);
    5. }

    3. 爬取每一期下面的每篇文章

    上面我们获取到了每一期文章的请求地址,接下来我们就要通过请求,拿到每一期文章里面的数据。

    1. String requestUrl = taskUrl + "/" + element.attr("href");
    2. ......
    3. HtmlPage periodPage = webClient.getPage(requestUrl);
    4. List<DomElement> dlDom = periodPage.getByXPath("//dl");
    5. for(DomElement y : dlDom) {
    6. }

    这里我们是获取的所有的 dl,来看一下 dl 里面有什么

    可以看到,到时候我们通过获取 dt 能拿到文章的类型名称,通过 dd 拿到具体的文章~

    1. for(DomElement y : dlDom) {
    2. Document menuDocument = Jsoup.parse(y.asXml());
    3. Element dtElement = menuDocument.select("dt").first();
    4. Element dt_span = dtElement.select("span").first();
    5. String typeName = dt_span.text();
    6. Elements ddElements = menuDocument.select("dd a[href]");
    7. for(Element a : ddElements) {
    8. }
    9. }

    这里解释一下,dd 里面可能有多个文章,所以说我们这里获取的一定是一个 Elements。

    最后,我们可以通过获取 dd 里面的 a 标签,拿到 href 属性,这样就可以通过 url 拿到文章的连接了。

    1. for(Element a : ddElements) {
    2. String url = a.attr("href");
    3. String title = a.text();
    4. String articleCode = getArticleCode(url);
    5. if(!articleService.existsArticle(articleCode)) {
    6. Article article = new Article();
    7. article.setArticleCode(articleCode);
    8. article.setPeriodicalId(periodicalId);
    9. article.setTypeName(typeName);
    10. article.setRequestUrl(taskUrl + "/" + issuanceYear + "/yl" + issuanceYear + period + "/" + url);
    11. article.setTitle(title);
    12. parseArticle(webClient, article);
    13. articleService.saveArticle(article);
    14. count++;
    15. logger.info("========== 文章《{}》爬取完成 ========================================", title);
    16. } else {
    17. logger.info("========== 跳过文章《{}》 ========================================", title);
    18. }
    19. }

    getArticleCode() 方法:

    1. public String getArticleCode(String url) {
    2. return url.replace(".html", "");
    3. }

    existsArticle() 方法:

    这个就是根据上面 getArticleCode 解析出来的 code 去查有没有这个文章,有就跳过,没有就进行爬取更新~

    parseArticle() 方法:

    1. public void parseArticle(WebClient webClient, Article article) throws Exception {
    2. HtmlPage page = webClient.getPage(article.getRequestUrl());
    3. List<DomElement> p = page.getByXPath("//div[@class='blkContainerSblkCon']/p");
    4. Integer size = p.size();
    5. StringBuffer context = new StringBuffer();
    6. for(int i = 0; i < size; i++) {
    7. Document pDocument = Jsoup.parse(p.get(i).asXml());
    8. Element dtElement = pDocument.select("p").first();
    9. if(i == 0) {
    10. if(dtElement.text().length() > 10) {
    11. article.setAuthor("佚名");
    12. context.append(dtElement.text());
    13. } else {
    14. article.setAuthor(dtElement.text());
    15. }
    16. }
    17. if(i != size) {
    18. context.append(dtElement.text());
    19. }
    20. }
    21. article.setContext(context.toString());
    22. }

    这个方法就是再通过文章的请求地址,在进行获取文章的内容,还有文章的作者。

    可以发现,这文章都是在 class 为 blkContainerSblkCon 的 div 下面的 p 标签中,所以我们只需要去得到这一堆 p 标签即可。

    但是,这文章的第一个 p 一般都是作者的名称,所以说我们这里需要去特殊的处理一下,如果第一个 p 标签中文字超过 10 个字,那么就说明是:“佚名”,反之就是作者名称。

    三、测试

    代码写完之后,我们来测试一波

    1. 404 问题

    这里在爬取的时候一定会出现 404 问题,但是我们不能因为 404 就不去爬取了,所以这里我们需要绕过这种的文章;

    • parseArticle() 方法中增加 try catch 进行捕获
    1. HtmlPage page = null;
    2. try {
    3.     page = webClient.getPage(article.getRequestUrl());
    4. } catch (FailingHttpStatusCodeException e) {
    5.     logger.info("========== 文章《{}》爬取过程中出现异常:{} ========================================", article.getTitle(), e.getMessage());
    6.     return;
    7. }
    • 在调用 parseArticle 方法的地方增加判断,如果作者或者文章内容为空,就跳过这种文章
    1. parseArticle(webClient, article);
    2. if(StrUtil.isEmpty(article.getAuthor()) || StrUtil.isEmpty(article.getContext())) {
    3. logger.info("========== 跳过文章《{}》 ========================================", title);
    4. continue;
    5. }

    2. 文章地址不同

    来看报错

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

     

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

    再看一下最近几期的,所以说我们如果想真的爬取这些数据,还得根据年份判断他们文章 url 组成的方式,这里我就不修复这个 bug 了~。

    3. 超时

    我这里当时写代码的时候在家里,所以老是碰到一个 http 连接超时,这个的解决办法就是增加 Thread.sleep 进行处理;

    要增加的地方有两个,一个是:

    还有一个是:

     

    四、总结

    到这里位置,我们的爬虫项目就完结了。

    从我们这两个爬虫项目中,我们可以发现,不管爬什么项目,核心就是我们要爬取的网页的结构,这个我们一定要很了解,然后就是测试、优化。

    好比我们上面的这个文章爬取,最后的一个问题,就是因为它网页的 url 结构做了改变,导致我们无法爬取,所以一定要有大量的测试来去纠正我们的项目代码。

    还有一点就是优化,随着我们爬取的数量越来越多,数据库压力也会慢慢增加,所以我们需要通过优化我们的项目,来让项目运行的更加流畅。

    最后再发一个这个项目的 git 地址:lemon1234_scraper: 一个基于 htmlunit 的 Java 爬虫项目(无页面版,有需要的同学可以自己 clone 下来后进行二开)。本项目切记慎用(请求频率过高,近乎DDOS的请求频率,一旦造成服务器瘫痪,约等于网络攻击),仅供学习使用~~~icon-default.png?t=M5H6https://gitee.com/soul-sys/lemon1234_scraper


    这一讲就讲到这里,有问题可以联系我:QQ 2100363119,欢迎大家访问我的个人网站:https://www.lemon1234.com

  • 相关阅读:
    bvh文件,人体骨骼重定向
    C语言习题---(数组)
    [鹏城杯 2022]简单的php - 无数字字母RCE+取反【*】
    如何使用SQL系列 之 如何在MySQL中使用索引
    【详解配置文件系列】es7配置文件详解
    思腾云计算
    CVE-2017-12615 Tomcat远程命令执行漏洞
    如何创建一个aop
    【SAP后台配置】如何通过前台屏幕字段找到对应SPRO后台路径?
    spoof_call的分析与改进
  • 原文地址:https://blog.csdn.net/weixin_45908370/article/details/125598700