共计 3097 个字符,预计需要花费 8 分钟才能阅读完成。
Java 爬虫:使用 Jvppeteer(Puppeteer) 轻松爬淘宝商品
想要爬取某宝的商品,如果只是用 HttpURLConnection 发个请求,失败率是很高的。一般想要保证成功率的话,都会选择真实的浏览器去抓取。
以前常用的解决方案是 selenium 或 phantomjs,但是它两的环境配置太麻烦了,对程序员极度不友好,自从谷歌推出 Puppeteer 后,puppeteer 迅速流行起来,获得大家一致称赞。它是一个 NodeJS 库,但今天并不是要使用它来爬取某宝商品,而是使用 Java 语言写的 Jvppeteer,Jvppeteer 与 Puppeteer 是同样的实现原理。
思路
- 使用多线程,一个线程负责一个页面的爬取(接下来的内容会使用 page 代替页面)
- 创建与线程池线程数相同的 page 队列,page 放在 LinkedBlockingQueue 队列里,每当有爬取任务时,就从队列里取出一个 page, 爬取任务完成时,将 page 放回队列的后面。这样做的原因是重复利用 page, 减少页面的创建频率,但是要注意的是一个页面不能利用太久或者次数太多,防止出现 crash 的情况
- 拦截图片和多媒体资源的加载,多媒体资源和图片的加载会极大影响页面的加载速度,从而影响爬虫效率,所以要拦截(可选)。
-
我们选择获取整个页面内容,然后解析得到商品信息
代码实现
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
正文完