代码之家  ›  专栏  ›  技术社区  ›  Gideon

提高crawer4j的性能

  •  4
  • Gideon  · 技术社区  · 8 年前

    我需要写一个网页刮擦器,它可以刮擦大约1M个网站,并将它们的标题、描述和关键字保存到一个大文件中(包含刮擦的URL和相关单词)。URL应该从一个大文件中提取。

    我在1M URL文件上运行了Crawler4j,并使用以下命令启动了网络爬虫程序: controller.start(MyCrawler.class, 20) .20是任意数。每个爬虫程序将结果字传递到阻塞队列中,以便单个线程将这些字和URL写入文件。为了不同步文件,我使用了1个writer线程。我将爬网深度设置为0(我只需要爬网我的种子列表)

    运行了一晚之后,我只下载了大约20万个URL。我使用有线连接在一台机器上运行刮刀。由于大多数URL来自不同的主机,我认为礼貌参数在这里没有任何重要性。

    编辑

    我尝试使用非阻塞启动启动Crawler4j,但它被阻塞了。我的Crawler4j版本是:4.2。这是我正在使用的代码:

    CrawlConfig config = new CrawlConfig();
    List<Header> headers = Arrays.asList(
            new BasicHeader("Accept", "text/html,text/xml"),
            new BasicHeader("Accept-Language", "en-gb, en-us, en-uk")
    );
    config.setDefaultHeaders(headers);
    config.setCrawlStorageFolder(crawlStorageFolder);
    config.setMaxDepthOfCrawling(0);
    config.setUserAgentString("testcrawl");
    config.setIncludeBinaryContentInCrawling(false);
    config.setPolitenessDelay(10);
    
    PageFetcher pageFetcher = new PageFetcher(config);
    RobotstxtConfig robotstxtConfig = new RobotstxtConfig();
    RobotstxtServer robotstxtServer = new RobotstxtServer(robotstxtConfig, pageFetcher);
    
    BlockingQueue<String> urlsQueue = new ArrayBlockingQueue<>(400);
    controller = new CrawlController(config, pageFetcher, robotstxtServer);
    
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    Runnable writerThread = new FileWriterThread(urlsQueue, crawlStorageFolder, outputFile);
    
    executorService.execute(writerThread);
    
    controller.startNonBlocking(() -> {
        return new MyCrawler(urlsQueue);
    }, 4);
    
    File file = new File(urlsFileName);
    try (BufferedReader br = new BufferedReader(new FileReader(file))) {
        String url;
        while ((url = br.readLine()) != null) {
            controller.addSeed(url);
        }
    }
    

    编辑1-这是MyCrawler的代码

    public class MyCrawler extends WebCrawler {
        private final static Pattern FILTERS = Pattern.compile(".*(\\.(css|js|gif|jpg|png|mp3|mp3|zip|gz))$");
        public static final String DELIMETER = "||||";
        private final StringBuilder buffer = new StringBuilder();
        private final BlockingQueue<String> urlsQueue;
    
        public MyCrawler(BlockingQueue<String> urlsQueue) {
            this.urlsQueue = urlsQueue;
        }
    
        @Override
        public boolean shouldVisit(Page referringPage, WebURL url) {
            String href = url.getURL().toLowerCase();
            return !FILTERS.matcher(href).matches();
        }
    
        @Override
        public void visit(Page page) {
            String url = page.getWebURL().getURL();
    
            if (page.getParseData() instanceof HtmlParseData) {
                HtmlParseData parseData = (HtmlParseData) page.getParseData();
                String html = parseData.getHtml();
                String title = parseData.getTitle();
    
                Document document = Jsoup.parse(html);
                buffer.append(url.replaceAll("[\n\r]", "")).append(DELIMETER).append(title);
                Elements descriptions = document.select("meta[name=description]");
                for (Element description : descriptions) {
                    if (description.hasAttr("content"))
                        buffer.append(description.attr("content").replaceAll("[\n\r]", ""));
                }
    
                Elements elements = document.select("meta[name=keywords]");
                for (Element element : elements) {
                    String keywords = element.attr("content").replaceAll("[\n\r]", "");
                    buffer.append(keywords);
                }
                buffer.append("\n");
                String urlContent = buffer.toString();
                buffer.setLength(0);
                urlsQueue.add(urlContent);
            }
        }
    
        private boolean isSuccessful(int statusCode) {
            return 200 <= statusCode && statusCode < 400;
        }
    }
    

    所以我有两个问题:

    1. 有人能提出任何其他方法来缩短这个过程的时间吗?也许可以通过某种方式调整爬虫线程的数量?也许还有其他优化?我更喜欢一个不需要多台机器的解决方案,但如果你认为这是扮演角色的唯一方式,有人能建议怎么做吗?也许是一个代码示例?
    2. 有什么方法可以让爬虫程序开始处理某些URL,并在爬虫过程中不断添加更多URL?我看过 crawler.startNonBlocking 但似乎效果不太好

    提前谢谢

    1 回复  |  直到 8 年前
        1
  •  4
  •   rzo1    8 年前

    crawler4j 默认设计为在一台机器上运行。来自的字段 web-crawling 我们知道,网络爬虫的性能主要取决于以下四种资源:

    • 磁盘
    • 中央处理器
    • 带宽
    • (闸板)

    定义最佳线程数取决于硬件设置。因此,更多的机器将产生更高的吞吐量。下一个硬限制是网络带宽。如果你没有通过高速互联网连接,这将是你的方法的瓶颈。

    此外 履带4j 默认情况下不会加载这么大的种子文件。这是因为 履带4j 重新审视爬虫式的礼貌。这意味着,在爬网开始之前,将检查每个种子点的 robots.txt ,这可能需要相当长的时间。

    如果爬网是在非阻塞模式下启动的,则在爬网开始后添加种子是可能的,并且应该会起作用。然而,处理URL可能需要一段时间。

    对于多机设置,您可以查看 Apache Nutch 然而,Nutch有点难学。

    编辑:

    在重现您的设置之后,我能够以动态方式回答您关于添加种子页面的问题。

    以这种方式启动爬虫

    controller.startNonBlocking(() -> {
        return new MyCrawler(urlsQueue);
    }, 4);
    

    将调用 run() 每个爬虫线程的方法。研究这个方法,我们发现一个名为 frontier.getNextURLs(50, assignedURLs); ,负责从前沿获取看不见的URL以便处理它们。在这种方法中,我们发现一个所谓的 waitingList ,这会导致线程等待。自从 notifyAll 从未在上调用 等待列表 在控制器关闭之前,线程永远不会重新调度新的URL。

    要解决这个问题,您有两种可能的解决方案:

    1. 只需为每个线程添加至少一个URL作为种子点。不会出现死锁情况。在非阻塞模式下启动线程后,您可以随意添加种子。

      controller.addSeed("https://www.google.de");
      
      controller.startNonBlocking(() -> {
          return new MyCrawler(urlsQueue);
      }, 4);
      
      controller.addSeed("https://www.google.de/test");
      
      controller.waitUntilFinish();
      
    2. 选择Github项目的一个分支,并修改 Frontier.java 因此 waitingList.notifyAll() 方法可以从 CrawlController 在动态添加种子页面之后。