在Java中获取多个网页的最快方法

7
我正在尝试编写一个快速的HTML爬虫,目前我只关注如何最大化吞吐量而不进行解析。我已经缓存了URL的IP地址:
public class Data {
    private static final ArrayList<String> sites = new ArrayList<String>();
    public static final ArrayList<URL> URL_LIST = new ArrayList<URL>();
    public static final ArrayList<InetAddress> ADDRESSES = new ArrayList<InetAddress>();

    static{
        /*
        add all the URLs to the sites array list
        */

        // Resolve the DNS prior to testing the throughput 
        for(int i = 0; i < sites.size(); i++){

            try {
                URL tmp = new URL(sites.get(i));
                InetAddress address = InetAddress.getByName(tmp.getHost());
                ADDRESSES.add(address);
                URL_LIST.add(new URL("http", address.getHostAddress(), tmp.getPort(), tmp.getFile()));
                System.out.println(tmp.getHost() + ": " + address.getHostAddress());
            } catch (MalformedURLException e) {
            } catch (UnknownHostException e) {
            }
        }
    }
}

我的下一步是通过从互联网获取100个URL,读取前64KB并移动到下一个URL来测试速度。我创建了一个FetchTaskConsumer的线程池,并尝试运行多个线程(在i7四核机器上运行了16到64个线程),每个消费者的外观如下:
public class FetchTaskConsumer implements Runnable{
    private final CountDownLatch latch;
    private final int[] urlIndexes;
    public FetchTaskConsumer (int[] urlIndexes, CountDownLatch latch){
        this.urlIndexes = urlIndexes;
        this.latch = latch;
    }

    @Override
    public void run() {

        URLConnection resource;
        InputStream is = null;
        for(int i = 0; i < urlIndexes.length; i++)
        {
            int numBytes = 0;
            try {                   
                resource = Data.URL_LIST.get(urlIndexes[i]).openConnection();

                resource.setRequestProperty("User-Agent", "Mozilla/5.0");

                is = resource.getInputStream();

                while(is.read()!=-1 && numBytes < 65536 )
                {
                    numBytes++;
                }

            } catch (IOException e) {
                System.out.println("Fetch Exception: " + e.getMessage());
            } finally {

                System.out.println(numBytes + " bytes for url index " + urlIndexes[i] + "; remaining: " + remaining.decrementAndGet());
                if(is!=null){
                    try {
                        is.close();
                    } catch (IOException e1) {/*eat it*/}
                }
            }
        }

        latch.countDown();
    }
}

我最多可以在30秒内浏览100个URL,但文献表明我应该能够每秒处理150个URL,需要注意的是,我可以访问Gigabit以太网,尽管我目前在家中使用20 Mbit连接运行测试...无论哪种情况,连接都没有真正被充分利用。

我已经尝试直接使用Socket连接,但速度更慢了!您有什么建议可以提高吞吐量吗?

P.S.
我有一个大约包含100万个流行URL的列表,因此如果100个URL不足以进行基准测试,我可以添加更多URL。

更新:
我所指的文献是与Najork Web爬虫相关的论文,Najork表示:

在17天内处理了8.91亿个URL,即每秒约606次下载,使用4台Compaq DS20E Alpha服务器,主存储器为4GB,磁盘空间为650GB,以及100 MBit/sec的以太网ISP带宽限制,因此实际上只有每秒约150页,而不是300页。我的电脑是Core i7,配备4GB RAM,远远达不到这个水平。并未看到特别说明他们使用了什么。

更新:
好吧,最终结果出来了!事实证明,100个URL对于基准测试来说有点低。我将其增加到1024个URL、64个线程,并设置每次获取的超时时间为2秒,我能够达到每秒21页(实际上我的连接速度约为10.5 Mbps,因此每秒21页*每页64KB大约为10.5 Mbps)。以下是抓取程序的外观:

public class FetchTask implements Runnable{
    private final int timeoutMS = 2000;
    private final CountDownLatch latch;
    private final int[] urlIndexes;
    public FetchTask(int[] urlIndexes, CountDownLatch latch){
        this.urlIndexes = urlIndexes;
        this.latch = latch;
    }

    @Override
    public void run() {

        URLConnection resource;
        InputStream is = null;
        for(int i = 0; i < urlIndexes.length; i++)
        {
            int numBytes = 0;
            try {                   
                resource = Data.URL_LIST.get(urlIndexes[i]).openConnection();

                resource.setConnectTimeout(timeoutMS);

                resource.setRequestProperty("User-Agent", "Mozilla/5.0");

                is = resource.getInputStream();

                while(is.read()!=-1 && numBytes < 65536 )
                {
                    numBytes++;
                }

            } catch (IOException e) {
                System.out.println("Fetch Exception: " + e.getMessage());
            } finally {

                System.out.println(numBytes + "," + urlIndexes[i] + "," + remaining.decrementAndGet());
                if(is!=null){
                    try {
                        is.close();
                    } catch (IOException e1) {/*eat it*/}
                }
            }
        }

        latch.countDown();
    }
}

为爬虫设置浏览器用户代理不是一个好的做法。 - Mat
文献?你是指Javadocs吗?我找不到任何关于URLConnection每秒300个URL的相关信息。 - Babar
URLConnection通常每500毫秒获取一个页面,Java在这方面相当慢。 - Thomas Jungblut
@Mat,有时我必须扮演浏览器的角色,因为网站可能根据页面是面向浏览器还是机器人来更改其内容。如果我正在进行内容爬取,那么我不想错过对用户有价值的内容。但我仍会遵守robots.txt规则。 - Kiril
@Lirik:那些这样做的网站都是有意为之。你很可能会违反他们的使用政策。 - Mat
显示剩余3条评论
2个回答

2

您确定您的计算正确吗?

每秒300个URL,每个URL读取64千字节

这需要:300 x 64 = 19,200千字节/秒

转换为比特:19,200千字节/秒=(8 * 19,200)千比特/秒

所以我们有:8*19,200 = 153,600千比特/秒

然后转换为Mb/s:153,600 / 1024 = 150兆比特/秒

...但是您只有一个20 Mb/s的通道。

然而,我想像很多您获取的URL大小都不到64Kb,因此吞吐量看起来比您的通道更快。您不慢,您很快!


即使以20 MB/s的速度(我的家庭连接速度),我至少应该能够以每秒40页的速度达到最大值...但我远远达不到这个速度(30秒内100个URL,大约每秒3页)!我也可以使用100 Mbps的连接,但在使用100 Mbps之前,如果我能将家庭连接速度提升到最大,我会很高兴。 - Kiril
抱歉,我更关注您的期望而非您的成就。我会看看能找出什么解决方案。 - Simon G.
这是一个不错的观点...即使它并没有真正回答我的问题 :). 现在我正在研究Nutch,以及他们如何在获取如此多的URL时避免遇到和我一样的问题。也许他们没有使用URLConnection... - Kiril

1

这一次我们聚焦在你的成就上。我尝试运行了一下你的代码,发现访问主要网站每秒钟可以得到大约三页内容。然而,当我访问我的私人Web服务器下载静态页面时,系统达到了最大值。

如今,在互联网上,一个主要网站生成一个页面通常需要超过一秒钟的时间。通过查看他们现在发送给我的数据包,我们可以看出这些页面是由多个TCP/IP数据包传输而来的。从英国到日本的www.yahoo.co.jp需要3秒才能下载完成,amazon.com需要2秒,但facebook.com只需要不到0.1秒。这是因为facebook.com的首页是静态的,而其他两个页面则是动态的。对于人类而言,关键因素在于第一个字节的时间,即浏览器开始执行操作的时间,而不是第65536个字节的时间。没有人会优化那部分内容 :-)

那么这对你意味着什么呢?既然你关注的是受欢迎的页面,我想你也会关注动态页面,但这些页面的传输速度不如静态页面。由于我查看的网站都是将页面分成多个数据包进行传输,这就意味着如果你同时获取多个页面,这些数据包就可能在以太网上相互碰撞。

当两个网站同时向您发送数据包时,数据包碰撞就会发生。在某个时刻,来自两个网站的输入必须被协调到传输到您计算机的单根电线上。当两个数据包同时到达时,将它们合并的路由器会拒绝两个数据包,并指示两个发送者在不同的短延迟后重新发送。这有效地减慢了两个网站的速度。

所以:

1)现在生成页面的速度不是很快。 2)以太网难以处理多个同时下载的文件。 3)静态网站(曾经更为常见)比动态网站更快,使用的数据包也更少。

所有这些意味着最大化您的连接速度真的很困难。

您可以尝试与我进行相同的测试,即上传1000个64Kb的文件,并查看您的代码可以多快地下载它们。对我而言,您的代码完全正常。


西蒙,我将批处理大小增加到300个URL,并在60秒内获取了它们...所以我的表现越来越好:每秒5个URL。我参考的文献也在读取前64KB。Nutch似乎也做得非常好:每秒40页。现在我将初始的URL批次设置为1000,以查看是否再次提高性能。 - Kiril
我现在添加了1024个URL,使用相同的代码,现在每秒可以达到21页(在约47秒内获取1024个)。 - Kiril

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接