Skip to content

Commit 90e4750

Browse files
Improve Flickr scraper compatibility and CI (#45)
Co-authored-by: UltralyticsAssistant <web@ultralytics.com>
1 parent 35f8ef0 commit 90e4750

8 files changed

Lines changed: 304 additions & 43 deletions

File tree

.github/workflows/ci.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
2+
3+
name: CI
4+
5+
on:
6+
pull_request:
7+
branches: [main]
8+
push:
9+
branches: [main]
10+
schedule:
11+
- cron: "0 8 * * *"
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
tests:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout repository
21+
uses: actions/checkout@v4
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.11"
27+
28+
- name: Set up uv
29+
uses: astral-sh/setup-uv@v5
30+
31+
- name: Install dependencies
32+
run: uv pip install --system -r requirements.txt pytest ultralytics
33+
34+
- name: Compile Python files
35+
run: python -m compileall -q .
36+
37+
- name: Run tests
38+
run: pytest -q

README.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ The Flickr Scraper is a [Python](https://www.python.org/) tool designed to help
1717

1818
## 🔧 Requirements
1919

20-
Ensure you have Python 3.7 or later installed. The necessary dependencies can be installed using [pip](https://pip.pypa.io/en/stable/):
20+
Ensure you have Python 3.8 or later installed. The necessary dependencies can be installed using [pip](https://pip.pypa.io/en/stable/):
2121

2222
```bash
2323
pip install -U -r requirements.txt
@@ -47,7 +47,14 @@ pip install -U -r requirements.txt
4747
Before you begin scraping images:
4848

4949
1. **Get a Flickr API Key**: Obtain your unique API key and secret by applying [here](https://www.flickr.com/services/apps/create/apply).
50-
2. **Configure API Credentials**: Insert your API key and secret into the `flickr_scraper.py` script:
50+
2. **Configure API Credentials**: Set your Flickr API key and secret as environment variables:
51+
52+
```bash
53+
export FLICKR_API_KEY="YOUR_API_KEY"
54+
export FLICKR_API_SECRET="YOUR_API_SECRET"
55+
```
56+
57+
You can also pass credentials directly with `--key` and `--secret`, or insert them into `flickr_scraper.py` if you prefer a local-only script:
5158

5259
```python
5360
# flickr_scraper.py
@@ -69,9 +76,9 @@ python3 flickr_scraper.py --search 'honeybees on flowers' --n 10 --download
6976
You should see output indicating the download progress:
7077

7178
```plaintext
72-
0/10 https://live.staticflickr.com/21/38596887_40df118fd9_o.jpg
79+
1/10 https://live.staticflickr.com/21/38596887_40df118fd9_o.jpg
7380
...
74-
9/10 https://live.staticflickr.com/1770/43276172331_e779b8c161_o.jpg
81+
10/10 https://live.staticflickr.com/1770/43276172331_e779b8c161_o.jpg
7582
Done. (4.1s)
7683
All images saved to /Users/glennjocher/PycharmProjects/flickr_scraper/images/honeybees_on_flowers/
7784
```
@@ -80,6 +87,22 @@ The downloaded images will be available in the specified folder (e.g., `images/h
8087

8188
<img src="https://user-images.githubusercontent.com/26833433/75074332-4792c600-54b0-11ea-8c98-22acf58ba8e7.jpg" width="600" alt="Example scraped image of a honeybee on a flower">
8289

90+
## 🔁 Downloading More Results
91+
92+
Flickr search order is controlled by the Flickr API and may change over time. If you run the same command again with the same search term, Flickr can return many of the same results. To continue from a later result page, use `--page`:
93+
94+
```bash
95+
python3 flickr_scraper.py --search 'honeybees on flowers' --n 50 --download --page 2
96+
```
97+
98+
The script de-duplicates URLs within each run, but it does not keep a persistent database of previously downloaded Flickr photo IDs. For repeat dataset collection, keep each search in its own folder and use `--page` to request later Flickr pages.
99+
100+
## 🧩 Compatibility Notes
101+
102+
This scraper uses Flickr's JSON `photos.search` API response directly. It does not call `flickrapi.walk()`, avoiding the older `getchildren()` compatibility error reported with `flickrapi==2.4.0` on Python 3.9 and newer.
103+
104+
If Flickr returns a `500` or another API error, the script reports the Flickr error code and message. These errors usually come from Flickr or invalid credentials; retry the command after checking your API key and secret.
105+
83106
## 📜 Citation
84107

85108
If the Flickr Scraper tool helps your research or work, please consider citing it using the following DOI:

flickr_scraper.py

Lines changed: 90 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Generated by Glenn Jocher (glenn.jocher@ultralytics.com) for https://github.com/ultralytics
44

55
import argparse
6+
import os
67
import time
78
from pathlib import Path
89

@@ -14,54 +15,109 @@
1415
secret = ""
1516

1617

17-
def get_urls(search="honeybees on flowers", n=10, download=False):
18-
"""Fetch Flickr URLs for `search` term images, optionally downloading them; supports up to `n` images."""
19-
t = time.time()
20-
flickr = FlickrAPI(key, secret)
21-
license = () # https://www.flickr.com/services/api/explore/?method=flickr.photos.licenses.getInfo
22-
photos = flickr.walk(
23-
text=search, # http://www.flickr.com/services/api/flickr.photos.search.html
24-
extras="url_o",
25-
per_page=500, # 1-500
26-
license=license,
27-
sort="relevance",
18+
def build_photo_url(photo):
19+
"""Return the best available static image URL from a Flickr photo response."""
20+
url = photo.get("url_o") # original size
21+
return url or (
22+
f"https://farm{photo.get('farm')}.staticflickr.com/"
23+
f"{photo.get('server')}/{photo.get('id')}_{photo.get('secret')}_b.jpg"
2824
)
2925

26+
27+
def resolve_credentials(api_key=None, api_secret=None):
28+
"""Resolve Flickr API credentials from arguments, environment variables, or source constants."""
29+
api_key = api_key or os.getenv("FLICKR_API_KEY") or key
30+
api_secret = api_secret or os.getenv("FLICKR_API_SECRET") or secret
31+
if not api_key or not api_secret:
32+
help_url = "https://www.flickr.com/services/apps/create/apply"
33+
raise ValueError(
34+
"Flickr API key and secret are required. Pass --key/--secret, set "
35+
f"FLICKR_API_KEY/FLICKR_API_SECRET, or edit flickr_scraper.py. To apply visit {help_url}"
36+
)
37+
return api_key, api_secret
38+
39+
40+
def get_urls(search="honeybees on flowers", n=10, download=False, api_key=None, api_secret=None, page=1):
41+
"""Fetch Flickr URLs for a search term, optionally downloading up to n images."""
42+
if n < 1:
43+
return []
44+
45+
t = time.time()
46+
api_key, api_secret = resolve_credentials(api_key, api_secret)
47+
flickr = FlickrAPI(api_key, api_secret, format="parsed-json")
48+
licenses = "" # https://www.flickr.com/services/api/explore/?method=flickr.photos.licenses.getInfo
49+
page = max(page, 1)
50+
3051
if download:
3152
dir_path = Path.cwd() / "images" / search.replace(" ", "_")
3253
dir_path.mkdir(parents=True, exist_ok=True)
3354

3455
urls = []
35-
for i, photo in enumerate(photos):
36-
if i <= n:
37-
try:
38-
url = photo.get("url_o") # original size
39-
if url is None:
40-
url = f"https://farm{photo.get('farm')}.staticflickr.com/{photo.get('server')}/{photo.get('id')}_{photo.get('secret')}_b.jpg"
41-
42-
if download:
43-
download_uri(url, dir_path)
44-
45-
urls.append(url)
46-
print(f"{i}/{n} {url}")
47-
except Exception:
48-
print(f"{i}/{n} error...")
49-
50-
else:
51-
print(f"Done. ({time.time() - t:.1f}s)" + (f"\nAll images saved to {dir_path}" if download else ""))
56+
while len(urls) < n:
57+
try:
58+
response = flickr.photos.search(
59+
text=search, # https://www.flickr.com/services/api/flickr.photos.search.html
60+
extras="url_o",
61+
per_page=min(500, n - len(urls)), # 1-500
62+
page=page,
63+
license=licenses,
64+
media="photos",
65+
sort="relevance",
66+
)
67+
except Exception as e:
68+
raise RuntimeError(f"Flickr API request failed: {e}") from e
69+
70+
if response.get("stat") == "fail":
71+
code = response.get("code", "unknown")
72+
message = response.get("message", "unknown Flickr API error")
73+
raise RuntimeError(f"Flickr API error {code}: {message}")
74+
75+
photos = response.get("photos", {})
76+
photo_list = photos.get("photo", [])
77+
if not photo_list:
5278
break
5379

80+
for photo in photo_list:
81+
url = build_photo_url(photo)
82+
if url in urls:
83+
continue
84+
85+
if download:
86+
download_uri(url, dir_path)
87+
88+
urls.append(url)
89+
print(f"{len(urls)}/{n} {url}")
90+
91+
if len(urls) >= n:
92+
break
93+
94+
if page >= int(photos.get("pages", page)):
95+
break
96+
page += 1
97+
98+
print(f"Done. ({time.time() - t:.1f}s)" + (f"\nAll images saved to {dir_path}" if download else ""))
99+
return urls
100+
54101

55102
if __name__ == "__main__":
56103
parser = argparse.ArgumentParser()
57104
parser.add_argument("--search", nargs="+", default=["honeybees on flowers"], help="flickr search term")
58105
parser.add_argument("--n", type=int, default=10, help="number of images")
59106
parser.add_argument("--download", action="store_true", help="download images")
107+
parser.add_argument("--page", type=int, default=1, help="Flickr results page to start from")
108+
parser.add_argument("--key", default=None, help="Flickr API key, overrides FLICKR_API_KEY")
109+
parser.add_argument("--secret", default=None, help="Flickr API secret, overrides FLICKR_API_SECRET")
60110
opt = parser.parse_args()
61111

62-
print(f"nargs {opt.search}")
63-
help_url = "https://www.flickr.com/services/apps/create/apply"
64-
assert key and secret, f"Flickr API key required in flickr_scraper.py L11-12. To apply visit {help_url}"
65-
66-
for search in opt.search:
67-
get_urls(search=search, n=opt.n, download=opt.download)
112+
try:
113+
for search in opt.search:
114+
get_urls(
115+
search=search,
116+
n=opt.n,
117+
download=opt.download,
118+
api_key=opt.key,
119+
api_secret=opt.secret,
120+
page=opt.page,
121+
)
122+
except (RuntimeError, ValueError) as e:
123+
parser.exit(1, f"{e}\n")

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
flickrapi
44
numpy
55
pillow
6+
requests
67
tqdm

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
2+
3+
import sys
4+
from pathlib import Path
5+
6+
ROOT = Path(__file__).resolve().parents[1]
7+
if str(ROOT) not in sys.path:
8+
sys.path.insert(0, str(ROOT))

0 commit comments

Comments
 (0)