Как скачать и сохранить файл из Интернета с помощью Java?


425

Есть онлайн-файл (например, http://www.example.com/information.asp), который мне нужно взять и сохранить в каталоге. Я знаю, что есть несколько способов построчного захвата и чтения онлайн-файлов (URL), но есть ли способ просто загрузить и сохранить файл с помощью Java?


Ответы:


560

Дайте 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 байта (больше, чем любой существующий файл).


22
Закройте все три с помощью Java 7 try-with-resource: try (InputStream inputStream = website.openStream (); ReadableByteChannel readableByteChannel = Channels.newChannel (inputStream); FileOutputStream fileOutputStream = новый FileOutputStream (outputFileName)) {fileOutputFream (). (readableByteChannel, 0, 1 << 24); }
mazatwork

80
При этом будут загружены только первые 16 МБ файла: stackoverflow.com/questions/8405062/downloading-files-with-java
Бен Макканн

31
@ kirdie а если я хочу больше чем 8388608туберкулез?
Cruncher

22
Один звонок не подходит. transferFrom()isnt 'указан для завершения всей передачи за один вызов. Вот почему он возвращает счет. Вы должны зациклить.
маркиз Лорн

11
Почему этот ответ даже был принят? URL::openStream()возвращает обычный поток, то есть весь трафик все еще копируется через массивы Java byte [], а не остается в собственных буферах. Только fos.getChannel()на самом деле является родным каналом, поэтому накладные расходы остаются в полном объеме. Это нулевая выгода от использования NIO в этом случае. Помимо того, что EJP и Бен Макканн правильно заметили, что они сломаны.
Ext3h

496

Используйте apache commons-io , всего один строчный код:

FileUtils.copyURLToFile(URL, File)

22
Ницца! Как раз то, что я ищу! Я знал, что библиотеки Apache уже охватят это. Кстати, рекомендуется использовать перегруженную версию с параметрами тайм-аута!
Хенди Ираван

6
... и при использовании этой перегруженной версии помните, что время ожидания указывается в миллисекундах, а не секундах.
Ласло ван ден Хук

5
Обратите внимание, что copyURLToFileпараметр timeout доступен только с версии 2.0 библиотеки Commons IO. Смотрите документацию по Java
Стэнли

6
Что, если к запросу необходимо добавить заголовок базовой аутентификации? есть ли обходной путь?
Дамиан

3
Хотя это «короткий», на самом деле это очень медленно.
Meinkraft

123

Упрощенное использование nio:

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

5
К сожалению, это происходит молча (загрузка 0 байт) в случае перенаправления, такого как «302 Найдено».
Александр К

1
@AlexanderK Но зачем вам вслепую скачивать такой ресурс?
xuesheng

5
Несмотря на то, что это элегантное решение, за кулисами такой подход мог бы молчаливо предать вас. Files.copy (InputStream, Paths, FileOption) делегирует процесс копирования в Files.copy (InputStream, OutputStream). Этот последний метод не проверяет конец потока (-1), но проверяет отсутствие чтения байта (0). Это означает, что если в вашей сети была небольшая пауза, она могла бы прочитать 0 байтов и завершить процесс копирования, даже если поток не завершен для загрузки ОС.
Miere

6
@Miere Невозможно InputStream.read()вернуть ноль, если вы не указали буфер нулевой длины или счетчик, «небольшую паузу» или иное. Он будет блокироваться до тех пор, пока не будет передан хотя бы один байт или пока не закончится поток или не произойдет ошибка. Ваше заявление о внутренностях Files.copy()безосновательно.
Маркиз Лорн

3
У меня есть модульный тест, который читает двоичный файл с 2,6 ТБ. Использование Files.copy всегда приводит к сбою на моем сервере хранения HDD (XFS), но это происходит только несколько раз по моему SSH. Глядя на JDK 8, код File.copy, который я обнаружил, проверяет, чтобы '> 0' покинул цикл while. Я просто скопировал один и тот же код с -1, и оба модульных теста больше никогда не останавливались. Как только InputStream может представлять дескрипторы сетевых и локальных файлов, и обе операции ввода-вывода подлежат переключению контекста ОС, я не могу понять, почему мое утверждение является необоснованным. Кто-то может утверждать, что это работает удачей, но больше не доставляло головной боли.
Miere

85
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

11
Если in.closeвыдает исключение, fout.closeне вызывается.
Бериллий

1
@ComFreek Это просто неправда. Использование BufferedInputStreamимеет точно нулевой эффект на время ожидания сокета. Я уже опроверг это как «городской миф» в моих комментариях к «второстепенным деталям», которые вы цитировали. Тремя годами ранее
маркиз Лорн

@EJP Спасибо за исправление! Я удалил свой комментарий (для архива: я связался с этим ответом, заявив, что BufferedInputStream«может вызвать непредсказуемые сбои»).
ComFreek,

+1 Мое единственное возражение против этого ответа (и других здесь) состоит в том, что вызывающий абонент не может отличить событие «не найдено» от некоторой ошибки подключения (при которой вы можете повторить попытку).
leonbloy

24

Это старый вопрос, но вот краткое, читаемое, JDK-решение с правильно закрытыми ресурсами:

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

Две строки кода и никаких зависимостей.


Для людей, которые задаются вопросом, вот импорт, необходимый для этого фрагмента кода:import java.io.InputStream; import java.net.URI; import java.nio.file.Files; import java.nio.file.Paths;
BelovedFool

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);
    }

17

При использовании 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;
}

Документация здесь .


16

Этот ответ почти так же, как выбранный ответ, но с двумя улучшениями: это метод, и он закрывает объект 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();
        }
    }

2
Один звонок не подходит. transferFrom()isnt 'указан для завершения всей передачи за один вызов. Вот почему он возвращает счет. Вы должны зациклить.
маркиз Лорн

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]);
        }
    }
}

5
Если in.closeвыдает исключение, out.closeне вызывается.
Бериллий

8

Лично я обнаружил, что Apache HttpClient более чем способен на все, что мне нужно сделать в связи с этим. Вот отличное руководство по использованию HttpClient


2
также Обще-Ио большая библиотека
DFA

6

Это еще один вариант java7, основанный на ответе Брайана Риска с использованием оператора try-with:

public static void downloadFileFromURL(String urlString, File destination) throws Throwable {

      URL website = new URL(urlString);
      try(
              ReadableByteChannel rbc = Channels.newChannel(website.openStream());
              FileOutputStream fos = new FileOutputStream(destination);  
              ){
          fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
      }

  }

Один звонок не подходит. transferFrom()isnt 'указан для завершения всей передачи за один вызов. Вот почему он возвращает счет. Вы должны зациклить.
маркиз Лорн

Я не знаю, почему вы задаете мне этот глупый вопрос. Это не имеет ничего общего с тем, что я сказал, и я действительно отказываюсь вставлять слова в мой рот.
маркиз Лорн

2

Можно загрузить файл с помощью Apache HttpComponentsвместо Commons-IO. Этот код позволяет загружать файл в Java в соответствии с его URL-адресом и сохранять его в определенном месте назначения.

public static boolean saveFile(URL fileURL, String fileSavePath) {

    boolean isSucceed = true;

    CloseableHttpClient httpClient = HttpClients.createDefault();

    HttpGet httpGet = new HttpGet(fileURL.toString());
    httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0");
    httpGet.addHeader("Referer", "https://www.google.com");

    try {
        CloseableHttpResponse httpResponse = httpClient.execute(httpGet);
        HttpEntity fileEntity = httpResponse.getEntity();

        if (fileEntity != null) {
            FileUtils.copyInputStreamToFile(fileEntity.getContent(), new File(fileSavePath));
        }

    } catch (IOException e) {
        isSucceed = false;
    }

    httpGet.releaseConnection();

    return isSucceed;
}

В отличие от одной строки кода:

FileUtils.copyURLToFile(fileURL, new File(fileSavePath),
                        URLS_FETCH_TIMEOUT, URLS_FETCH_TIMEOUT);

этот код даст вам больший контроль над процессом и позволит вам указать не только тайм-ауты, но User-Agentи Refererзначения, которые важны для многих веб-сайтов.


2

Здесь есть много элегантных и эффективных ответов. Но краткость может заставить нас потерять некоторую полезную информацию. В частности, часто не требуется рассматривать ошибку соединения как исключение , и можно по-разному относиться к каким-либо сетевым ошибкам, например, чтобы решить, следует ли повторить попытку загрузки.

Вот метод, который не выбрасывает исключения для сетевых ошибок (только для действительно исключительных проблем, таких как неправильный URL или проблемы с записью в файл)

/**
 * Downloads from a (http/https) URL and saves to a file. 
 * Does not consider a connection error an Exception. Instead it returns:
 *  
 *    0=ok  
 *    1=connection interrupted, timeout (but something was read)
 *    2=not found (FileNotFoundException) (404) 
 *    3=server error (500...) 
 *    4=could not connect: connection timeout (no internet?) java.net.SocketTimeoutException
 *    5=could not connect: (server down?) java.net.ConnectException
 *    6=could not resolve host (bad host, or no internet - no dns)
 * 
 * @param file File to write. Parent directory will be created if necessary
 * @param url  http/https url to connect
 * @param secsConnectTimeout Seconds to wait for connection establishment
 * @param secsReadTimeout Read timeout in seconds - trasmission will abort if it freezes more than this 
 * @return See above
 * @throws IOException Only if URL is malformed or if could not create the file
 */
public static int saveUrl(final Path file, final URL url, 
  int secsConnectTimeout, int secsReadTimeout) throws IOException {
    Files.createDirectories(file.getParent()); // make sure parent dir exists , this can throw exception
    URLConnection conn = url.openConnection(); // can throw exception if bad url
    if( secsConnectTimeout > 0 ) conn.setConnectTimeout(secsConnectTimeout * 1000);
    if( secsReadTimeout > 0 ) conn.setReadTimeout(secsReadTimeout * 1000);
    int ret = 0;
    boolean somethingRead = false;
    try (InputStream is = conn.getInputStream()) {
        try (BufferedInputStream in = new BufferedInputStream(is); OutputStream fout = Files
                .newOutputStream(file)) {
            final byte data[] = new byte[8192];
            int count;
            while((count = in.read(data)) > 0) {
                somethingRead = true;
                fout.write(data, 0, count);
            }
        }
    } catch(java.io.IOException e) { 
        int httpcode = 999;
        try {
            httpcode = ((HttpURLConnection) conn).getResponseCode();
        } catch(Exception ee) {}
        if( somethingRead && e instanceof java.net.SocketTimeoutException ) ret = 1;
        else if( e instanceof FileNotFoundException && httpcode >= 400 && httpcode < 500 ) ret = 2; 
        else if( httpcode >= 400 && httpcode < 600 ) ret = 3; 
        else if( e instanceof java.net.SocketTimeoutException ) ret = 4; 
        else if( e instanceof java.net.ConnectException ) ret = 5; 
        else if( e instanceof java.net.UnknownHostException ) ret = 6;  
        else throw e;
    }
    return ret;
}

2

Ниже приведен пример кода для загрузки фильма из Интернета с использованием кода Java:

URL url = new 
URL("http://103.66.178.220/ftp/HDD2/Hindi%20Movies/2018/Hichki%202018.mkv");
    BufferedInputStream bufferedInputStream = new  BufferedInputStream(url.openStream());
    FileOutputStream stream = new FileOutputStream("/home/sachin/Desktop/test.mkv");


    int count=0;
    byte[] b1 = new byte[100];

    while((count = bufferedInputStream.read(b1)) != -1) {
        System.out.println("b1:"+b1+">>"+count+ ">> KB downloaded:"+new File("/home/sachin/Desktop/test.mkv").length()/1024);
        stream.write(b1, 0, count);
    }

Как правило, ответы гораздо полезнее, если они включают в себя объяснение того, для чего предназначен код, и почему это решает проблему, не представляя других.
Тим Дикманн

1

Существует проблема с простым использованием:

org.apache.commons.io.FileUtils.copyURLToFile(URL, File) 

если вам нужно скачать и сохранить очень большие файлы, или вообще, если вам нужны автоматические повторные попытки в случае разрыва соединения.

В таких случаях я предлагаю использовать Apache HttpClient вместе с org.apache.commons.io.FileUtils. Например:

GetMethod method = new GetMethod(resource_url);
try {
    int statusCode = client.executeMethod(method);
    if (statusCode != HttpStatus.SC_OK) {
        logger.error("Get method failed: " + method.getStatusLine());
    }       
    org.apache.commons.io.FileUtils.copyInputStreamToFile(
        method.getResponseBodyAsStream(), new File(resource_file));
    } catch (HttpException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
    method.releaseConnection();
}

1

Подводить итоги (и как-то полировать и обновлять) предыдущие ответы. Три следующих метода практически эквивалентны. (Я добавил явные тайм-ауты, потому что я думаю, что они просто необходимы, никто не хочет, чтобы загрузка зависла навсегда, когда соединение потеряно.)

public static void saveUrl1(final Path file, final URL url,
   int secsConnectTimeout, int secsReadTimeout)) 
    throws MalformedURLException, IOException {
    // Files.createDirectories(file.getParent()); // optional, make sure parent dir exists
    try (BufferedInputStream in = new BufferedInputStream(
       streamFromUrl(url, secsConnectTimeout,secsReadTimeout)  );
        OutputStream fout = Files.newOutputStream(file)) {
        final byte data[] = new byte[8192];
        int count;
        while((count = in.read(data)) > 0)
            fout.write(data, 0, count);
    }
}

public static void saveUrl2(final Path file, final URL url,
   int secsConnectTimeout, int secsReadTimeout))  
    throws MalformedURLException, IOException {
    // Files.createDirectories(file.getParent()); // optional, make sure parent dir exists
    try (ReadableByteChannel rbc = Channels.newChannel(
      streamFromUrl(url, secsConnectTimeout,secsReadTimeout) 
        );
        FileChannel channel = FileChannel.open(file,
             StandardOpenOption.CREATE, 
             StandardOpenOption.TRUNCATE_EXISTING,
             StandardOpenOption.WRITE) 
        ) {
        channel.transferFrom(rbc, 0, Long.MAX_VALUE);
    }
}

public static void saveUrl3(final Path file, final URL url, 
   int secsConnectTimeout, int secsReadTimeout))  
    throws MalformedURLException, IOException {
    // Files.createDirectories(file.getParent()); // optional, make sure parent dir exists
    try (InputStream in = streamFromUrl(url, secsConnectTimeout,secsReadTimeout) ) {
        Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING);
    }
}

public static InputStream streamFromUrl(URL url,int secsConnectTimeout,int secsReadTimeout) throws IOException {
    URLConnection conn = url.openConnection();
    if(secsConnectTimeout>0) conn.setConnectTimeout(secsConnectTimeout*1000);
    if(secsReadTimeout>0) conn.setReadTimeout(secsReadTimeout*1000);
    return conn.getInputStream();
}

Я не нахожу существенных различий, мне все кажется правильным. Они безопасны и эффективны. (Различия в скорости кажутся едва уместными - я записываю 180Mb с локального сервера на SSD-диск во времена, которые колеблются от 1,2 до 1,5 сегментов). Они не требуют внешних библиотек. Все работают с произвольными размерами и (по моему опыту) перенаправления HTTP.

Кроме того, все выбрасывают, FileNotFoundExceptionесли ресурс не найден (обычно ошибка 404) и java.net.UnknownHostExceptionесли разрешение DNS не удалось; Другие IOException соответствуют ошибкам при передаче.

(Помечено как вики сообщества, не стесняйтесь добавлять информацию или исправления)


1

В библиотеке underscore-java есть метод U.fetch (url) .

pom.xml:

  <groupId>com.github.javadev</groupId>
  <artifactId>underscore</artifactId>
  <version>1.45</version>

Пример кода:

import com.github.underscore.lodash.U;

public class Download {
    public static void main(String ... args) {
        String text = U.fetch("https://stackoverflow.com/questions"
        + "/921262/how-to-download-and-save-a-file-from-internet-using-java").text();
    }
}

Насколько полезен этот ответ, когда ссылка становится недействительной? Пожалуйста, посмотрите как ответить
JimHawkins

Ваш код не будет компилироваться. Вопрос, спросите решение в Java, но ваш ответ похож наJavaScript
talex

@talex Я добавил раздел pom.xml и улучшил пример кода.
Валентин Колесников

0
public class DownloadManager {

    static String urls = "[WEBSITE NAME]";

    public static void main(String[] args) throws IOException{
        URL url = verify(urls);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        InputStream in = null;
        String filename = url.getFile();
        filename = filename.substring(filename.lastIndexOf('/') + 1);
        FileOutputStream out = new FileOutputStream("C:\\Java2_programiranje/Network/DownloadTest1/Project/Output" + File.separator + filename);
        in = connection.getInputStream();
        int read = -1;
        byte[] buffer = new byte[4096];
        while((read = in.read(buffer)) != -1){
            out.write(buffer, 0, read);
            System.out.println("[SYSTEM/INFO]: Downloading file...");
        }
        in.close();
        out.close();
        System.out.println("[SYSTEM/INFO]: File Downloaded!");
    }
    private static URL verify(String url){
        if(!url.toLowerCase().startsWith("http://")) {
            return null;
        }
        URL verifyUrl = null;

        try{
            verifyUrl = new URL(url);
        }catch(Exception e){
            e.printStackTrace();
        }
        return verifyUrl;
    }
}

Вы можете улучшить свой ответ, предоставив информацию о том, как работает ваш код, а не просто выгрузив его.
Матей Кормут

0

Вы можете сделать это в 1 строку, используя netloader for Java :

new NetFile(new File("my/zips/1.zip"), "https://example.com/example.zip", -1).load(); //returns true if succeed, otherwise false.

0

Если вы находитесь за прокси, вы можете установить прокси в java программе, как показано ниже:

        Properties systemSettings = System.getProperties();
        systemSettings.put("proxySet", "true");
        systemSettings.put("https.proxyHost", "https proxy of your org");
        systemSettings.put("https.proxyPort", "8080");

Если вы не используете прокси-сервер, не включайте приведенные выше строки в ваш код. Полный рабочий код для загрузки файла, когда вы находитесь за прокси.

public static void main(String[] args) throws IOException {
        String url="https://raw.githubusercontent.com/bpjoshi/fxservice/master/src/test/java/com/bpjoshi/fxservice/api/TradeControllerTest.java";
        OutputStream outStream=null;
        URLConnection connection=null;
        InputStream is=null;
        File targetFile=null;
        URL server=null;
        //Setting up proxies
        Properties systemSettings = System.getProperties();
            systemSettings.put("proxySet", "true");
            systemSettings.put("https.proxyHost", "https proxy of my organisation");
            systemSettings.put("https.proxyPort", "8080");
            //The same way we could also set proxy for http
            System.setProperty("java.net.useSystemProxies", "true");
            //code to fetch file
        try {
            server=new URL(url);
            connection = server.openConnection();
            is = connection.getInputStream();
            byte[] buffer = new byte[is.available()];
            is.read(buffer);

                targetFile = new File("src/main/resources/targetFile.java");
                outStream = new FileOutputStream(targetFile);
                outStream.write(buffer);
        } catch (MalformedURLException e) {
            System.out.println("THE URL IS NOT CORRECT ");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.println("Io exception");
            e.printStackTrace();
        }
        finally{
            if(outStream!=null) outStream.close();
        }
    }

0

1-й метод с использованием нового канала

ReadableByteChannel aq = Channels.newChannel(new url("https//asd/abc.txt").openStream());
FileOutputStream fileOS = new FileOutputStream("C:Users/local/abc.txt")
FileChannel writech = fileOS.getChannel();

2-й метод с использованием FileUtils

FileUtils.copyURLToFile(new url("https//asd/abc.txt",new local file on system("C":/Users/system/abc.txt"));

3-й метод с использованием

InputStream xy = new ("https//asd/abc.txt").openStream();

Вот как мы можем загрузить файл, используя базовый код Java и другие сторонние библиотеки. Это только для справки. Пожалуйста, Google с вышеупомянутыми ключевыми словами, чтобы получить подробную информацию и другие варианты.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.