Repository navigation
Expand file tree
/
Copy pathfetch_book_data.py
More file actions
executable file
·161 lines (127 loc) · 4.46 KB
/
Copy pathfetch_book_data.py
File metadata and controls
executable file
·161 lines (127 loc) · 4.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
# "pandas",
# "requests",
# ]
# ///
from random import randint
import requests
import os
import pandas as pd
import time
# API key is optional - without it, requests are rate-limited but still work
API_KEY = os.getenv("GOOGLE_BOOKS_API_KEY")
BOOKS_API_URL = "https://www.googleapis.com/books/v1/volumes"
def get_books_page(pageIx: int, query: str = "intitle:the", res_per_page: int = 40):
offset = pageIx * res_per_page
fields = ",".join([
"items/volumeInfo/industryIdentifiers",
"items/volumeInfo/title",
"items/volumeInfo/authors",
"items/volumeInfo/publisher",
"items/volumeInfo/publishedDate",
"items/volumeInfo/categories",
])
params = {
"q": query,
"fields": fields,
"maxResults": 40,
"startIndex": offset,
}
if API_KEY:
params["key"] = API_KEY
response = requests.get(BOOKS_API_URL, params=params)
response.raise_for_status()
return response.json()
def get_isbn10(industry_identifiers: list[dict[str, str]]):
if len(industry_identifiers) == 0:
return None
for identifier in industry_identifiers:
if identifier["type"] == "ISBN_10":
return identifier["identifier"]
return None
def process_volume(volume: dict) -> dict[str, str | int | float | None] | None:
isbn = get_isbn10(volume.get("industryIdentifiers", []))
if isbn is None:
return None
title = volume.get("title")
authors = ",".join(volume.get("authors", []))
price = 0 # TODO
published_date = volume.get("publishedDate")
year = published_date.split("-")[0] if published_date else None
publisher = volume.get("publisher")
edited_by = None
out_of_print = 1 if randint(0, 20) == 0 else 0
categories = volume.get("categories", [])
category = categories[0] if len(categories) > 0 else None
updated_at = "2024-06-01T00:00:00Z" # Completely arbitrary date
return {
"isbn": isbn,
"title": title,
"authors": authors,
"price": price,
"year": year,
"publisher": publisher,
"edited_by": edited_by,
"out_of_print": out_of_print,
"category": category,
"updated_at": updated_at,
}
def process_items(items: list):
for item in items:
volume = process_volume(item["volumeInfo"])
if volume is not None:
yield volume
def fetch_with_retry(page_ix: int, query: str = "intitle:the", max_retries: int = 3, base_delay: float = 1.0):
"""Fetch a page with exponential backoff on failure."""
for attempt in range(max_retries):
try:
return get_books_page(page_ix, query=query)
except requests.exceptions.HTTPError as e:
if e.response.status_code in (429, 503) and attempt < max_retries - 1:
delay = base_delay * (2 ** attempt)
print(f" Rate limited, waiting {delay}s...")
time.sleep(delay)
else:
raise
def main():
start = time.time()
# Google Books API has a pagination limit (~1000 results max per query)
# Use multiple search queries to get more diverse results
queries = [
"intitle:the",
"intitle:and",
"intitle:of",
"intitle:to",
"intitle:a",
"subject:fiction",
"subject:science",
"subject:history",
"subject:biography",
"subject:philosophy",
]
pages_per_query = 10 # 10 pages * 40 results = 400 per query
results = []
os.makedirs("data", exist_ok=True)
for query in queries:
print(f"\nFetching: {query}")
for i in range(pages_per_query):
try:
res = fetch_with_retry(i, query=query)
results.append(res)
print(f" page {i + 1}/{pages_per_query}")
time.sleep(0.1)
except requests.exceptions.HTTPError as e:
if e.response.status_code == 400:
print(f" Hit pagination limit at page {i}")
break
raise
df = pd.DataFrame((item for res in results for item in process_items(res.get("items", []))))
# Remove duplicates by ISBN
df = df.drop_duplicates(subset=["isbn"])
df.to_csv("data/books.csv", index=False)
print(f"\ntook: {time.time() - start:.2f} seconds")
print(f"fetched {len(df)} unique books")
if __name__ == "__main__":
main()