Back
Featured image of post 门面+模板+策略实现网站爬虫

门面+模板+策略实现网站爬虫

一、需求背景

​ 最近项目上有这样一个需求,需要对重点网站进行数据爬取。这些不同网站的爬取步骤都是相同的,可以拆分为如下的步骤:

列表页url爬取->根据url解析详情页->数据处理

而且这些不同爬虫网站有一部分的url爬取代码逻辑和url解析的逻辑有很多相同的地方,比如:url爬取拿到页面的a标签和动态滚动下拉获取整个页面内容等等。因此考虑综合使用设计模式来进行爬取功能的代码设计。

二、模式拆解

1.模板方法模式:因网站爬取的步骤都相同,而且可能还会存在一些重复的代码逻辑,所以采用模式方法模式来做整个爬取流程的抽象骨架

2.策略模式:因存在多个网站,我们需要在爬取时只根据不同的参数可以走不同网站的爬取逻辑,因此采用策略模式来达到运行时动态的切换网站爬取逻辑

3.门面模式:爬取时需要一个统一的入口,客户端只需要关心传什么参数,一些选择爬取流程的逻辑不应该暴露给外部,因此采用门面模式。

三、具体实现

  • SpiderEnum枚举

    这个枚举主要针对一些重点的网站,当添加不同的网站时可以添加枚举的值,同时枚举也作为爬取策略的选择参数

    /**
     * 爬虫业务流程枚举
     *
     * @author zp
     */
    public enum SpiderEnum {
        UNKNOWN("未知网站"),
        SHAI("xxxx网");
    
        private String value;
    
        SpiderEnum(String value) {
            this.value = value;
        }
    
        public String getValue() {
            return value;
        }
    
        public static SpiderEnum fromValue(String value) {
            for (SpiderEnum option : values()) {
    
                if (option.getValue().equals(value)){
                    return option;
                }
            }
            return UNKNOWN;
        }
    
    }
    
  • AbstractSpiderFlow抽象爬取流程

    抽象爬取流程主要用于固定爬取步骤的算法骨架,以及存储策略实例和枚举的对应关系和一些通用代码的编写。

    @Slf4j
    public abstract class AbstractSpiderFlow implements Consumer<WebDriver>{
    
        private static final int CORE_POOL_SIZE = 50;
        private static final int MAX_POOL_SIZE = 100;
        private static final int QUEUE_CAPACITY = 1000;
        private static final Long KEEP_ALIVE_TIME = 1L;
    
        @Resource
        private SeleniumDownloader downloader;
    
        /**
         * 存储爬取策略
         */
        private static Map<SpiderEnum,String> SpiderSiteMap =new ConcurrentHashMap<>();
    
    
        public AbstractSpiderFlow() {
            SpiderSiteMap.put(this.accessWorkflow(),this.accessBeanName());
        }
    
    
        /**
         * 爬虫模板方法,部分方法做钩子方法
         *
         */
        final void SpiderBegin(Tuple2<String,SpiderEnum> tuple){
    
            //1.解析基础网站的信息
            String context = downloader.downLoad(tuple._1,this);
    
            Set<String> urls=crawlerUrl(context);
    
            //2.根据url解析不同页面的规则,解析数据并做数据处理
            ........省略代码
            if (urls.isEmpty()){
                log.error("未爬取到url无法进行后续爬取,网址为---{}",tuple._1);
    
                throw new RuntimeException("url爬取异常");
    
            }else {
    
                resolveUrl(urls);
            }
    
        }
    
    
    
        /**
         * 下载器消费接口默认动态下拉,主要用于url爬取阶段通常需要一些特定的下载处理,
         * 在页面解析阶段由子类控制是否需要特定的解析规则
         */
        @Override
        public void accept(WebDriver webDriver) {
            //模拟下拉,刷新页面
    		for (int i=0; i < 5; i++){
    
    			try {
    				//滚动到最底部
    				((JavascriptExecutor)webDriver).executeScript("window.scrollTo(0,document.body.scrollHeight)");
    				//休眠,等待加载页面
    				Thread.sleep(1000);
    				//往回滚一点,否则不加载
    				((JavascriptExecutor)webDriver).executeScript("window.scrollBy(0,-300)");
    			} catch (InterruptedException e) {
                    log.error("页面下拉时出现打断异常");
    				e.printStackTrace();
    			}
    		}
    
        }
    
    
    
        /**
         * 通用url解析规则,子类可自由实现
         */
        protected Set<String> crawlerUrl(String context){
    
    
            //默认抓取网站下的所有a标签
            Document document = Jsoup.parse(context);
    
            Elements body = document.select("body");
    
            return  body.select("a").stream().map(href -> href.attr("href")).collect(Collectors.toSet());
    
        }
    
    
        /**
         * 根据url解析规则
         */
        protected void resolveUrl(Set<String> urls){
            //拿到爬取集合,下载页面
            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                    CORE_POOL_SIZE,
                    MAX_POOL_SIZE,
                    KEEP_ALIVE_TIME,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                    new ThreadPoolExecutor.CallerRunsPolicy());
    
                urls.forEach(x->
                    executor.execute(()->{
                        //解析页面
                        resolveContext(downloader,x);
                    })
            );
    
    
        }
    
    
        /**
         * 验证访问请求处理程序
         *
         */
        public static final AbstractSpiderFlow accessRequestValidationHandler(SpiderEnum workflowId) {
            String beanName = SpiderSiteMap.get(workflowId);
            if(StringUtils.isEmpty(beanName)) {
                log.error("can not find {}'s component",beanName);
    
                throw new RuntimeException("can not find "+beanName + "'s component,current UPDATE_WORKFLOW_ID is :" + workflowId.getValue());
            }
            return ApplicationUtil.getApplicationContext().getBean(beanName,AbstractSpiderFlow.class);
        }
    
    
    
        //页面解析
        protected abstract void resolveContext(SeleniumDownloader downloader,String url);
    
    
    
    
        protected abstract SpiderEnum accessWorkflow();
    
    
    
    
    
        protected abstract String accessBeanName();
    
    
    }
    

    这里实现了一个Consumer接口是为了能够为了让不同的网站可以有不同的页面下载爬取策略例如:动态下拉、分页等。同时将当前类作为下载的爬取策略传递给下载器。

  • SeleniumDownloader下载器

     public String downLoad(String url, Consumer<WebDriver> consumer) {
            checkInit();
            WebDriver webDriver;
       			......................省略代码
    			//这里用来消费不同爬取策略对于页面渲染的不同操作
                if (Objects.nonNull(consumer)){
                    consumer.accept(webDriver);
                }
    
    
                WebElement webElement = webDriver.findElement(By.xpath("/html"));
    
                String html = webElement.getAttribute("outerHTML");
                return html;
            }catch (Exception e){
                e.printStackTrace();
                return null;
            }finally {
                webDriverPool.returnToPool(webDriver);
            }
        }
    
        private void checkInit() {
            if (webDriverPool == null) {
                synchronized (this) {
                    webDriverPool = new WebDriverPool(poolSize);
                }
            }
        }
    
  • XXXSpiderFlow

    具体网站的爬取策略,可重用抽象流程的代码也可以,自定义一些爬取的逻辑

    
    @Component(value = XXXSpiderFlow.BEAN_NAME)
    @Slf4j
    public class XXXSpiderFlow extends AbstractSpiderFlow {
    
        public static final String BEAN_NAME = "XXXSpiderFlow";
    
        public static final HashSet<String> customList=new HashSet<>();
    
    
        @Override
        public void accept(WebDriver webDriver) {
            RemoteWebDriver driver = (RemoteWebDriver) webDriver;
            //先在xx页分页
            spiderPaging(driver);
    ....省略代代码
        }
    
    
        protected Set<String> crawlerUrl(String context){
         ....省略代码重写url获取逻辑
        }
    
    
        @Override
        protected void resolveContext(SeleniumDownloader seleniumDownloader, String analysisUrl) {
    
            try{
             ...解析省略代码
    
            }catch (RuntimeException e){
                      e.printStackTrace();
                   log.error("网站解析失败-----{}",analysisUrl);
            }
    
    
        }
    
        /**
         * 获得页面的url
         *
         */
        public Set<String> getHtmlUrls(String context){
            //默认抓取网站下的所有a标签
          ....省略代码
    
    
        /**
         * 爬虫分页逻辑
         *
         */
        private void spiderPaging(RemoteWebDriver driver){
            //新闻分页
            WebElement nextLink=null;
            do {
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
            	.....省略代码
                    WebElement webElement = driver.findElement(By.xpath("/html"));
                    String html = webElement.getAttribute("outerHTML");
                    customList.add(html);
                }catch (WebDriverException e){
                    log.info("已经到达尾页无法继续解析--------");
                    nextLink=null;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }while (nextLink!=null);
        }
    
    
        @Override
        protected SpiderEnum accessWorkflow() {
           return SpiderEnum.SHAI;
        }
    
    
    
    
    
        @Override
        protected String accessBeanName() {
            return BEAN_NAME;
        }
    }
    
  • SpiderFlowFacade门面类

    经过以上步骤整个爬取流程就已经完成这时还需要一个门面类来让客户端根据不同的枚举值调用执行不同的爬取策略。

    @Component
    public class SpiderFlowFacade {
    
        public void beginInSpiderFlow(Tuple2<String,SpiderEnum> tuple) {
            choseSpiderFlow(tuple).SpiderBegin(tuple);
        }
    
        private AbstractSpiderFlow choseSpiderFlow(Tuple2<String,SpiderEnum> tuple) {
            SpiderEnum workflowId = tuple._2;
            AbstractSpiderFlow spiderFlow = AbstractSpiderFlow.accessRequestValidationHandler(workflowId);
            return spiderFlow;
        }
    }
    

四、总结

以上就是本人在实际工作中,根据具体的业务逻辑使用设计模式的一次尝试,可能还有很大的不足欢迎大家去帮忙指正改进。