anxpp的博客

As they sow , so let them reap .

基于Spring Boot+jsoup+redis抓取CSDN每周干货的RESTFul爬虫

    一个简单的爬虫,用于抓取csdn上的每周干货推荐。

    使用到的相关技术:SpringBoot、Redis、Jsoup、JQuery、Bootstrap等。

示例地址:

    http://tinyspider.anxpp.com/

效果图:

 

1、写在前面

    准备熟悉下Spring Boot + Redis的使用,所以就想到爬点东西出来,于是用上了号称Java版JQuery的Jsoup,实现的功能是获取每周的CSDN推荐文章,并缓存到Redis中(当然也可以持久化到数据库,相关配置已添加,只是没有实现),网页解析部分已抽象为接口,根据要抓取的不同网页,可以自定义对应的实现,也就是可以爬取任何网页了。

    解析网页的方法返回的数据为List<Map>,再定义对应的实体,可以直接反射为实体(已实现),具体见后文的代码介绍。

    下面介绍具体实现的步骤。

2、搭建Spring Boot并集成Redis

    Spring Boot工程的搭建不用多说了,不管是Eclipse还是Idea,Spring都提供了懒人工具,可根据要使用的组件一键生成项目。

    下面是Redis,首先是引入依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>

    然后添加配置文件:

  1. #Redis
  2. spring.redis.database=0
  3. spring.redis.host=****
  4. spring.redis.password=a****
  5. spring.redis.pool.max-active=8
  6. spring.redis.pool.max-idle=8
  7. spring.redis.pool.max-wait=-1
  8. spring.redis.pool.min-idle=0
  9. spring.redis.port=****
  10. #spring.redis.sentinel.master= # Name of Redis server.
  11. #spring.redis.sentinel.nodes= # Comma-separated list of host:port pairs.
  12. spring.redis.timeout=0

    ip和端口请自行根据实际情况填写。

    然后是配置Redis,此处使用JavaConfig的方式:

  1. package com.anxpp.tinysoft.config;
  2. import com.fasterxml.jackson.annotation.JsonAutoDetect;
  3. import com.fasterxml.jackson.annotation.PropertyAccessor;
  4. import com.fasterxml.jackson.databind.ObjectMapper;
  5. import org.springframework.beans.factory.annotation.Value;
  6. import org.springframework.cache.CacheManager;
  7. import org.springframework.cache.annotation.EnableCaching;
  8. import org.springframework.cache.interceptor.KeyGenerator;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.context.annotation.Configuration;
  11. import org.springframework.data.redis.cache.RedisCacheManager;
  12. import org.springframework.data.redis.connection.RedisConnectionFactory;
  13. import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
  14. import org.springframework.data.redis.core.RedisTemplate;
  15. import org.springframework.data.redis.core.StringRedisTemplate;
  16. import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
  17. /**
  18. * Redis缓存配置
  19. * Created by anxpp.com on 2017/3/11.
  20. */
  21. @Configuration
  22. @EnableCaching
  23. public class RedisCacheConfig {
  24. @Value("${spring.redis.host}")
  25. private String host;
  26. @Value("${spring.redis.port}")
  27. private int port;
  28. @Value("${spring.redis.timeout}")
  29. private int timeout;
  30. @Value("${spring.redis.password}")
  31. private String password;
  32. @Bean
  33. public KeyGenerator csdnKeyGenerator() {
  34. return (target, method, params) -> {
  35. StringBuilder sb = new StringBuilder();
  36. sb.append(target.getClass().getName());
  37. sb.append(method.getName());
  38. for (Object obj : params) {
  39. sb.append(obj.toString());
  40. }
  41. return sb.toString();
  42. };
  43. }
  44. @Bean
  45. public JedisConnectionFactory redisConnectionFactory() {
  46. JedisConnectionFactory factory = new JedisConnectionFactory();
  47. factory.setHostName(host);
  48. factory.setPort(port);
  49. factory.setPassword(password);
  50. factory.setTimeout(timeout); //设置连接超时时间
  51. return factory;
  52. }
  53. @Bean
  54. public CacheManager cacheManager(RedisTemplate redisTemplate) {
  55. RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
  56. // Number of seconds before expiration. Defaults to unlimited (0)
  57. cacheManager.setDefaultExpiration(10); //设置key-value超时时间
  58. return cacheManager;
  59. }
  60. @Bean
  61. public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
  62. StringRedisTemplate template = new StringRedisTemplate(factory);
  63. setSerializer(template); //设置序列化工具,这样ReportBean不需要实现Serializable接口
  64. template.afterPropertiesSet();
  65. return template;
  66. }
  67. private void setSerializer(StringRedisTemplate template) {
  68. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
  69. ObjectMapper om = new ObjectMapper();
  70. om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  71. om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
  72. jackson2JsonRedisSerializer.setObjectMapper(om);
  73. template.setValueSerializer(jackson2JsonRedisSerializer);
  74. }
  75. }

    如果我们有多个程序(甚至是不同语言编写的的),需要注意Redis的key和value的序列化机制,比如PHP和Java中使用Redis的默认序列化机制是不同的,如果不做配置,可能会导致两边存的数据互相取不出来。

    这样一来,就配置好了,后面直接使用就好。

3、网页解析抽象和csdnweekly实现

    首先定义网页解析接口:

  1. package com.anxpp.tinysoft.Utils.analyzer;
  2. import org.jsoup.nodes.Document;
  3. import java.util.List;
  4. import java.util.Map;
  5. /**
  6. * 解析html文档抽象
  7. * Created by anxpp.com on 2017/3/11.
  8. */
  9. public interface DocumentAnalyzer {
  10. /**
  11. * 根据html文档对象获取List<Map>
  12. * @param document html文档对象
  13. * @return 结果
  14. */
  15. List<Map<String,Object>> forListMap(Document document);
  16. }

    针对csdn的每周干货推荐,编写具体实现:

  1. package com.anxpp.tinysoft.Utils.analyzer.impl;
  2. import com.anxpp.tinysoft.Utils.analyzer.DocumentAnalyzer;
  3. import org.jsoup.nodes.Document;
  4. import org.springframework.stereotype.Component;
  5. import org.springframework.util.ObjectUtils;
  6. import java.util.ArrayList;
  7. import java.util.HashMap;
  8. import java.util.List;
  9. import java.util.Map;
  10. /**
  11. * 解析CSDN每周知识干货html文档具体实现
  12. * Created by anxpp.com on 2017/3/11.
  13. */
  14. @Component
  15. public class CsdnWeeklyDocumentAnalyzer implements DocumentAnalyzer {
  16. /**
  17. * 根据html文档对象获取List<Map>
  18. * @param document html文档对象
  19. * @return 结果
  20. */
  21. @Override
  22. public List<Map<String,Object>> forListMap(Document document) {
  23. List<Map<String,Object>> results = new ArrayList<>();
  24. if(ObjectUtils.isEmpty(document))
  25. return results;
  26. document.body().getElementsByClass("pclist").get(0).children().forEach(ele -> {
  27. Map<String,Object> result = new HashMap<>();
  28. result.put("type",ele.getElementsByTag("span").get(0).getElementsByTag("a").get(0).attr("href"));
  29. result.put("img",ele.getElementsByTag("span").get(0).getElementsByTag("a").get(0).getElementsByTag("img").get(0).attr("src"));
  30. result.put("url",ele.getElementsByTag("span").get(1).getElementsByTag("a").get(0).attr("href"));
  31. result.put("name",ele.getElementsByTag("span").get(1).getElementsByTag("a").get(0).text());
  32. result.put("views",Integer.valueOf(ele.getElementsByTag("span").get(1).getElementsByTag("span").get(0).getElementsByTag("em").get(0).text().replaceAll("\\D+","")));
  33. result.put("collections",Integer.valueOf(ele.getElementsByTag("span").get(1).getElementsByTag("span").get(1).getElementsByTag("em").get(0).text().replaceAll("\\D+","")));
  34. results.add(result);
  35. });
  36. return results;
  37. }
  38. }

    当然,如果需要解析其他网页,实现DocumentAnalyzer接口,完成对应的解析方式也是完全可以的。

    然后,我们需要一个工具将Map转换为实体对象:

  1. package com.anxpp.tinysoft.Utils;
  2. import java.lang.reflect.Method;
  3. import java.util.Map;
  4. import java.util.Set;
  5. /**
  6. * 简单工具集合
  7. * Created by anxpp.com on 2017/3/11.
  8. */
  9. class TinyUtil {
  10. /**
  11. * map转对象
  12. *
  13. * @param map map
  14. * @param type 类型
  15. * @param <T> 泛型
  16. * @return 对象
  17. * @throws Exception 反射异常
  18. */
  19. static <T> T mapToBean(Map<String, Object> map, Class<T> type) throws Exception {
  20. if (map == null) {
  21. return null;
  22. }
  23. Set<Map.Entry<String, Object>> sets = map.entrySet();
  24. T entity = type.newInstance();
  25. Method[] methods = type.getDeclaredMethods();
  26. for (Map.Entry<String, Object> entry : sets) {
  27. String str = entry.getKey();
  28. String setMethod = "set" + str.substring(0, 1).toUpperCase() + str.substring(1);
  29. for (Method method : methods) {
  30. if (method.getName().equals(setMethod)) {
  31. method.invoke(entity, entry.getValue());
  32. }
  33. }
  34. }
  35. return entity;
  36. }
  37. }

    下面就是具体的数据获取逻辑了。

4、提供API以及数据获取逻辑

    首先我们需要定义一个实体:

  1. package com.anxpp.tinysoft.core.entity;
  2. import javax.persistence.*;
  3. import java.util.Date;
  4. /**
  5. * 文章信息
  6. * Created by anxpp.com on 2017/3/11.
  7. */
  8. @Entity
  9. @Table(name = "t_csdn_weekly_article")
  10. public class Article extends BaseEntity{
  11. /**
  12. * 文章名称
  13. */
  14. private String name;
  15. /**
  16. * 文章名称
  17. */
  18. private String url;
  19. /**
  20. * 属于哪一期
  21. */
  22. private Integer stage;
  23. /**
  24. * 浏览量
  25. */
  26. private Integer views;
  27. /**
  28. * 收藏数
  29. */
  30. private Integer collections;
  31. /**
  32. * 所属知识库类别
  33. */
  34. private String type;
  35. /**
  36. * 类别图片地址
  37. */
  38. private String img;
  39. /**
  40. * 更新时间
  41. */
  42. @Column(name = "update_at", nullable = false)
  43. @Temporal(TemporalType.TIMESTAMP)
  44. private Date updateAt;
  45. //省略get set 方法
  46. }

    如果要持久化数据到数据库,也可以添加Repo层,使用Spring Data JPA也是超级方便的,博客中已提供相关文章参考,此处直接使用Redis,跳过此层。

    Service接口定义:

  1. package com.anxpp.tinysoft.core.service;
  2. import com.anxpp.tinysoft.core.entity.Article;
  3. import java.util.List;
  4. /**
  5. * 文章数据service
  6. * Created by anxpp.com on 2017/3/11.
  7. */
  8. public interface ArticleService {
  9. /**
  10. * 根据期号获取文章列表
  11. * @param stage 期号
  12. * @return 文章列表
  13. */
  14. List<Article> forWeekly(Integer stage) throws Exception;
  15. }

    Service实现:

  1. package com.anxpp.tinysoft.core.service.impl;
  2. import com.anxpp.tinysoft.Utils.ArticleSpider;
  3. import com.anxpp.tinysoft.Utils.analyzer.impl.CsdnWeeklyDocumentAnalyzer;
  4. import com.anxpp.tinysoft.core.entity.Article;
  5. import com.anxpp.tinysoft.core.service.ArticleService;
  6. import org.springframework.beans.factory.annotation.Value;
  7. import org.springframework.cache.annotation.Cacheable;
  8. import org.springframework.stereotype.Service;
  9. import javax.annotation.Resource;
  10. import java.util.List;
  11. /**
  12. * 文章service实现
  13. * Created by anxpp.com on 2017/3/11.
  14. */
  15. @Service
  16. public class ArticleServiceImpl implements ArticleService {
  17. @Value("${csdn.weekly.preurl}")
  18. private String preUrl;
  19. @Resource
  20. private CsdnWeeklyDocumentAnalyzer csdnWeeklyDocumentAnalyzer;
  21. /**
  22. * 根据期号获取文章列表
  23. *
  24. * @param stage 期号
  25. * @return 文章列表
  26. */
  27. @Override
  28. @Cacheable(value = "reportcache", keyGenerator = "csdnKeyGenerator")
  29. public List<Article> forWeekly(Integer stage) throws Exception {
  30. List<Article> articleList = ArticleSpider.forEntityList(preUrl + stage, csdnWeeklyDocumentAnalyzer, Article.class);
  31. articleList.forEach(article -> article.setStage(stage));
  32. return articleList;
  33. }
  34. }

    csdn.weekly.preurl为配置文件中配置的url前缀,后面会放出完整的配置文件。

    最后就是提供对外的接口,本文只添加了一个,也可以按需添加其他API:

  1. package com.anxpp.tinysoft.controller;
  2. import com.anxpp.tinysoft.core.entity.Article;
  3. import com.anxpp.tinysoft.core.service.ArticleService;
  4. import org.springframework.stereotype.Controller;
  5. import org.springframework.web.bind.annotation.GetMapping;
  6. import org.springframework.web.bind.annotation.PathVariable;
  7. import org.springframework.web.bind.annotation.RequestMapping;
  8. import org.springframework.web.bind.annotation.ResponseBody;
  9. import javax.annotation.Resource;
  10. import java.util.List;
  11. /**
  12. * 默认页面
  13. * Created by anxpp.com on 2017/3/11.
  14. */
  15. @Controller
  16. @RequestMapping("/article")
  17. public class ArticleController {
  18. @Resource
  19. private ArticleService articleService;
  20. @ResponseBody
  21. @GetMapping("/get/stage/{stage}")
  22. public List<Article> getArticleByStage(@PathVariable("stage") Integer stage) throws Exception {
  23. return articleService.forWeekly(stage);
  24. }
  25. }

    完整的配置文件:

  1. server.port=****
  2. #DataSource
  3. spring.datasource.url=jdbc:mysql://****.***:****/****?createDatabaseIfNotExist=true
  4. spring.datasource.username=****
  5. spring.datasource.password=****
  6. spring.datasource.driver-class-name=com.mysql.jdbc.Driver
  7. #multiple Setting
  8. spring.jpa.hibernate.ddl-auto=update
  9. spring.jpa.show-sql=true
  10. #Redis
  11. spring.redis.database=0
  12. spring.redis.host=****
  13. spring.redis.password=a****
  14. spring.redis.pool.max-active=8
  15. spring.redis.pool.max-idle=8
  16. spring.redis.pool.max-wait=-1
  17. spring.redis.pool.min-idle=0
  18. spring.redis.port=****
  19. #spring.redis.sentinel.master= # Name of Redis server.
  20. #spring.redis.sentinel.nodes= # Comma-separated list of host:port pairs.
  21. spring.redis.timeout=0
  22. #csdn setting
  23. csdn.weekly.preurl=http://lib.csdn.net/weekly/

    数据库等的配置请根据实际情况配置。

    现在启动程序即可访问。

    

    源码已提交到GitHub:https://github.com/anxpp/csdnweeklySpider

    后续有时间会继续完善本程序添加更多网站内容的抓取。

阅读更多
版权声明:http://blog.csdn.net/anxpp https://blog.csdn.net/anxpp/article/details/61945975
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

基于Spring Boot+jsoup+redis抓取CSDN每周干货的RESTFul爬虫

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭