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 將所有檔案下載連結搵曬出黎:
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")
);
註:
3.3 Java
我地可以:
- 用 jsoup 既 Java Maven library 直接下載個網頁。
- 然後用 jsoup 既 CSS selector API 去搵曬所有檔案既下載連結。
- 用 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 參考資料