Java爬虫:使用Jvppeteer(Puppeteer)轻松爬淘宝商品

想要爬取某宝的商品,如果只是用HttpURLConnection发个请求,失败率是很高的。一般想要保证成功率的话,都会选择真实的浏览器去抓取。

以前常用的解决方案是selenium或phantomjs,但是它两的环境配置太麻烦了,对程序员极度不友好,自从谷歌推出Puppeteer后,puppeteer迅速流行起来,获得大家一致称赞。它是一个NodeJS库,但今天并不是要使用它来爬取某宝商品,而是使用Java语言写的Jvppeteer,Jvppeteer与Puppeteer是同样的实现原理。

思路

  1. 使用多线程,一个线程负责一个页面的爬取(接下来的内容会使用page代替页面)
  2. 创建与线程池线程数相同的page队列,page放在LinkedBlockingQueue队列里,每当有爬取任务时,就从队列里取出一个page,爬取任务完成时,将page放回队列的后面。这样做的原因是重复利用page,减少页面的创建频率,但是要注意的是一个页面不能利用太久或者次数太多,防止出现crash的情况
  3. 拦截图片和多媒体资源的加载,多媒体资源和图片的加载会极大影响页面的加载速度,从而影响爬虫效率,所以要拦截(可选)。
  4. 我们选择获取整个页面内容,然后解析得到商品信息

    代码实现

    1.启动浏览器
     //指定启动路径,启动浏览器        String path = new String("F:\\java教程\\49期\\vuejs\\puppeteer\\.local-chromium\\win64-722234\\chrome-win\\chrome.exe".getBytes(), "UTF-8");        ArrayList<String> argList = new ArrayList<>();        LaunchOptions options = new OptionsBuilder().withArgs(argList).withHeadless(false).withExecutablePath(path).build();        argList.add("--no-sandbox");        argList.add("--disable-setuid-sandbox");        Browser browser = Puppeteer.launch(options);
    2.创建page队列与线程池
    //启动一个线程池多线程抓取        int threadCount = 5;        ThreadPoolExecutor executor = new ThreadPoolExecutor(threadCount, threadCount, 30, TimeUnit.SECONDS, new LinkedBlockingDeque<>());        CompletionService service = new ExecutorCompletionService(executor);        //打开5个页面同时抓取,这些页面可以多次利用,这样减少创建网页带来的性能消耗        LinkedBlockingQueue<Page> pages = new LinkedBlockingQueue<>();        for (int i = 0; i < threadCount; i++) {            Page page = browser.newPage();            //拦截请求,可选,但是存在线程切换比较严重,建议不拦截//            page.onRequest(request -> {//                if ("image".equals(request.resourceType()) || "media".equals(request.resourceType())) {//                    //遇到多媒体或者图片资源请求,拒绝,加载页面加载//                    request.abort();//                } else {//其他资源放行//                    request.continueRequest();//                }//            });//            page.setRequestInterception(true);            pages.put(page);//往队列后面放,阻塞        }
    3.定义爬虫线程静态内部类
    static class CrawlerCallable implements Callable<Object> {        private LinkedBlockingQueue<Page> pages;        public CrawlerCallable(LinkedBlockingQueue<Page> pages) {            this.pages = pages;        }        @Override        public Object call() {            Page page = null;            try {                page = pages.take();                PageNavigateOptions navigateOptions = new PageNavigateOptions();                //如果不设置 domcontentloaded 算页面导航完成的话,那么goTo方法会超时,因为图片请求被拦截了,页面不会达到loaded阶段                navigateOptions.setWaitUntil(Arrays.asList("domcontentloaded"));                page.goTo("https://item.taobao.com/item.htm?id=541605195654", navigateOptions);                String content = page.content();                return parseItem(content);            } catch (Exception e) {                e.printStackTrace();            } finally {                if (page != null) {                    try {                        pages.put(page);//把已经抓取完的网页放回队列里                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }            return null;        }   }
    4.解析商品,获取结果
    //结果集        List<Future<Object>> futures = new ArrayList<>();        //抓取100次        long start = System.currentTimeMillis();        for (int i = 0; i < 100; i++) {            Future<Object> future = service.submit(new CrawlerCallable(pages));            futures.add(future);        }        //关闭线程池        executor.shutdown();        //获取结果        int i = 0;        for (Future<Object> result : futures) {            Object item = result.get();            i++;            System.out.println(i + ":" + Constant.OBJECTMAPPER.writeValueAsString(item));        }        long end = System.currentTimeMillis();        System.out.println("时间:" + (end - start));

经过测试,爬取的速度非常之快,100个任务用了15s就完成了,但不同的电脑配置和带宽有不同结果,因为爬虫是比较吃配置还有带宽的。

小结

除了创建page队列以外,还可以创建browser队列,因为一个browser永久也有可能发生crash的时候,可以在项目启动的时候启动一定数量的browser,当某个browser到达某个使用次数(比如2000)或者使用时间(2h)后关闭browser,然后取出队列中的另外一个browser,同时启动一个bowser放进队列里。

完整的代码地址:demo