一个简单的爬虫,用于抓取csdn上的每周干货推荐。
使用到的相关技术:SpringBoot、Redis、Jsoup、JQuery、Bootstrap等。
示例地址:
http://tinyspider.anxpp.com/
效果图:
准备熟悉下Spring Boot + Redis的使用,所以就想到爬点东西出来,于是用上了号称Java版JQuery的Jsoup,实现的功能是获取每周的CSDN推荐文章,并缓存到Redis中(当然也可以持久化到数据库,相关配置已添加,只是没有实现),网页解析部分已抽象为接口,根据要抓取的不同网页,可以自定义对应的实现,也就是可以爬取任何网页了。
解析网页的方法返回的数据为List
下面介绍具体实现的步骤。
Spring Boot工程的搭建不用多说了,不管是Eclipse还是Idea,Spring都提供了懒人工具,可根据要使用的组件一键生成项目。
下面是Redis,首先是引入依赖:
然后添加配置文件:
- #Redis
- spring.redis.database=0
- spring.redis.host=****
- spring.redis.password=a****
- spring.redis.pool.max-active=8
- spring.redis.pool.max-idle=8
- spring.redis.pool.max-wait=-1
- spring.redis.pool.min-idle=0
- spring.redis.port=****
- #spring.redis.sentinel.master= # Name of Redis server.
- #spring.redis.sentinel.nodes= # Comma-separated list of host:port pairs.
- spring.redis.timeout=0
ip和端口请自行根据实际情况填写。
然后是配置Redis,此处使用JavaConfig的方式:
- package com.anxpp.tinysoft.config;
- import com.fasterxml.jackson.annotation.JsonAutoDetect;
- import com.fasterxml.jackson.annotation.PropertyAccessor;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.cache.CacheManager;
- import org.springframework.cache.annotation.EnableCaching;
- import org.springframework.cache.interceptor.KeyGenerator;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.data.redis.cache.RedisCacheManager;
- import org.springframework.data.redis.connection.RedisConnectionFactory;
- import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
- /**
- * Redis缓存配置
- * Created by anxpp.com on 2017/3/11.
- */
- @Configuration
- @EnableCaching
- public class RedisCacheConfig {
- @Value("${spring.redis.host}")
- private String host;
- @Value("${spring.redis.port}")
- private int port;
- @Value("${spring.redis.timeout}")
- private int timeout;
- @Value("${spring.redis.password}")
- private String password;
- @Bean
- public KeyGenerator csdnKeyGenerator() {
- return (target, method, params) -> {
- StringBuilder sb = new StringBuilder();
- sb.append(target.getClass().getName());
- sb.append(method.getName());
- for (Object obj : params) {
- sb.append(obj.toString());
- }
- return sb.toString();
- };
- }
- @Bean
- public JedisConnectionFactory redisConnectionFactory() {
- JedisConnectionFactory factory = new JedisConnectionFactory();
- factory.setHostName(host);
- factory.setPort(port);
- factory.setPassword(password);
- factory.setTimeout(timeout); //设置连接超时时间
- return factory;
- }
- @Bean
- public CacheManager cacheManager(RedisTemplate redisTemplate) {
- RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
- // Number of seconds before expiration. Defaults to unlimited (0)
- cacheManager.setDefaultExpiration(10); //设置key-value超时时间
- return cacheManager;
- }
- @Bean
- public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
- StringRedisTemplate template = new StringRedisTemplate(factory);
- setSerializer(template); //设置序列化工具,这样ReportBean不需要实现Serializable接口
- template.afterPropertiesSet();
- return template;
- }
- private void setSerializer(StringRedisTemplate template) {
- Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
- ObjectMapper om = new ObjectMapper();
- om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
- om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
- jackson2JsonRedisSerializer.setObjectMapper(om);
- template.setValueSerializer(jackson2JsonRedisSerializer);
- }
- }
如果我们有多个程序(甚至是不同语言编写的的),需要注意Redis的key和value的序列化机制,比如PHP和Java中使用Redis的默认序列化机制是不同的,如果不做配置,可能会导致两边存的数据互相取不出来。
这样一来,就配置好了,后面直接使用就好。
首先定义网页解析接口:
- package com.anxpp.tinysoft.Utils.analyzer;
- import org.jsoup.nodes.Document;
- import java.util.List;
- import java.util.Map;
- /**
- * 解析html文档抽象
- * Created by anxpp.com on 2017/3/11.
- */
- public interface DocumentAnalyzer {
- /**
- * 根据html文档对象获取List
- * @param document html文档对象
- * @return 结果
- */
- List<Map<String,Object>> forListMap(Document document);
- }
针对csdn的每周干货推荐,编写具体实现:
- package com.anxpp.tinysoft.Utils.analyzer.impl;
- import com.anxpp.tinysoft.Utils.analyzer.DocumentAnalyzer;
- import org.jsoup.nodes.Document;
- import org.springframework.stereotype.Component;
- import org.springframework.util.ObjectUtils;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- /**
- * 解析CSDN每周知识干货html文档具体实现
- * Created by anxpp.com on 2017/3/11.
- */
- @Component
- public class CsdnWeeklyDocumentAnalyzer implements DocumentAnalyzer {
- /**
- * 根据html文档对象获取List
- * @param document html文档对象
- * @return 结果
- */
- @Override
- public List<Map<String,Object>> forListMap(Document document) {
- List<Map<String,Object>> results = new ArrayList<>();
- if(ObjectUtils.isEmpty(document))
- return results;
- document.body().getElementsByClass("pclist").get(0).children().forEach(ele -> {
- Map<String,Object> result = new HashMap<>();
- result.put("type",ele.getElementsByTag("span").get(0).getElementsByTag("a").get(0).attr("href"));
- result.put("img",ele.getElementsByTag("span").get(0).getElementsByTag("a").get(0).getElementsByTag("img").get(0).attr("src"));
- result.put("url",ele.getElementsByTag("span").get(1).getElementsByTag("a").get(0).attr("href"));
- result.put("name",ele.getElementsByTag("span").get(1).getElementsByTag("a").get(0).text());
- result.put("views",Integer.valueOf(ele.getElementsByTag("span").get(1).getElementsByTag("span").get(0).getElementsByTag("em").get(0).text().replaceAll("\\D+","")));
- result.put("collections",Integer.valueOf(ele.getElementsByTag("span").get(1).getElementsByTag("span").get(1).getElementsByTag("em").get(0).text().replaceAll("\\D+","")));
- results.add(result);
- });
- return results;
- }
- }
当然,如果需要解析其他网页,实现DocumentAnalyzer接口,完成对应的解析方式也是完全可以的。
然后,我们需要一个工具将Map转换为实体对象:
- package com.anxpp.tinysoft.Utils;
- import java.lang.reflect.Method;
- import java.util.Map;
- import java.util.Set;
- /**
- * 简单工具集合
- * Created by anxpp.com on 2017/3/11.
- */
- class TinyUtil {
- /**
- * map转对象
- *
- * @param map map
- * @param type 类型
- * @param
泛型
- * @return 对象
- * @throws Exception 反射异常
- */
- static <T> T mapToBean(Map<String, Object> map, Class<T> type) throws Exception {
- if (map == null) {
- return null;
- }
- Set<Map.Entry<String, Object>> sets = map.entrySet();
- T entity = type.newInstance();
- Method[] methods = type.getDeclaredMethods();
- for (Map.Entry<String, Object> entry : sets) {
- String str = entry.getKey();
- String setMethod = "set" + str.substring(0, 1).toUpperCase() + str.substring(1);
- for (Method method : methods) {
- if (method.getName().equals(setMethod)) {
- method.invoke(entity, entry.getValue());
- }
- }
- }
- return entity;
- }
- }
下面就是具体的数据获取逻辑了。
首先我们需要定义一个实体:
- package com.anxpp.tinysoft.core.entity;
- import javax.persistence.*;
- import java.util.Date;
- /**
- * 文章信息
- * Created by anxpp.com on 2017/3/11.
- */
- @Entity
- @Table(name = "t_csdn_weekly_article")
- public class Article extends BaseEntity{
- /**
- * 文章名称
- */
- private String name;
- /**
- * 文章名称
- */
- private String url;
- /**
- * 属于哪一期
- */
- private Integer stage;
- /**
- * 浏览量
- */
- private Integer views;
- /**
- * 收藏数
- */
- private Integer collections;
- /**
- * 所属知识库类别
- */
- private String type;
- /**
- * 类别图片地址
- */
- private String img;
- /**
- * 更新时间
- */
- @Column(name = "update_at", nullable = false)
- @Temporal(TemporalType.TIMESTAMP)
- private Date updateAt;
- //省略get set 方法
- }
如果要持久化数据到数据库,也可以添加Repo层,使用Spring Data JPA也是超级方便的,博客中已提供相关文章参考,此处直接使用Redis,跳过此层。
Service接口定义:
- package com.anxpp.tinysoft.core.service;
- import com.anxpp.tinysoft.core.entity.Article;
- import java.util.List;
- /**
- * 文章数据service
- * Created by anxpp.com on 2017/3/11.
- */
- public interface ArticleService {
- /**
- * 根据期号获取文章列表
- * @param stage 期号
- * @return 文章列表
- */
- List<Article> forWeekly(Integer stage) throws Exception;
- }
Service实现:
- package com.anxpp.tinysoft.core.service.impl;
- import com.anxpp.tinysoft.Utils.ArticleSpider;
- import com.anxpp.tinysoft.Utils.analyzer.impl.CsdnWeeklyDocumentAnalyzer;
- import com.anxpp.tinysoft.core.entity.Article;
- import com.anxpp.tinysoft.core.service.ArticleService;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.cache.annotation.Cacheable;
- import org.springframework.stereotype.Service;
- import javax.annotation.Resource;
- import java.util.List;
- /**
- * 文章service实现
- * Created by anxpp.com on 2017/3/11.
- */
- @Service
- public class ArticleServiceImpl implements ArticleService {
- @Value("${csdn.weekly.preurl}")
- private String preUrl;
- @Resource
- private CsdnWeeklyDocumentAnalyzer csdnWeeklyDocumentAnalyzer;
- /**
- * 根据期号获取文章列表
- *
- * @param stage 期号
- * @return 文章列表
- */
- @Override
- @Cacheable(value = "reportcache", keyGenerator = "csdnKeyGenerator")
- public List<Article> forWeekly(Integer stage) throws Exception {
- List<Article> articleList = ArticleSpider.forEntityList(preUrl + stage, csdnWeeklyDocumentAnalyzer, Article.class);
- articleList.forEach(article -> article.setStage(stage));
- return articleList;
- }
- }
csdn.weekly.preurl为配置文件中配置的url前缀,后面会放出完整的配置文件。
最后就是提供对外的接口,本文只添加了一个,也可以按需添加其他API:
- package com.anxpp.tinysoft.controller;
- import com.anxpp.tinysoft.core.entity.Article;
- import com.anxpp.tinysoft.core.service.ArticleService;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PathVariable;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.ResponseBody;
- import javax.annotation.Resource;
- import java.util.List;
- /**
- * 默认页面
- * Created by anxpp.com on 2017/3/11.
- */
- @Controller
- @RequestMapping("/article")
- public class ArticleController {
- @Resource
- private ArticleService articleService;
- @ResponseBody
- @GetMapping("/get/stage/{stage}")
- public List<Article> getArticleByStage(@PathVariable("stage") Integer stage) throws Exception {
- return articleService.forWeekly(stage);
- }
- }
完整的配置文件:
- server.port=****
- #DataSource
- spring.datasource.url=jdbc:mysql://****.***:****/****?createDatabaseIfNotExist=true
- spring.datasource.username=****
- spring.datasource.password=****
- spring.datasource.driver-class-name=com.mysql.jdbc.Driver
- #multiple Setting
- spring.jpa.hibernate.ddl-auto=update
- spring.jpa.show-sql=true
- #Redis
- spring.redis.database=0
- spring.redis.host=****
- spring.redis.password=a****
- spring.redis.pool.max-active=8
- spring.redis.pool.max-idle=8
- spring.redis.pool.max-wait=-1
- spring.redis.pool.min-idle=0
- spring.redis.port=****
- #spring.redis.sentinel.master= # Name of Redis server.
- #spring.redis.sentinel.nodes= # Comma-separated list of host:port pairs.
- spring.redis.timeout=0
- #csdn setting
- csdn.weekly.preurl=http://lib.csdn.net/weekly/
数据库等的配置请根据实际情况配置。
现在启动程序即可访问。
源码已提交到GitHub:https://github.com/anxpp/csdnweeklySpider。
后续有时间会继续完善本程序添加更多网站内容的抓取。