➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
Spring Cloud Config——使用 RDBMS(JDBC)存放配置Spring Webflux(一)

批量下載檔案

Table of contents

1 情境

有一個網頁,上面有好多一般大小既檔案畀人下載,我既目標就係要自動咁一下子下載曬所有檔案。

2 分析

呢個網站:
  • 所有檔案既下載連結都出現喺個網頁上面。
  • 裝住檔案下載連結既係 <table>
  • 因為所有下載連結喺曬第 2 個 table column,所以我地可以用 td:nth-of-type(2) 黎 select 曬果個 column 既所有 rows 既 <td> cells。
  • 第一個 table row 係 Parent Directory,並唔係檔案下載連結,但冇任何 CSS class 可以分辨到佢同其他 table rows。
    • 因為係目錄既關係,佢既 <a> child element 既 href attribute 前面有個 /,同其他 table rows 既 <a> child elements 唔同,所以我地可以用 CSS attribute selector 黎 exclude 走呢類既 table row(s)。
  • 我地可以用以下既 CSS selector 將所有檔案下載連結搵曬出黎:
    • table tbody tr > td:nth-of-type(2) a:not([href^='/'])
      
      • 可以見到有 163<a> 結果。

2.1 假設、限制

  • 為左簡單起見,我會假設個網站前面冇類似 Cloudflare 既 rate limiting 或者 request throttling 既安全功能。
    • 否則 Cloudflare 可能會察覺到我地用左自動化程式而 block 左我地。

3 動手寫

3.1 cURL

我地可以喺個網頁執行 JavaScript,佢會喺 console 度 print 一堆 curl commands 出黎:
console.log( [...document.querySelectorAll("table tbody tr > td:nth-of-type(2) a:not([href^='/'])")] .map((e) => `start /b curl --remote-time --output-dir output_curl -o \"${e.innerText}\" \"${e.href}\"`) .join("\n") );
註:
  • -R 等於 --remote-time
  • -o 等於 --output

3.2 wget

我地可以喺個網頁執行 JavaScript,佢會喺 console 度 print 一堆 wget commands 出黎:
console.log( [...document.querySelectorAll("table tbody tr > td:nth-of-type(2) a:not([href^='/'])")] .map((e) => `start /b wget -O "output_wget/${e.innerText}" "${e.href}"`) .join("\n") );
註:
  • -O 等於 --output-document

3.3 Java

我地可以:
  1. 用 jsoup 既 Java Maven library 直接下載個網頁。
  2. 然後用 jsoup 既 CSS selector API 去搵曬所有檔案既下載連結。
  3. 用 Java 既 Process API 去 call cURL 或者 wget CLI 去下載檔案。

3.3.1 Maven dependencies

<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.17.2</version> </dependency>

3.3.2 寫 Java code

1@Slf4j // lombok 2public class MainDownloader { 3 private static final String OUTPUT_DIR = "C:/Users/Michael/Downloads/output_java"; 4 5 public static void main(String[] args) throws Exception { 6 7 System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", 8 String.valueOf(100)); 9 10 new File(OUTPUT_DIR).mkdirs(); 11 12 Jsoup.connect("https://www.dre.vanderbilt.edu/~schmidt/cs253/2023-PDFs/") 13 .get() 14 .select("table tbody tr > td:nth-of-type(2) a:not([href^='/'])") 15 .stream() 16 .parallel() 17 .forEach(e -> { 18 final String fileName = e.ownText(); 19 final String url = e.absUrl("href"); 20 final String command = format("wget -O \"%s/%s\" \"%s\"", 21 OUTPUT_DIR, fileName, url); 22 23 log.info("Downloading [{}] from: {}", fileName, url); 24 25 try { 26 new ProcessBuilder("cmd", "/c", command) 27 .inheritIO() 28 .start() 29 .waitFor(); 30 } catch (Exception ex) { 31 log.error("Failed to execute command: {}", command, ex); 32 } 33 }); 34 } 35}
註:
  • ProcessBuilder 強迫我地一定要將一句完整既 command 斬件,再用 varargs 既形式傳入。
    • 但如果我地執行既 command 係 cmd /c,咁我地就可以將一句完整既 command 作為 cmd /c 既一個單獨既 argument 咁傳入。

4 參考資料