wayback2warc 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wayback2warc-1.0.0/LICENSE +21 -0
- wayback2warc-1.0.0/PKG-INFO +101 -0
- wayback2warc-1.0.0/README.md +57 -0
- wayback2warc-1.0.0/pyproject.toml +32 -0
- wayback2warc-1.0.0/setup.cfg +4 -0
- wayback2warc-1.0.0/wayback2warc.egg-info/PKG-INFO +101 -0
- wayback2warc-1.0.0/wayback2warc.egg-info/SOURCES.txt +10 -0
- wayback2warc-1.0.0/wayback2warc.egg-info/dependency_links.txt +1 -0
- wayback2warc-1.0.0/wayback2warc.egg-info/entry_points.txt +2 -0
- wayback2warc-1.0.0/wayback2warc.egg-info/requires.txt +3 -0
- wayback2warc-1.0.0/wayback2warc.egg-info/top_level.txt +1 -0
- wayback2warc-1.0.0/wayback2warc.py +227 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tmctmt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wayback2warc
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: WARC exporter for the Wayback Machine
|
|
5
|
+
Author-email: tmctmt <tmctmt@proton.me>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 tmctmt
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Keywords: Internet Archive,Wayback Machine,WARC
|
|
29
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
30
|
+
Classifier: Environment :: Console
|
|
31
|
+
Classifier: Intended Audience :: System Administrators
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Natural Language :: English
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Classifier: Programming Language :: Python
|
|
36
|
+
Classifier: Topic :: Utilities
|
|
37
|
+
Requires-Python: >=3.8
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: httpx[socks]==0.28.1
|
|
41
|
+
Requires-Dist: tqdm==4.67.3
|
|
42
|
+
Requires-Dist: warcio==1.8.1
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# wayback2warc
|
|
46
|
+
A CLI tool that exports captures from the Wayback Machine and packs them into WARC files.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
usage: wayback2warc [-h] [-c COLLAPSE] [-f FILTER] [-m] [-p PROXY] [-t THREADS] [-w WARC_SIZE] url [prefix]
|
|
50
|
+
|
|
51
|
+
positional arguments:
|
|
52
|
+
url *.example.org or example.org/*
|
|
53
|
+
prefix output path prefix
|
|
54
|
+
|
|
55
|
+
options:
|
|
56
|
+
-h, --help show this help message and exit
|
|
57
|
+
-c, --collapse COLLAPSE
|
|
58
|
+
dedupe records based on returned key
|
|
59
|
+
-f, --filter FILTER filter records based on returned boolean
|
|
60
|
+
-m, --meta dump cdx metadata into jsonl file
|
|
61
|
+
-p, --proxy PROXY url formatted proxy, can be a single value or a file
|
|
62
|
+
-t, --threads THREADS
|
|
63
|
+
concurrent downloads (default: 2)
|
|
64
|
+
-w, --warc-size WARC_SIZE
|
|
65
|
+
max size of produced warc files in MB (default: 1024)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
`pip install wayback2warc`
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
Download all subdomains, all pages, and all captures for a domain:
|
|
74
|
+
|
|
75
|
+
`wayback2warc '*.example.org'`
|
|
76
|
+
|
|
77
|
+
Download all pages and all captures for a prefix:
|
|
78
|
+
|
|
79
|
+
`wayback2warc 'example.org/*'`
|
|
80
|
+
|
|
81
|
+
Download all captures for a single page:
|
|
82
|
+
|
|
83
|
+
`wayback2warc 'example.org'`
|
|
84
|
+
|
|
85
|
+
Download all pages but only a yearly capture of each:
|
|
86
|
+
|
|
87
|
+
`wayback2warc 'example.org/*' --collapse 'lambda m: (m.urlkey, m.timestamp[:4])'`
|
|
88
|
+
|
|
89
|
+
## Filtering and collapsing
|
|
90
|
+
This tool implements filtering/collapsing functionality on the client-side rather than relying on the CDX server to do so.
|
|
91
|
+
This is a necessity due to the way CDX paging works, but on the bright side it allows for much more granular queries.
|
|
92
|
+
|
|
93
|
+
Instead of a query language, lambda functions are used to narrow down captures for downloading.
|
|
94
|
+
A [CaptureMetadata](https://github.com/tmctmt/wayback2warc/blob/main/wayback2warc.py#L28) instance is passed to both functions, and the returned value is used as a key for collapsing, or as a boolean for filtering.
|
|
95
|
+
|
|
96
|
+
Collapse captures by URL excluding query:
|
|
97
|
+
`--collapse 'lambda m: m.urlkey.split("?")[0]'`
|
|
98
|
+
|
|
99
|
+
Filter captures before 2020 and exclude images:
|
|
100
|
+
`--filter 'lambda m: m.timestamp < "2020" and "image/" not in m.mimetype'`
|
|
101
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# wayback2warc
|
|
2
|
+
A CLI tool that exports captures from the Wayback Machine and packs them into WARC files.
|
|
3
|
+
|
|
4
|
+
```
|
|
5
|
+
usage: wayback2warc [-h] [-c COLLAPSE] [-f FILTER] [-m] [-p PROXY] [-t THREADS] [-w WARC_SIZE] url [prefix]
|
|
6
|
+
|
|
7
|
+
positional arguments:
|
|
8
|
+
url *.example.org or example.org/*
|
|
9
|
+
prefix output path prefix
|
|
10
|
+
|
|
11
|
+
options:
|
|
12
|
+
-h, --help show this help message and exit
|
|
13
|
+
-c, --collapse COLLAPSE
|
|
14
|
+
dedupe records based on returned key
|
|
15
|
+
-f, --filter FILTER filter records based on returned boolean
|
|
16
|
+
-m, --meta dump cdx metadata into jsonl file
|
|
17
|
+
-p, --proxy PROXY url formatted proxy, can be a single value or a file
|
|
18
|
+
-t, --threads THREADS
|
|
19
|
+
concurrent downloads (default: 2)
|
|
20
|
+
-w, --warc-size WARC_SIZE
|
|
21
|
+
max size of produced warc files in MB (default: 1024)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
`pip install wayback2warc`
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Download all subdomains, all pages, and all captures for a domain:
|
|
30
|
+
|
|
31
|
+
`wayback2warc '*.example.org'`
|
|
32
|
+
|
|
33
|
+
Download all pages and all captures for a prefix:
|
|
34
|
+
|
|
35
|
+
`wayback2warc 'example.org/*'`
|
|
36
|
+
|
|
37
|
+
Download all captures for a single page:
|
|
38
|
+
|
|
39
|
+
`wayback2warc 'example.org'`
|
|
40
|
+
|
|
41
|
+
Download all pages but only a yearly capture of each:
|
|
42
|
+
|
|
43
|
+
`wayback2warc 'example.org/*' --collapse 'lambda m: (m.urlkey, m.timestamp[:4])'`
|
|
44
|
+
|
|
45
|
+
## Filtering and collapsing
|
|
46
|
+
This tool implements filtering/collapsing functionality on the client-side rather than relying on the CDX server to do so.
|
|
47
|
+
This is a necessity due to the way CDX paging works, but on the bright side it allows for much more granular queries.
|
|
48
|
+
|
|
49
|
+
Instead of a query language, lambda functions are used to narrow down captures for downloading.
|
|
50
|
+
A [CaptureMetadata](https://github.com/tmctmt/wayback2warc/blob/main/wayback2warc.py#L28) instance is passed to both functions, and the returned value is used as a key for collapsing, or as a boolean for filtering.
|
|
51
|
+
|
|
52
|
+
Collapse captures by URL excluding query:
|
|
53
|
+
`--collapse 'lambda m: m.urlkey.split("?")[0]'`
|
|
54
|
+
|
|
55
|
+
Filter captures before 2020 and exclude images:
|
|
56
|
+
`--filter 'lambda m: m.timestamp < "2020" and "image/" not in m.mimetype'`
|
|
57
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wayback2warc"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "WARC exporter for the Wayback Machine"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "tmctmt", email = "tmctmt@proton.me" },
|
|
12
|
+
]
|
|
13
|
+
license = { file = "LICENSE" }
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: System Administrators",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Natural Language :: English",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python",
|
|
23
|
+
"Topic :: Utilities",
|
|
24
|
+
]
|
|
25
|
+
keywords = ["Internet Archive", "Wayback Machine", "WARC"]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx[socks]==0.28.1",
|
|
28
|
+
"tqdm==4.67.3",
|
|
29
|
+
"warcio==1.8.1",
|
|
30
|
+
]
|
|
31
|
+
[project.scripts]
|
|
32
|
+
wayback2warc = "wayback2warc.py"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wayback2warc
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: WARC exporter for the Wayback Machine
|
|
5
|
+
Author-email: tmctmt <tmctmt@proton.me>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 tmctmt
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Keywords: Internet Archive,Wayback Machine,WARC
|
|
29
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
30
|
+
Classifier: Environment :: Console
|
|
31
|
+
Classifier: Intended Audience :: System Administrators
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Natural Language :: English
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Classifier: Programming Language :: Python
|
|
36
|
+
Classifier: Topic :: Utilities
|
|
37
|
+
Requires-Python: >=3.8
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: httpx[socks]==0.28.1
|
|
41
|
+
Requires-Dist: tqdm==4.67.3
|
|
42
|
+
Requires-Dist: warcio==1.8.1
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# wayback2warc
|
|
46
|
+
A CLI tool that exports captures from the Wayback Machine and packs them into WARC files.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
usage: wayback2warc [-h] [-c COLLAPSE] [-f FILTER] [-m] [-p PROXY] [-t THREADS] [-w WARC_SIZE] url [prefix]
|
|
50
|
+
|
|
51
|
+
positional arguments:
|
|
52
|
+
url *.example.org or example.org/*
|
|
53
|
+
prefix output path prefix
|
|
54
|
+
|
|
55
|
+
options:
|
|
56
|
+
-h, --help show this help message and exit
|
|
57
|
+
-c, --collapse COLLAPSE
|
|
58
|
+
dedupe records based on returned key
|
|
59
|
+
-f, --filter FILTER filter records based on returned boolean
|
|
60
|
+
-m, --meta dump cdx metadata into jsonl file
|
|
61
|
+
-p, --proxy PROXY url formatted proxy, can be a single value or a file
|
|
62
|
+
-t, --threads THREADS
|
|
63
|
+
concurrent downloads (default: 2)
|
|
64
|
+
-w, --warc-size WARC_SIZE
|
|
65
|
+
max size of produced warc files in MB (default: 1024)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
`pip install wayback2warc`
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
Download all subdomains, all pages, and all captures for a domain:
|
|
74
|
+
|
|
75
|
+
`wayback2warc '*.example.org'`
|
|
76
|
+
|
|
77
|
+
Download all pages and all captures for a prefix:
|
|
78
|
+
|
|
79
|
+
`wayback2warc 'example.org/*'`
|
|
80
|
+
|
|
81
|
+
Download all captures for a single page:
|
|
82
|
+
|
|
83
|
+
`wayback2warc 'example.org'`
|
|
84
|
+
|
|
85
|
+
Download all pages but only a yearly capture of each:
|
|
86
|
+
|
|
87
|
+
`wayback2warc 'example.org/*' --collapse 'lambda m: (m.urlkey, m.timestamp[:4])'`
|
|
88
|
+
|
|
89
|
+
## Filtering and collapsing
|
|
90
|
+
This tool implements filtering/collapsing functionality on the client-side rather than relying on the CDX server to do so.
|
|
91
|
+
This is a necessity due to the way CDX paging works, but on the bright side it allows for much more granular queries.
|
|
92
|
+
|
|
93
|
+
Instead of a query language, lambda functions are used to narrow down captures for downloading.
|
|
94
|
+
A [CaptureMetadata](https://github.com/tmctmt/wayback2warc/blob/main/wayback2warc.py#L28) instance is passed to both functions, and the returned value is used as a key for collapsing, or as a boolean for filtering.
|
|
95
|
+
|
|
96
|
+
Collapse captures by URL excluding query:
|
|
97
|
+
`--collapse 'lambda m: m.urlkey.split("?")[0]'`
|
|
98
|
+
|
|
99
|
+
Filter captures before 2020 and exclude images:
|
|
100
|
+
`--filter 'lambda m: m.timestamp < "2020" and "image/" not in m.mimetype'`
|
|
101
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
wayback2warc.py
|
|
5
|
+
wayback2warc.egg-info/PKG-INFO
|
|
6
|
+
wayback2warc.egg-info/SOURCES.txt
|
|
7
|
+
wayback2warc.egg-info/dependency_links.txt
|
|
8
|
+
wayback2warc.egg-info/entry_points.txt
|
|
9
|
+
wayback2warc.egg-info/requires.txt
|
|
10
|
+
wayback2warc.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wayback2warc
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
3
|
+
from dataclasses import dataclass, astuple
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from operator import attrgetter
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import os.path
|
|
10
|
+
import random
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
import uuid
|
|
14
|
+
|
|
15
|
+
from tqdm import tqdm
|
|
16
|
+
from warcio import WARCWriter, StatusAndHeaders
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Capture:
|
|
21
|
+
url: str
|
|
22
|
+
date: datetime
|
|
23
|
+
statusline: str
|
|
24
|
+
headers: list[tuple[str, str]]
|
|
25
|
+
content: bytes
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class CaptureMetadata:
|
|
29
|
+
urlkey: str
|
|
30
|
+
timestamp: str
|
|
31
|
+
original: str
|
|
32
|
+
mimetype: str
|
|
33
|
+
statuscode: str
|
|
34
|
+
digest: str
|
|
35
|
+
length: str
|
|
36
|
+
|
|
37
|
+
class PooledClient:
|
|
38
|
+
base_url = 'https://web.archive.org'
|
|
39
|
+
timeout = httpx.Timeout(30, connect=5, read=30)
|
|
40
|
+
max_retries = 30
|
|
41
|
+
|
|
42
|
+
def __init__(self, proxy_list: list[str | None]):
|
|
43
|
+
self.proxy_list = proxy_list
|
|
44
|
+
self.session_pool = deque()
|
|
45
|
+
|
|
46
|
+
def request(self, method: str, url: str, retry: int = 1, **kwargs):
|
|
47
|
+
try:
|
|
48
|
+
session = self.session_pool.popleft()
|
|
49
|
+
except IndexError:
|
|
50
|
+
proxy = random.choice(self.proxy_list)
|
|
51
|
+
session = httpx.Client(base_url=self.base_url, timeout=self.timeout, proxy=proxy)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
resp = session.request(method, url, **kwargs)
|
|
55
|
+
# we're blocked if this header is not present
|
|
56
|
+
# exempt error 400 from retries as the cause is usually url encoding issues on our part
|
|
57
|
+
assert 'x-app-server' in resp.headers or resp.status_code == 400
|
|
58
|
+
except:
|
|
59
|
+
session.close()
|
|
60
|
+
if retry > self.max_retries:
|
|
61
|
+
raise
|
|
62
|
+
return self.request(method, url, retry+1, **kwargs)
|
|
63
|
+
else:
|
|
64
|
+
self.session_pool.append(session)
|
|
65
|
+
return resp
|
|
66
|
+
|
|
67
|
+
def get_cdx_page_count(self, url: str):
|
|
68
|
+
resp = self.request('GET', '/cdx/search/cdx', params={
|
|
69
|
+
'url': url,
|
|
70
|
+
'showNumPages': True
|
|
71
|
+
})
|
|
72
|
+
return int(resp.text)
|
|
73
|
+
|
|
74
|
+
def get_cdx_page(self, url: str, page: int):
|
|
75
|
+
resp = self.request('GET', '/cdx/search/cdx', params={
|
|
76
|
+
'url': url,
|
|
77
|
+
'page': page,
|
|
78
|
+
'output': 'json'
|
|
79
|
+
})
|
|
80
|
+
rows = resp.json()
|
|
81
|
+
return [CaptureMetadata(*row) for row in rows[1:]]
|
|
82
|
+
|
|
83
|
+
def get_capture(self, url: str, timestamp: str):
|
|
84
|
+
try:
|
|
85
|
+
resp = self.request('GET', f'/web/{timestamp}id_/{url}')
|
|
86
|
+
except:
|
|
87
|
+
# .request already handles retries
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# capture is not available
|
|
91
|
+
if 'memento-datetime' not in resp.headers:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
headers = [('content-length', f'{len(resp.content)}')]
|
|
95
|
+
for header, value in resp.headers.multi_items():
|
|
96
|
+
if header.startswith('x-archive-orig-'):
|
|
97
|
+
header = header[15:]
|
|
98
|
+
elif header == 'location':
|
|
99
|
+
value = re.sub(r'^(https?://web\.archive\.org)?/web/\d{14}\w*/', '', value)
|
|
100
|
+
elif header == 'content-type':
|
|
101
|
+
pass
|
|
102
|
+
else:
|
|
103
|
+
continue
|
|
104
|
+
if header not in ('content-length', 'content-encoding', 'transfer-encoding'):
|
|
105
|
+
headers.append((header, value))
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
original = resp.links['original']['url']
|
|
109
|
+
except KeyError:
|
|
110
|
+
original = url
|
|
111
|
+
date = datetime.strptime(timestamp, '%Y%m%d%H%M%S').replace(tzinfo=timezone.utc)
|
|
112
|
+
statusline = f'HTTP/1.1 {resp.status_code} {resp.reason_phrase}'
|
|
113
|
+
|
|
114
|
+
return Capture(original, date, statusline, headers, resp.content)
|
|
115
|
+
|
|
116
|
+
def unordered_map(pool, fn, iterable):
|
|
117
|
+
return (future.result() for future in as_completed(pool.submit(fn, item) for item in iterable))
|
|
118
|
+
|
|
119
|
+
if __name__ == '__main__':
|
|
120
|
+
arg_parser = argparse.ArgumentParser(
|
|
121
|
+
description='WARC exporter for the Wayback Machine',
|
|
122
|
+
epilog=(
|
|
123
|
+
"capture fields: urlkey, timestamp, original, mimetype, statuscode, digest, length\n"
|
|
124
|
+
"\n"
|
|
125
|
+
"examples:\n"
|
|
126
|
+
"%(prog)s '*.example.org' --collapse 'lambda m: m.digest'\n"
|
|
127
|
+
"%(prog)s 'twitter.com/elonmusk/status/*' -c 'lambda m: m.urlkey.split(\"?\")[0]' -f 'lambda m: m.timestamp < \"2022\"'"
|
|
128
|
+
),
|
|
129
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
130
|
+
)
|
|
131
|
+
arg_parser.add_argument('url',
|
|
132
|
+
help='*.example.org or example.org/*')
|
|
133
|
+
arg_parser.add_argument('prefix', nargs='?', default='',
|
|
134
|
+
help='output path prefix')
|
|
135
|
+
arg_parser.add_argument('-c', '--collapse', type=eval,
|
|
136
|
+
help='dedupe records based on returned key')
|
|
137
|
+
arg_parser.add_argument('-f', '--filter', type=eval,
|
|
138
|
+
help='filter records based on returned boolean')
|
|
139
|
+
arg_parser.add_argument('-m', '--meta', action='store_true',
|
|
140
|
+
help='dump cdx metadata into jsonl file')
|
|
141
|
+
arg_parser.add_argument('-p', '--proxy',
|
|
142
|
+
help='url formatted proxy, can be a single value or a file')
|
|
143
|
+
arg_parser.add_argument('-t', '--threads', type=int, default=2,
|
|
144
|
+
help='concurrent downloads (default: %(default)s)')
|
|
145
|
+
arg_parser.add_argument('-w', '--warc-size', type=int, default=1024,
|
|
146
|
+
help='max size of produced warc files in MB (default: %(default)s)')
|
|
147
|
+
args = arg_parser.parse_args()
|
|
148
|
+
|
|
149
|
+
if not args.proxy:
|
|
150
|
+
proxy_list = [None]
|
|
151
|
+
elif os.path.exists(args.proxy):
|
|
152
|
+
with open(args.proxy) as file:
|
|
153
|
+
proxy_list = file.read().splitlines()
|
|
154
|
+
else:
|
|
155
|
+
proxy_list = [args.proxy]
|
|
156
|
+
|
|
157
|
+
client = PooledClient(proxy_list)
|
|
158
|
+
pages = client.get_cdx_page_count(args.url)
|
|
159
|
+
pool = ThreadPoolExecutor(max_workers=args.threads)
|
|
160
|
+
queue: list[CaptureMetadata] = []
|
|
161
|
+
|
|
162
|
+
with tqdm(total=pages, desc='listing cdx') as progress:
|
|
163
|
+
keys = set()
|
|
164
|
+
|
|
165
|
+
for chunk in unordered_map(pool, lambda page: client.get_cdx_page(args.url, page), range(pages)):
|
|
166
|
+
for meta in chunk:
|
|
167
|
+
if int(meta.length) > 1024**2 * 100:
|
|
168
|
+
print(f'skipping large file: web.archive.org/web/{meta.timestamp}/{meta.original}', file=sys.stderr)
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
if args.filter and not args.filter(meta):
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
if args.collapse:
|
|
175
|
+
key = args.collapse(meta)
|
|
176
|
+
if key in keys:
|
|
177
|
+
continue
|
|
178
|
+
keys.add(key)
|
|
179
|
+
|
|
180
|
+
queue.append(meta)
|
|
181
|
+
|
|
182
|
+
progress.set_postfix({'queued': len(queue)})
|
|
183
|
+
progress.update()
|
|
184
|
+
|
|
185
|
+
del keys
|
|
186
|
+
|
|
187
|
+
queue.sort(key=attrgetter('timestamp'))
|
|
188
|
+
|
|
189
|
+
if args.meta:
|
|
190
|
+
with open(f'{args.prefix}meta.jsonl', 'w') as file:
|
|
191
|
+
for meta in queue:
|
|
192
|
+
file.write(json.dumps(astuple(meta)) + '\n')
|
|
193
|
+
sys.exit()
|
|
194
|
+
|
|
195
|
+
with tqdm(total=len(queue), desc='downloading captures') as progress:
|
|
196
|
+
file = None
|
|
197
|
+
skipped = 0
|
|
198
|
+
|
|
199
|
+
for capture in unordered_map(pool, lambda meta: client.get_capture(meta.original, meta.timestamp), queue):
|
|
200
|
+
if not capture:
|
|
201
|
+
skipped += 1
|
|
202
|
+
progress.set_postfix({'skipped': skipped})
|
|
203
|
+
progress.update()
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
if not file or file.tell() > 1024**2 * args.warc_size:
|
|
207
|
+
if file:
|
|
208
|
+
file.close()
|
|
209
|
+
file = open(f'{args.prefix}{uuid.uuid4()}.warc.gz', 'xb')
|
|
210
|
+
writer = WARCWriter(file, gzip=True)
|
|
211
|
+
|
|
212
|
+
record = writer.create_warc_record(
|
|
213
|
+
uri=capture.url,
|
|
214
|
+
record_type='response',
|
|
215
|
+
payload=BytesIO(capture.content),
|
|
216
|
+
warc_headers_dict={'WARC-Date': capture.date.isoformat().replace('+00:00', 'Z')},
|
|
217
|
+
http_headers=StatusAndHeaders(capture.statusline, capture.headers)
|
|
218
|
+
)
|
|
219
|
+
deterministic_uuid = uuid.uuid5(uuid.NAMESPACE_URL, f'{capture.url}|{capture.date.isoformat()}')
|
|
220
|
+
record.rec_headers.replace_header('WARC-Record-ID', f'<urn:uuid:{deterministic_uuid}>')
|
|
221
|
+
writer.write_record(record)
|
|
222
|
+
progress.update()
|
|
223
|
+
|
|
224
|
+
if file:
|
|
225
|
+
file.close()
|
|
226
|
+
|
|
227
|
+
pool.shutdown()
|