如何使用Java从互联网下载并保存文件?

468

我需要抓取并保存到目录的是一个在线文件(例如http://www.example.com/information.asp)。我知道有几种逐行读取在线文件(URL)的方法,但是否有一种使用Java仅下载和保存文件的方式?


2
如何从URL对象创建文件对象 - Adriano
https://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#copy(java.io.InputStream,%20java.nio.file.Path,%20java.nio.file.CopyOption...) - Grigoriev Nick
24个回答

585

尝试使用Java NIO

URL website = new URL("http://www.website.com/information.asp");
ReadableByteChannel rbc = Channels.newChannel(website.openStream());
FileOutputStream fos = new FileOutputStream("information.html");
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);

transferFrom()比简单读取源通道并将数据写入此通道的循环要更高效,因为许多操作系统可以直接将字节从源通道传输到文件系统缓存而不必复制它们。

了解更多信息,请点击这里

注意:transferFrom中的第三个参数是最大传输字节数。 Integer.MAX_VALUE最多会传输2^31个字节,Long.MAX_VALUE允许最多传输2^63个字节(大于任何现有的文件)。


26
使用Java 7中的try-with-resource关闭所有三个资源:try (InputStream inputStream = website.openStream(); ReadableByteChannel readableByteChannel = Channels.newChannel(inputStream); FileOutputStream fileOutputStream = new FileOutputStream(outputFileName)) { fileOutputStream.getChannel().transferFrom(readableByteChannel, 0, 1 << 24); } - mazatwork
88
这将仅下载文件的前16MB:https://dev59.com/questions/6Woy5IYBdhLWcg3wq_sk。 - Ben McCann
34
如果我需要的容量超过8388608 TB怎么办? - Cruncher
24
一次调用不足够。transferFrom()没有指定完成整个转账需要一次调用。这就是它返回计数的原因。你需要循环执行。 - user207421
14
为什么这个答案被接受了?URL::openStream()只返回一个常规的流,这意味着整个流量仍然通过Java byte[]数组复制,而不是保留在本地缓冲区中。只有fos.getChannel()才是真正的本地通道,因此开销仍然存在。在这种情况下,使用NIO没有任何好处。正如EJP和Ben MacCann正确指出的那样,除了出现故障。 - Ext3h
显示剩余20条评论

537

使用Apache Commons IO 库。只需要一行代码:

FileUtils.copyURLToFile(URL, File)

30
好的!正是我正在寻找的!我知道 Apache 库已经涵盖了这个。顺便说一句,建议使用带有超时参数的重载版本! - Hendy Irawan
9
使用那个重载版本时,请记住超时时间是以毫秒为单位指定的,而不是秒。 - László van den Hoek
5
注意,带有超时参数的 copyURLToFile 方法只在 Commons IO 库的 2.0 版本及以上可用。请参阅 Java 文档 - Stanley
8
如果需要在请求中添加基本认证头,有什么解决方法吗?是否有变通的办法? - damian
3
在我看来,原生的Java解决方案比使用外部库更好。 - ndm13
显示剩余7条评论

130

更简单的非阻塞I/O使用方法:

URL website = new URL("http://www.website.com/information.asp");
try (InputStream in = website.openStream()) {
    Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
}

6
如果出现像“302 Found”这样的重定向,那么不幸的是下载将会默默失败(下载0字节)。 - Alexander K
2
@AlexanderK 但是你为什么要盲目地下载这样的资源呢? - xuesheng
5
虽然这是一个优雅的解决方案,但在幕后,这种方法可能会悄悄地出现问题。Files.copy(InputStream, Paths, FileOption)将复制过程委托给Files.copy(InputStream, OutputStream)。后者不检查流的末尾(-1),而是检查是否读取了字节(0)。这意味着,如果您的网络稍微暂停了一下,就可能读取0字节并结束复制过程,即使流还没有被操作系统完全下载下来。 - Miere
6
除非您提供了零长度的缓冲区或计数,否则InputStream.read()不可能返回零。它会一直阻塞,直到至少传输一个字节,或者流结束或出现错误。您关于Files.copy()内部工作原理的说法是毫无根据的。 - user207421
3
我有一个单元测试,读取一个大小为2.6TiB的二进制文件。使用Files.copy在我的HDD存储服务器(XFS)上总是失败,但在我的SSH服务器上只有几次错误。查看JDK 8中File.copy的代码,我发现它检查'> 0'才能离开'while'循环。我刚刚使用了完全相同的代码和-1,两个单元测试都没有再停止过。由于InputStream可以表示网络和本地文件描述符,并且两个IO操作都受到操作系统上下文切换的影响,我无法看出我的说法是毫无根据的。有人可能会声称这只是幸运运行成功,但我再也没有遇到过问题。 - Miere
显示剩余5条评论

88
public void saveUrl(final String filename, final String urlString)
        throws MalformedURLException, IOException {
    BufferedInputStream in = null;
    FileOutputStream fout = null;
    try {
        in = new BufferedInputStream(new URL(urlString).openStream());
        fout = new FileOutputStream(filename);

        final byte data[] = new byte[1024];
        int count;
        while ((count = in.read(data, 0, 1024)) != -1) {
            fout.write(data, 0, count);
        }
    } finally {
        if (in != null) {
            in.close();
        }
        if (fout != null) {
            fout.close();
        }
    }
}

你需要处理异常,可能是在这个方法之外。


6
如何下载得更快?像下载加速器一样? - digz6666
12
如果 in.close 抛出异常,fout.close 将不会被调用。 - Beryllium
1
@ComFreek,那是不正确的。使用BufferedInputStream对套接字超时没有任何影响。我已经在您引用的“背景细节”评论中驳斥了这一点作为“城市传说”。早在三年前。 - user207421
我唯一对这个答案(还有其他答案)的反对是,调用者无法区分事件“未找到”和某些连接错误(在此情况下,您可能希望重试)。 - leonbloy
MalformedURLException extends IOException, so you only have to throw the IOException - Alex Jone
显示剩余2条评论

33

这是一个简洁易读的解决方案,仅使用JDK并正确关闭资源:

static long download(String url, String fileName) throws IOException {
    try (InputStream in = URI.create(url).toURL().openStream()) {
        return Files.copy(in, Paths.get(fileName));
    }
}

只需要两行代码,且不需要任何依赖。

这里有一个完整的文件下载示例程序,包括输出、错误检查和命令行参数检查:

package so.downloader;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Application {
    public static void main(String[] args) throws IOException {
        if (2 != args.length) {
            System.out.println("USAGE: java -jar so-downloader.jar <source-URL> <target-filename>");
            System.exit(1);
        }

        String sourceUrl = args[0];
        String targetFilename = args[1];

        long bytesDownloaded = download(sourceUrl, targetFilename);

        System.out.println(String.format("Downloaded %d bytes from %s to %s.", bytesDownloaded, sourceUrl, targetFilename));
    }

    static long download(String url, String fileName) throws IOException {
        try (InputStream in = URI.create(url).toURL().openStream()) {
            return Files.copy(in, Paths.get(fileName));
        }
    }    
}

正如在so-downloader存储库的README中所述:

要运行文件下载程序:

java -jar so-downloader.jar <source-URL> <target-filename>
例如:
java -jar so-downloader.jar https://github.com/JanStureNielsen/so-downloader/archive/main.zip so-downloader-source.zip

这对我很有效,谢谢。我有一个PDF文件的URL,它可以下载完全相同的PDF文件,而不需要使用任何PDF库。非常感激。 - Phil

23

下载文件需要先读取它。不管怎样,您都必须以某种方式查看文件。而不是逐行读取,您可以从流中按字节读取:

BufferedInputStream in = new BufferedInputStream(new URL("http://www.website.com/information.asp").openStream())
byte data[] = new byte[1024];
int count;
while((count = in.read(data, 0, 1024)) != -1)
{
    out.write(data, 0, count);
}

19

如果使用Java 7及以上版本,可以使用以下方法从互联网下载文件并将其保存到某个目录中:

private static Path download(String sourceURL, String targetDirectory) throws IOException
{
    URL url = new URL(sourceURL);
    String fileName = sourceURL.substring(sourceURL.lastIndexOf('/') + 1, sourceURL.length());
    Path targetPath = new File(targetDirectory + File.separator + fileName).toPath();
    Files.copy(url.openStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);

    return targetPath;
}

文档在这里


我希望有一个 StandardCopyOption 来重命名文件而不是替换现有文件。 - Sridhar Sarnobat

17

这个答案与被选中的答案几乎完全相同,但是它有两个增强功能:它是一种方法,并且关闭了FileOutputStream对象:

    public static void downloadFileFromURL(String urlString, File destination) {
        try {
            URL website = new URL(urlString);
            ReadableByteChannel rbc;
            rbc = Channels.newChannel(website.openStream());
            FileOutputStream fos = new FileOutputStream(destination);
            fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
            fos.close();
            rbc.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

3
单个调用是不足够的。transferFrom() 没有规定要在单个调用中完成整个转移。这就是为什么它会返回一个计数值。你需要循环操作。 - user207421
1
你的代码如果出现异常,就不会关闭任何东西。 - user207421

10
import java.io.*;
import java.net.*;

public class filedown {
    public static void download(String address, String localFileName) {
        OutputStream out = null;
        URLConnection conn = null;
        InputStream in = null;

        try {
            URL url = new URL(address);
            out = new BufferedOutputStream(new FileOutputStream(localFileName));
            conn = url.openConnection();
            in = conn.getInputStream();
            byte[] buffer = new byte[1024];

            int numRead;
            long numWritten = 0;

            while ((numRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, numRead);
                numWritten += numRead;
            }

            System.out.println(localFileName + "\t" + numWritten);
        } 
        catch (Exception exception) { 
            exception.printStackTrace();
        } 
        finally {
            try {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
            } 
            catch (IOException ioe) {
            }
        }
    }

    public static void download(String address) {
        int lastSlashIndex = address.lastIndexOf('/');
        if (lastSlashIndex >= 0 &&
        lastSlashIndex < address.length() - 1) {
            download(address, (new URL(address)).getFile());
        } 
        else {
            System.err.println("Could not figure out local file name for "+address);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < args.length; i++) {
            download(args[i]);
        }
    }
}

6
如果 in.close 抛出异常,则不会调用 out.close - Beryllium

9

2
同时,commons-io 是一个很棒的库。 - dfa

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