Java 多线程爬虫及分布式爬虫架构探索
- 采集效率特别慢,单线程之间都是串行的,下一个执行动作需要等上一个执行完才能执行
- 对服务器的CUP等利用率不高,想想我们的服务器都是 8核16G,32G 的只跑一个线程会不会太浪费啦
维护待采集的 URL
- JDK 的安全队列,例如 LinkedBlockingQueue
- 高性能的 NoSQL,比如 Redis、Mongodb
- MQ 消息中间件
URL 的去重
- 将 URL 保存到数据库进行去重,比如 redis、MongoDB
- 将 URL 放到哈希表中去重,例如 hashset
- 将 URL 经过 MD5 之后保存到哈希表中去重,相比于上面一种,能够节约空间
- 使用 布隆过滤器(Bloom Filter)去重,这种方式能够节约大量的空间,就是不那么准确。
/** * 多线程爬虫 */public class ThreadCrawler implements Runnable { // 采集的文章数 private final AtomicLong pageCount = new AtomicLong(0); // 列表页链接正则表达式 public static final String URL_LIST = "https://voice.hupu.com/nba"; protected Logger logger = LoggerFactory.getLogger(getClass()); // 待采集的队列 LinkedBlockingQueue<String> taskQueue; // 采集过的链接列表 HashSet<String> visited; // 线程池 CountableThreadPool threadPool; /** * * @param url 起始页 * @param threadNum 线程数 * @throws InterruptedException */ public ThreadCrawler(String url, int threadNum) throws InterruptedException { this.taskQueue = new LinkedBlockingQueue<>(); this.threadPool = new CountableThreadPool(threadNum); this.visited = new HashSet<>(); // 将起始页添加到待采集队列中 this.taskQueue.put(url); } @Override public void run() { logger.info("Spider started!"); while (!Thread.currentThread().isInterrupted()) { // 从队列中获取待采集 URL final String request = taskQueue.poll(); // 如果获取 request 为空,并且当前的线程采已经没有线程在运行 if (request == null) { if (threadPool.getThreadAlive() == 0) { break; } } else { // 执行采集任务 threadPool.execute(new Runnable() { @Override public void run() { try { processRequest(request); } catch (Exception e) { logger.error("process request " + request + " error", e); } finally { // 采集页面 +1 pageCount.incrementAndGet(); } } }); } } threadPool.shutdown(); logger.info("Spider closed! {} pages downloaded.", pageCount.get()); } /** * 处理采集请求 * @param url */ protected void processRequest(String url) { // 判断是否为列表页 if (url.matches(URL_LIST)) { // 列表页解析出详情页链接添加到待采集URL队列中 processTaskQueue(url); } else { // 解析网页 processPage(url); } } /** * 处理链接采集 * 处理列表页,将 url 添加到队列中 * * @param url */ protected void processTaskQueue(String url) { try { Document doc = Jsoup.connect(url).get(); // 详情页链接 Elements elements = doc.select(" div.news-list > ul > li > div.list-hd > h4 > a"); elements.stream().forEach((element -> { String request = element.attr("href"); // 判断该链接是否存在队列或者已采集的 set 中,不存在则添加到队列中 if (!visited.contains(request) && !taskQueue.contains(request)) { try { taskQueue.put(request); } catch (InterruptedException e) { e.printStackTrace(); } } })); // 列表页链接 Elements list_urls = doc.select("div.voice-paging > a"); list_urls.stream().forEach((element -> { String request = element.absUrl("href"); // 判断是否符合要提取的列表链接要求 if (request.matches(URL_LIST)) { // 判断该链接是否存在队列或者已采集的 set 中,不存在则添加到队列中 if (!visited.contains(request) && !taskQueue.contains(request)) { try { taskQueue.put(request); } catch (InterruptedException e) { e.printStackTrace(); } } } })); } catch (Exception e) { e.printStackTrace(); } } /** * 解析页面 * * @param url */ protected void processPage(String url) { try { Document doc = Jsoup.connect(url).get(); String title = doc.select("body > div.hp-wrap > div.voice-main > div.artical-title > h1").first().ownText(); System.out.println(Thread.currentThread().getName() + " 在 " + new Date() + " 采集了虎扑新闻 " + title); // 将采集完的 url 存入到已经采集的 set 中 visited.add(url); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { try { new ThreadCrawler("https://voice.hupu.com/nba", 5).run(); } catch (InterruptedException e) { e.printStackTrace(); } }}
分布式爬虫架构

浙公网安备 33010602011771号