pingrabber 0.1.7__py3-none-any.whl
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.
pingrabber/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pingrabber
|
|
3
|
+
==========
|
|
4
|
+
|
|
5
|
+
Thư viện đơn giản giúp cào (scrape) ảnh chất lượng cao từ một board
|
|
6
|
+
Pinterest công khai thông qua RSS feed của Pinterest.
|
|
7
|
+
|
|
8
|
+
Cách dùng nhanh:
|
|
9
|
+
|
|
10
|
+
import pingrabber
|
|
11
|
+
|
|
12
|
+
# Tải ảnh từ board hoặc pin đơn lẻ (tự nhận diện loại URL)
|
|
13
|
+
pingrabber.download("https://www.pinterest.com/username/boardname/")
|
|
14
|
+
|
|
15
|
+
# Tìm theo từ khóa, lấy link ảnh raw (không tải file về máy)
|
|
16
|
+
links = pingrabber.search("thiên nhiên")
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .core import PinGrabber, download, search
|
|
21
|
+
|
|
22
|
+
__all__ = ["PinGrabber", "download", "search"]
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.7"
|
pingrabber/core.py
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pingrabber.core
|
|
3
|
+
~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Logic chính: tải ảnh chất lượng cao từ Pinterest, hỗ trợ 2 dạng URL:
|
|
6
|
+
|
|
7
|
+
1. URL BOARD (pinterest.com/<user>/<board>/):
|
|
8
|
+
Lấy RSS feed của board, phân tích bằng BeautifulSoup để tìm các
|
|
9
|
+
liên kết ảnh, sau đó tải ảnh gốc (chất lượng cao nhất) về máy.
|
|
10
|
+
|
|
11
|
+
2. URL PIN ĐƠN LẺ (pinterest.com/pin/<id>/):
|
|
12
|
+
Pin lẻ không có RSS feed riêng, nên thư viện sẽ fetch trực tiếp
|
|
13
|
+
trang HTML của pin và lấy ảnh từ thẻ <meta property="og:image">.
|
|
14
|
+
|
|
15
|
+
Hàm `download()` / `PinGrabber.download()` tự động nhận diện loại URL
|
|
16
|
+
và xử lý phù hợp, người dùng không cần phân biệt thủ công.
|
|
17
|
+
|
|
18
|
+
Ghi chú kỹ thuật:
|
|
19
|
+
Pinterest cung cấp RSS feed công khai cho mỗi board theo dạng:
|
|
20
|
+
https://www.pinterest.com/<user>/<board>.rss
|
|
21
|
+
|
|
22
|
+
Mỗi <item> trong feed chứa một đoạn HTML (trong <description>) có thẻ
|
|
23
|
+
<img src="..."> trỏ tới ảnh thumbnail. URL ảnh thumbnail thường có dạng:
|
|
24
|
+
https://i.pinimg.com/236x/xx/xx/xx/xxxxxxx.jpg
|
|
25
|
+
|
|
26
|
+
Để lấy ảnh gốc (kích thước lớn nhất), ta thay thế phần kích thước
|
|
27
|
+
(ví dụ "236x") bằng "originals".
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import re
|
|
34
|
+
import time
|
|
35
|
+
import random
|
|
36
|
+
import logging
|
|
37
|
+
from typing import List, Optional
|
|
38
|
+
from urllib.parse import urlparse
|
|
39
|
+
|
|
40
|
+
import requests
|
|
41
|
+
from bs4 import BeautifulSoup
|
|
42
|
+
|
|
43
|
+
# `ddgs` (trước đây là `duckduckgo-search`) là dependency TÙY CHỌN, chỉ dùng
|
|
44
|
+
# cho tính năng search() theo từ khóa. Nếu chưa cài, search() sẽ tự động
|
|
45
|
+
# rơi về phương án dự phòng dùng requests + BeautifulSoup thuần.
|
|
46
|
+
try:
|
|
47
|
+
from ddgs import DDGS # package mới, khuyến nghị
|
|
48
|
+
_HAS_DDGS = True
|
|
49
|
+
except ImportError:
|
|
50
|
+
try:
|
|
51
|
+
from duckduckgo_search import DDGS # tên package cũ, vẫn còn trên PyPI
|
|
52
|
+
_HAS_DDGS = True
|
|
53
|
+
except ImportError:
|
|
54
|
+
DDGS = None
|
|
55
|
+
_HAS_DDGS = False
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger("pingrabber")
|
|
58
|
+
logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
|
|
59
|
+
|
|
60
|
+
# Danh sách User-Agent thật để xoay vòng, giảm khả năng bị nhận diện là bot
|
|
61
|
+
# khi gọi đến công cụ tìm kiếm (search engine thường nhạy với UA hơn Pinterest).
|
|
62
|
+
USER_AGENTS = [
|
|
63
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
64
|
+
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
65
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
|
|
66
|
+
"(KHTML, like Gecko) Version/17.4 Safari/605.1.15",
|
|
67
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
|
68
|
+
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
69
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) "
|
|
70
|
+
"Gecko/20100101 Firefox/125.0",
|
|
71
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) "
|
|
72
|
+
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
# User-Agent mặc định dùng cho các request tải ảnh/RSS (ổn định, không cần xoay)
|
|
76
|
+
DEFAULT_HEADERS = {
|
|
77
|
+
"User-Agent": USER_AGENTS[0],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _random_search_headers() -> dict:
|
|
82
|
+
"""Tạo header ngẫu nhiên (User-Agent + Accept-Language) cho mỗi lần search,
|
|
83
|
+
giúp giảm khả năng bị nhận diện và chặn theo pattern cố định."""
|
|
84
|
+
return {
|
|
85
|
+
"User-Agent": random.choice(USER_AGENTS),
|
|
86
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
87
|
+
"Accept-Language": random.choice(["en-US,en;q=0.9", "vi-VN,vi;q=0.9,en;q=0.8"]),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Regex để tìm URL ảnh i.pinimg.com bên trong đoạn HTML mô tả của RSS item
|
|
91
|
+
IMG_URL_PATTERN = re.compile(
|
|
92
|
+
r"https?://i\.pinimg\.com/[^\s\"'<>]+\.(?:jpg|jpeg|png|gif|webp)",
|
|
93
|
+
re.IGNORECASE,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Các kích thước thumbnail phổ biến mà Pinterest dùng trong URL ảnh,
|
|
97
|
+
# cần thay bằng "originals" để lấy ảnh gốc chất lượng cao nhất.
|
|
98
|
+
THUMBNAIL_SIZE_PATTERN = re.compile(
|
|
99
|
+
r"/(\d+x(?:\d+)?|originals)/", re.IGNORECASE
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Nhận diện URL pin đơn lẻ, ví dụ: pinterest.com/pin/119134352618387326/
|
|
103
|
+
SINGLE_PIN_PATTERN = re.compile(r"^/pin/(\d+)", re.IGNORECASE)
|
|
104
|
+
|
|
105
|
+
# Danh sách công cụ tìm kiếm để thử lần lượt (fallback) khi một engine bị
|
|
106
|
+
# chặn hoặc trả lỗi. Mỗi engine có format URL kết quả khác nhau nên cần
|
|
107
|
+
# parser riêng (xem hàm _parse_search_results).
|
|
108
|
+
SEARCH_ENGINES = [
|
|
109
|
+
{
|
|
110
|
+
"name": "duckduckgo_html",
|
|
111
|
+
"url": "https://html.duckduckgo.com/html/",
|
|
112
|
+
"param": "q",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "duckduckgo_lite",
|
|
116
|
+
"url": "https://lite.duckduckgo.com/lite/",
|
|
117
|
+
"param": "q",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "bing",
|
|
121
|
+
"url": "https://www.bing.com/search",
|
|
122
|
+
"param": "q",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "brave",
|
|
126
|
+
"url": "https://search.brave.com/search",
|
|
127
|
+
"param": "q",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "yandex_lite",
|
|
131
|
+
"url": "https://yandex.com", # Bản Lite của Yandex Nga, HTML siêu nhẹ, ít check bot
|
|
132
|
+
"param": "text",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "yahoo",
|
|
136
|
+
"url": "https://yahoo.com", # HTML lỏng lẻo giống Bing, quét rất mượt
|
|
137
|
+
"param": "p",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"name": "ecosia",
|
|
141
|
+
"url": "https://ecosia.org", # Sử dụng lõi Bing nhưng hệ thống lọc bot lỏng hơn
|
|
142
|
+
"param": "q",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"name": "qwant_lite",
|
|
146
|
+
"url": "https://qwant.com", # Phiên bản không JavaScript của công cụ tìm kiếm Pháp
|
|
147
|
+
"param": "q",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"name": "mojeek",
|
|
151
|
+
"url": "https://mojeek.com", # Bộ máy độc lập hoàn toàn, không có hệ thống chống bot gắt gao
|
|
152
|
+
"param": "q",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"name": "gibiru",
|
|
156
|
+
"url": "https://gibiru.com", # Trang tìm kiếm ẩn danh, cấu trúc HTML thô sơ
|
|
157
|
+
"param": "q",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"name": "ask",
|
|
161
|
+
"url": "https://ask.com", # Trang tìm kiếm lâu đời, máy chủ lọc bot rất yếu
|
|
162
|
+
"param": "q",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "seznam",
|
|
166
|
+
"url": "https://seznam.cz", # Công cụ tìm kiếm nội địa của Séc, bot cào thoải mái
|
|
167
|
+
"param": "q",
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# Regex nhận diện URL board hợp lệ trong kết quả tìm kiếm:
|
|
173
|
+
# pinterest.com/<user>/<board>/ nhưng KHÔNG phải /pin/..., /search/..., /explore/...
|
|
174
|
+
BOARD_URL_PATTERN = re.compile(
|
|
175
|
+
r"^https?://(?:[a-z]{2,3}\.)?pinterest\.[a-z.]+/"
|
|
176
|
+
r"(?!pin/|search/|explore/|today/)([\w.\-%]+)/([\w.\-%]+)/?$",
|
|
177
|
+
re.IGNORECASE,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class PinGrabberError(Exception):
|
|
182
|
+
"""Lỗi tùy biến cho thư viện pingrabber."""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class PinGrabber:
|
|
186
|
+
"""Đối tượng chính để cào và tải ảnh từ một board Pinterest."""
|
|
187
|
+
|
|
188
|
+
def __init__(self, timeout: int = 15, session: Optional[requests.Session] = None):
|
|
189
|
+
self.timeout = timeout
|
|
190
|
+
self.session = session or requests.Session()
|
|
191
|
+
self.session.headers.update(DEFAULT_HEADERS)
|
|
192
|
+
|
|
193
|
+
# ------------------------------------------------------------------ #
|
|
194
|
+
# Tiện ích: kiểm tra URL là pin đơn lẻ hay board
|
|
195
|
+
# ------------------------------------------------------------------ #
|
|
196
|
+
@staticmethod
|
|
197
|
+
def is_single_pin_url(url: str) -> bool:
|
|
198
|
+
"""Trả về True nếu URL có dạng pinterest.com/pin/<id>/ (pin đơn lẻ)."""
|
|
199
|
+
parsed = urlparse(url)
|
|
200
|
+
return bool(SINGLE_PIN_PATTERN.match(parsed.path))
|
|
201
|
+
|
|
202
|
+
# ------------------------------------------------------------------ #
|
|
203
|
+
# TÌM KIẾM THEO TỪ KHÓA
|
|
204
|
+
# ------------------------------------------------------------------ #
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _extract_bing_real_url(href: str) -> Optional[str]:
|
|
207
|
+
"""Bing thường bọc link thật trong link redirect dạng base64
|
|
208
|
+
(tham số 'u', tiền tố 'a1' + base64 của URL thật). Hàm này giải mã
|
|
209
|
+
ra URL thật nếu có, ngược lại trả về href gốc không đổi."""
|
|
210
|
+
import base64
|
|
211
|
+
from urllib.parse import urlparse as _urlparse, parse_qs
|
|
212
|
+
|
|
213
|
+
parsed = _urlparse(href)
|
|
214
|
+
qs = parse_qs(parsed.query)
|
|
215
|
+
if "u" in qs:
|
|
216
|
+
raw = qs["u"][0]
|
|
217
|
+
if raw.startswith("a1"):
|
|
218
|
+
raw = raw[2:]
|
|
219
|
+
# Bing dùng base64 URL-safe, có thể thiếu padding '='
|
|
220
|
+
padding = "=" * (-len(raw) % 4)
|
|
221
|
+
try:
|
|
222
|
+
decoded = base64.urlsafe_b64decode(raw + padding).decode("utf-8", errors="ignore")
|
|
223
|
+
return decoded
|
|
224
|
+
except (ValueError, UnicodeDecodeError):
|
|
225
|
+
return href
|
|
226
|
+
return href
|
|
227
|
+
|
|
228
|
+
def _parse_search_results(self, engine_name: str, html: str) -> List[str]:
|
|
229
|
+
"""Phân tích HTML kết quả tìm kiếm để lấy danh sách URL board hợp lệ.
|
|
230
|
+
Mỗi engine có cấu trúc HTML khác nhau nên xử lý link hơi khác nhau."""
|
|
231
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
232
|
+
board_urls: List[str] = []
|
|
233
|
+
|
|
234
|
+
for link in soup.find_all("a", href=True):
|
|
235
|
+
href = link["href"]
|
|
236
|
+
|
|
237
|
+
if engine_name == "bing":
|
|
238
|
+
href = self._extract_bing_real_url(href) or href
|
|
239
|
+
|
|
240
|
+
match = BOARD_URL_PATTERN.match(href)
|
|
241
|
+
if match:
|
|
242
|
+
normalized = href.split("?")[0].rstrip("/") + "/"
|
|
243
|
+
if normalized not in board_urls:
|
|
244
|
+
board_urls.append(normalized)
|
|
245
|
+
|
|
246
|
+
return board_urls
|
|
247
|
+
|
|
248
|
+
def _find_boards_via_ddgs(self, keyword: str, max_results: int = 5) -> List[str]:
|
|
249
|
+
"""
|
|
250
|
+
Tìm board bằng package `ddgs` (trước đây là `duckduckgo-search`).
|
|
251
|
+
|
|
252
|
+
Ưu điểm so với cách tự gọi requests:
|
|
253
|
+
- Package được cộng đồng duy trì, tự xử lý header/cookie giống
|
|
254
|
+
trình duyệt thật hơn, tỷ lệ bị chặn thấp hơn.
|
|
255
|
+
- Hỗ trợ proxy (http/https/socks5) ngay trong constructor, đây
|
|
256
|
+
là cách hiệu quả nhất để né chặn theo IP nếu bạn có proxy.
|
|
257
|
+
- Tự động thử nhiều backend bên trong (DuckDuckGo, Bing, Google...).
|
|
258
|
+
|
|
259
|
+
Trả về [] nếu ddgs chưa được cài, hoặc nếu gọi thất bại (lỗi sẽ
|
|
260
|
+
được log lại, không raise, để find_boards() có thể rơi về fallback).
|
|
261
|
+
"""
|
|
262
|
+
if not _HAS_DDGS:
|
|
263
|
+
return []
|
|
264
|
+
|
|
265
|
+
query = f"site:pinterest.com {keyword}"
|
|
266
|
+
try:
|
|
267
|
+
ddgs = DDGS(timeout=self.timeout)
|
|
268
|
+
try:
|
|
269
|
+
raw_results = ddgs.text(query, max_results=max_results * 3)
|
|
270
|
+
finally:
|
|
271
|
+
# Một số phiên bản DDGS hỗ trợ context manager / .close(),
|
|
272
|
+
# đóng lại nếu có để giải phóng kết nối HTTP.
|
|
273
|
+
close_fn = getattr(ddgs, "close", None)
|
|
274
|
+
if callable(close_fn):
|
|
275
|
+
close_fn()
|
|
276
|
+
except Exception as exc: # noqa: BLE001 - ddgs có thể raise nhiều loại lỗi riêng
|
|
277
|
+
logger.warning(" [ddgs] Gọi thất bại: %s", exc)
|
|
278
|
+
return []
|
|
279
|
+
|
|
280
|
+
board_urls: List[str] = []
|
|
281
|
+
for item in raw_results or []:
|
|
282
|
+
# Tùy phiên bản, key có thể là "href" hoặc "url"
|
|
283
|
+
href = item.get("href") or item.get("url") or ""
|
|
284
|
+
if not href:
|
|
285
|
+
continue
|
|
286
|
+
match = BOARD_URL_PATTERN.match(href)
|
|
287
|
+
if match:
|
|
288
|
+
normalized = href.split("?")[0].rstrip("/") + "/"
|
|
289
|
+
if normalized not in board_urls:
|
|
290
|
+
board_urls.append(normalized)
|
|
291
|
+
|
|
292
|
+
return board_urls[:max_results]
|
|
293
|
+
|
|
294
|
+
def _find_boards_via_requests(
|
|
295
|
+
self,
|
|
296
|
+
keyword: str,
|
|
297
|
+
max_results: int = 5,
|
|
298
|
+
max_retries: int = 2,
|
|
299
|
+
delay_seconds: float = 1.5,
|
|
300
|
+
) -> List[str]:
|
|
301
|
+
"""
|
|
302
|
+
Phương án DỰ PHÒNG: tự gọi requests đến nhiều search engine, dùng khi
|
|
303
|
+
package `ddgs` chưa được cài hoặc gọi thất bại. Có xoay User-Agent,
|
|
304
|
+
retry, và fallback giữa nhiều engine (DuckDuckGo HTML/Lite, Bing).
|
|
305
|
+
"""
|
|
306
|
+
query = f"site:pinterest.com {keyword}"
|
|
307
|
+
last_error: Optional[Exception] = None
|
|
308
|
+
|
|
309
|
+
for engine in SEARCH_ENGINES:
|
|
310
|
+
for attempt in range(1, max_retries + 1):
|
|
311
|
+
try:
|
|
312
|
+
headers = _random_search_headers()
|
|
313
|
+
resp = self.session.get(
|
|
314
|
+
engine["url"],
|
|
315
|
+
params={engine["param"]: query},
|
|
316
|
+
headers=headers,
|
|
317
|
+
timeout=self.timeout,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if resp.status_code in (403, 429):
|
|
321
|
+
logger.warning(
|
|
322
|
+
" [%s] Bị chặn (HTTP %d), thử lại lần %d/%d...",
|
|
323
|
+
engine["name"], resp.status_code, attempt, max_retries,
|
|
324
|
+
)
|
|
325
|
+
time.sleep(delay_seconds + random.uniform(0, 1.5))
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
resp.raise_for_status()
|
|
329
|
+
|
|
330
|
+
board_urls = self._parse_search_results(engine["name"], resp.text)
|
|
331
|
+
if board_urls:
|
|
332
|
+
logger.info(
|
|
333
|
+
" [%s] Tìm thấy %d board.", engine["name"], len(board_urls)
|
|
334
|
+
)
|
|
335
|
+
return board_urls[:max_results]
|
|
336
|
+
|
|
337
|
+
logger.info(" [%s] Không có kết quả phù hợp, thử engine khác.", engine["name"])
|
|
338
|
+
break # Engine trả về OK nhưng không có board -> chuyển engine khác
|
|
339
|
+
|
|
340
|
+
except requests.RequestException as exc:
|
|
341
|
+
last_error = exc
|
|
342
|
+
logger.warning(
|
|
343
|
+
" [%s] Lỗi request (lần %d/%d): %s",
|
|
344
|
+
engine["name"], attempt, max_retries, exc,
|
|
345
|
+
)
|
|
346
|
+
time.sleep(delay_seconds + random.uniform(0, 1.5))
|
|
347
|
+
|
|
348
|
+
# Nghỉ một chút trước khi chuyển sang engine kế tiếp
|
|
349
|
+
time.sleep(delay_seconds)
|
|
350
|
+
|
|
351
|
+
if last_error:
|
|
352
|
+
logger.error(
|
|
353
|
+
"Tất cả công cụ tìm kiếm đều thất bại. Lỗi cuối: %s. "
|
|
354
|
+
"Mạng của bạn có thể đang bị chặn truy cập search engine — "
|
|
355
|
+
"hãy thử lại sau, đổi mạng/VPN, hoặc tự tìm board qua trình "
|
|
356
|
+
"duyệt rồi dùng download(url_board) trực tiếp.",
|
|
357
|
+
last_error,
|
|
358
|
+
)
|
|
359
|
+
return []
|
|
360
|
+
|
|
361
|
+
def find_boards(
|
|
362
|
+
self,
|
|
363
|
+
keyword: str,
|
|
364
|
+
max_results: int = 5,
|
|
365
|
+
max_retries: int = 2,
|
|
366
|
+
delay_seconds: float = 1.5,
|
|
367
|
+
) -> List[str]:
|
|
368
|
+
"""
|
|
369
|
+
Tìm các URL board Pinterest liên quan đến từ khóa.
|
|
370
|
+
|
|
371
|
+
Thứ tự ưu tiên:
|
|
372
|
+
1. Dùng package `ddgs` (khuyến nghị) nếu đã cài — package này
|
|
373
|
+
được cộng đồng duy trì, tự xử lý giả lập trình duyệt tốt
|
|
374
|
+
hơn và hỗ trợ proxy, nên tỷ lệ bị chặn thấp hơn nhiều.
|
|
375
|
+
2. Nếu chưa cài `ddgs` hoặc gọi thất bại, tự động rơi về
|
|
376
|
+
phương án dự phòng: gọi requests trực tiếp đến nhiều search
|
|
377
|
+
engine (DuckDuckGo HTML/Lite, Bing), có xoay User-Agent,
|
|
378
|
+
retry và delay giữa các lần thử.
|
|
379
|
+
|
|
380
|
+
Để dùng phương án 1 (khuyến nghị), cài thêm:
|
|
381
|
+
pip install ddgs
|
|
382
|
+
|
|
383
|
+
Lưu ý: đây KHÔNG phải tìm kiếm trực tiếp trong Pinterest (trang đó
|
|
384
|
+
cần JS render nên requests không đọc được), mà là dùng index của
|
|
385
|
+
công cụ tìm kiếm để tìm ra các board công khai liên quan. Kết quả
|
|
386
|
+
có thể rỗng tùy theo IP/mạng của bạn — nếu vậy, hãy tự tìm board
|
|
387
|
+
qua trình duyệt và dùng download(url_board) trực tiếp.
|
|
388
|
+
|
|
389
|
+
Trả về danh sách URL board (đã loại trùng), tối đa max_results.
|
|
390
|
+
"""
|
|
391
|
+
if _HAS_DDGS:
|
|
392
|
+
logger.info("Đang tìm board qua package ddgs...")
|
|
393
|
+
board_urls = self._find_boards_via_ddgs(keyword, max_results=max_results)
|
|
394
|
+
if board_urls:
|
|
395
|
+
logger.info(" [ddgs] Tìm thấy %d board.", len(board_urls))
|
|
396
|
+
return board_urls
|
|
397
|
+
logger.info(" [ddgs] Không có kết quả, chuyển sang phương án dự phòng (requests).")
|
|
398
|
+
else:
|
|
399
|
+
logger.info(
|
|
400
|
+
"Package 'ddgs' chưa được cài (pip install ddgs) — "
|
|
401
|
+
"dùng phương án dự phòng qua requests."
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
return self._find_boards_via_requests(
|
|
405
|
+
keyword,
|
|
406
|
+
max_results=max_results,
|
|
407
|
+
max_retries=max_retries,
|
|
408
|
+
delay_seconds=delay_seconds,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def search(
|
|
412
|
+
self,
|
|
413
|
+
keyword: str,
|
|
414
|
+
max_boards: int = 3,
|
|
415
|
+
max_images_per_board: int = 25,
|
|
416
|
+
max_retries: int = 2,
|
|
417
|
+
delay_seconds: float = 1.5,
|
|
418
|
+
) -> List[str]:
|
|
419
|
+
"""
|
|
420
|
+
Tìm board liên quan đến từ khóa, rồi tự động lấy danh sách URL ảnh
|
|
421
|
+
gốc (chất lượng cao) từ các board đó. KHÔNG tải ảnh về máy, chỉ
|
|
422
|
+
trả về danh sách link raw để bạn tự quyết định dùng tiếp.
|
|
423
|
+
|
|
424
|
+
Ví dụ:
|
|
425
|
+
import pingrabber
|
|
426
|
+
links = pingrabber.search("thiên nhiên")
|
|
427
|
+
print(links) # ['https://i.pinimg.com/originals/.../a.jpg', ...]
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
keyword: từ khóa tìm kiếm, ví dụ "thiên nhiên", "nature".
|
|
431
|
+
max_boards: số board tối đa sẽ quét qua.
|
|
432
|
+
max_images_per_board: số ảnh tối đa lấy từ mỗi board.
|
|
433
|
+
max_retries: số lần thử lại mỗi search engine nếu bị chặn/lỗi.
|
|
434
|
+
delay_seconds: thời gian nghỉ cơ bản (giây) giữa các lần thử,
|
|
435
|
+
giúp giảm khả năng bị search engine chặn vì gọi quá nhanh.
|
|
436
|
+
|
|
437
|
+
Trả về danh sách URL ảnh gốc (string), rỗng nếu không tìm được gì.
|
|
438
|
+
Nếu trả về rỗng, kiểm tra log WARNING/ERROR để biết nguyên nhân cụ
|
|
439
|
+
thể (bị chặn, hết engine, hay không có board liên quan).
|
|
440
|
+
"""
|
|
441
|
+
logger.info("Đang tìm board liên quan đến từ khóa: %s", keyword)
|
|
442
|
+
board_urls = self.find_boards(
|
|
443
|
+
keyword,
|
|
444
|
+
max_results=max_boards,
|
|
445
|
+
max_retries=max_retries,
|
|
446
|
+
delay_seconds=delay_seconds,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if not board_urls:
|
|
450
|
+
logger.warning(
|
|
451
|
+
"Không tìm thấy board nào liên quan đến: %s. "
|
|
452
|
+
"Nếu log phía trên cho thấy lỗi 403/429 ở tất cả engine, "
|
|
453
|
+
"khả năng cao mạng/IP của bạn đang bị search engine chặn.",
|
|
454
|
+
keyword,
|
|
455
|
+
)
|
|
456
|
+
return []
|
|
457
|
+
|
|
458
|
+
logger.info("Tìm thấy %d board, đang lấy ảnh...", len(board_urls))
|
|
459
|
+
|
|
460
|
+
all_image_urls: List[str] = []
|
|
461
|
+
for board_url in board_urls:
|
|
462
|
+
try:
|
|
463
|
+
rss_url = self.build_rss_url(board_url)
|
|
464
|
+
rss_content = self.fetch_rss(rss_url)
|
|
465
|
+
image_urls = self.extract_image_urls(rss_content)[:max_images_per_board]
|
|
466
|
+
logger.info(" + %s -> %d ảnh", board_url, len(image_urls))
|
|
467
|
+
for url in image_urls:
|
|
468
|
+
if url not in all_image_urls:
|
|
469
|
+
all_image_urls.append(url)
|
|
470
|
+
# Delay nhẹ giữa các board để tránh gửi request quá dồn dập
|
|
471
|
+
time.sleep(delay_seconds * 0.5)
|
|
472
|
+
except PinGrabberError as exc:
|
|
473
|
+
logger.error(" Lỗi khi xử lý board %s: %s", board_url, exc)
|
|
474
|
+
|
|
475
|
+
logger.info("Hoàn tất. Tổng cộng %d link ảnh raw.", len(all_image_urls))
|
|
476
|
+
return all_image_urls
|
|
477
|
+
|
|
478
|
+
# ------------------------------------------------------------------ #
|
|
479
|
+
# Bước 1: Chuyển URL board thường -> URL RSS feed
|
|
480
|
+
# ------------------------------------------------------------------ #
|
|
481
|
+
@staticmethod
|
|
482
|
+
def build_rss_url(board_url: str) -> str:
|
|
483
|
+
"""
|
|
484
|
+
Chuyển một URL board Pinterest thông thường thành URL RSS feed.
|
|
485
|
+
|
|
486
|
+
Ví dụ:
|
|
487
|
+
https://www.pinterest.com/username/boardname/
|
|
488
|
+
-> https://www.pinterest.com/username/boardname.rss
|
|
489
|
+
"""
|
|
490
|
+
parsed = urlparse(board_url)
|
|
491
|
+
if "pinterest." not in parsed.netloc:
|
|
492
|
+
raise PinGrabberError(f"URL không hợp lệ, không phải Pinterest: {board_url}")
|
|
493
|
+
|
|
494
|
+
path = parsed.path.strip("/")
|
|
495
|
+
if not path:
|
|
496
|
+
raise PinGrabberError(f"Không tìm thấy đường dẫn board trong URL: {board_url}")
|
|
497
|
+
|
|
498
|
+
if path.endswith(".rss"):
|
|
499
|
+
rss_path = path
|
|
500
|
+
else:
|
|
501
|
+
rss_path = f"{path}.rss"
|
|
502
|
+
|
|
503
|
+
return f"https://{parsed.netloc}/{rss_path}"
|
|
504
|
+
|
|
505
|
+
# ------------------------------------------------------------------ #
|
|
506
|
+
# Bước 2: Lấy nội dung RSS feed
|
|
507
|
+
# ------------------------------------------------------------------ #
|
|
508
|
+
def fetch_rss(self, rss_url: str) -> str:
|
|
509
|
+
"""Gửi request lấy nội dung RSS feed (dạng XML/text)."""
|
|
510
|
+
try:
|
|
511
|
+
resp = self.session.get(rss_url, timeout=self.timeout)
|
|
512
|
+
resp.raise_for_status()
|
|
513
|
+
except requests.RequestException as exc:
|
|
514
|
+
raise PinGrabberError(f"Không thể tải RSS feed: {rss_url} ({exc})") from exc
|
|
515
|
+
return resp.text
|
|
516
|
+
|
|
517
|
+
# ------------------------------------------------------------------ #
|
|
518
|
+
# Bước 3: Parse RSS bằng BeautifulSoup -> lấy danh sách URL ảnh gốc
|
|
519
|
+
# ------------------------------------------------------------------ #
|
|
520
|
+
def extract_image_urls(self, rss_content: str) -> List[str]:
|
|
521
|
+
"""
|
|
522
|
+
Dùng BeautifulSoup để parse XML của RSS feed, lấy tất cả thẻ <img>
|
|
523
|
+
nằm trong mỗi <item><description>, rồi nâng cấp URL thumbnail
|
|
524
|
+
thành URL ảnh gốc ("originals").
|
|
525
|
+
"""
|
|
526
|
+
soup = BeautifulSoup(rss_content, "lxml-xml")
|
|
527
|
+
items = soup.find_all("item")
|
|
528
|
+
|
|
529
|
+
if not items:
|
|
530
|
+
# Một số feed có thể không parse đúng theo "lxml-xml" (hiếm),
|
|
531
|
+
# thử lại bằng parser HTML mặc định như phương án dự phòng.
|
|
532
|
+
soup = BeautifulSoup(rss_content, "html.parser")
|
|
533
|
+
items = soup.find_all("item")
|
|
534
|
+
|
|
535
|
+
image_urls: List[str] = []
|
|
536
|
+
|
|
537
|
+
for item in items:
|
|
538
|
+
description = item.find("description")
|
|
539
|
+
if not description or not description.text:
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
# description chứa HTML dạng escape, parse tiếp lần 2
|
|
543
|
+
desc_soup = BeautifulSoup(description.text, "html.parser")
|
|
544
|
+
img_tags = desc_soup.find_all("img")
|
|
545
|
+
|
|
546
|
+
for img in img_tags:
|
|
547
|
+
src = img.get("src")
|
|
548
|
+
if not src:
|
|
549
|
+
continue
|
|
550
|
+
original_url = self._to_original_quality(src)
|
|
551
|
+
if original_url not in image_urls:
|
|
552
|
+
image_urls.append(original_url)
|
|
553
|
+
|
|
554
|
+
# Phòng trường hợp ảnh nằm trong text thay vì thẻ <img>
|
|
555
|
+
for match in IMG_URL_PATTERN.findall(description.text):
|
|
556
|
+
original_url = self._to_original_quality(match)
|
|
557
|
+
if original_url not in image_urls:
|
|
558
|
+
image_urls.append(original_url)
|
|
559
|
+
|
|
560
|
+
return image_urls
|
|
561
|
+
|
|
562
|
+
@staticmethod
|
|
563
|
+
def _to_original_quality(image_url: str) -> str:
|
|
564
|
+
"""Thay phần kích thước thumbnail (vd: 236x) bằng 'originals'."""
|
|
565
|
+
return THUMBNAIL_SIZE_PATTERN.sub("/originals/", image_url, count=1)
|
|
566
|
+
|
|
567
|
+
# ------------------------------------------------------------------ #
|
|
568
|
+
# Xử lý PIN ĐƠN LẺ (không có RSS, phải fetch trực tiếp trang HTML)
|
|
569
|
+
# ------------------------------------------------------------------ #
|
|
570
|
+
def fetch_pin_page(self, pin_url: str) -> str:
|
|
571
|
+
"""Tải nội dung HTML của trang pin đơn lẻ."""
|
|
572
|
+
try:
|
|
573
|
+
resp = self.session.get(pin_url, timeout=self.timeout)
|
|
574
|
+
resp.raise_for_status()
|
|
575
|
+
except requests.RequestException as exc:
|
|
576
|
+
raise PinGrabberError(f"Không thể tải trang pin: {pin_url} ({exc})") from exc
|
|
577
|
+
return resp.text
|
|
578
|
+
|
|
579
|
+
def extract_single_pin_image(self, html_content: str) -> Optional[str]:
|
|
580
|
+
"""
|
|
581
|
+
Dùng BeautifulSoup để tìm ảnh chính của một pin đơn lẻ.
|
|
582
|
+
|
|
583
|
+
Pinterest nhúng ảnh chính của pin vào thẻ:
|
|
584
|
+
<meta property="og:image" content="https://i.pinimg.com/originals/...">
|
|
585
|
+
Đây thường đã là ảnh chất lượng gốc, không cần nâng cấp thêm.
|
|
586
|
+
Nếu không tìm thấy, thử phương án dự phòng là quét toàn bộ HTML
|
|
587
|
+
bằng regex để tìm URL ảnh i.pinimg.com.
|
|
588
|
+
"""
|
|
589
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
590
|
+
|
|
591
|
+
og_image = soup.find("meta", property="og:image")
|
|
592
|
+
if og_image and og_image.get("content"):
|
|
593
|
+
return self._to_original_quality(og_image["content"])
|
|
594
|
+
|
|
595
|
+
# Phương án dự phòng: quét toàn bộ HTML tìm URL ảnh pinimg.com
|
|
596
|
+
matches = IMG_URL_PATTERN.findall(html_content)
|
|
597
|
+
if matches:
|
|
598
|
+
return self._to_original_quality(matches[0])
|
|
599
|
+
|
|
600
|
+
return None
|
|
601
|
+
|
|
602
|
+
def download_single_pin(self, pin_url: str, output_dir: str = "downloads") -> List[str]:
|
|
603
|
+
"""
|
|
604
|
+
Tải ảnh từ một URL pin đơn lẻ, ví dụ:
|
|
605
|
+
https://www.pinterest.com/pin/119134352618387326/
|
|
606
|
+
|
|
607
|
+
Trả về danh sách đường dẫn file đã lưu (rỗng nếu không tìm thấy ảnh).
|
|
608
|
+
"""
|
|
609
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
610
|
+
|
|
611
|
+
logger.info("Đang tải trang pin: %s", pin_url)
|
|
612
|
+
html_content = self.fetch_pin_page(pin_url)
|
|
613
|
+
|
|
614
|
+
image_url = self.extract_single_pin_image(html_content)
|
|
615
|
+
if not image_url:
|
|
616
|
+
logger.warning("Không tìm thấy ảnh nào trong pin: %s", pin_url)
|
|
617
|
+
return []
|
|
618
|
+
|
|
619
|
+
try:
|
|
620
|
+
path = self.download_image(image_url, output_dir)
|
|
621
|
+
logger.info("Đã tải: %s", path)
|
|
622
|
+
return [path]
|
|
623
|
+
except PinGrabberError as exc:
|
|
624
|
+
logger.error("Lỗi tải ảnh: %s", exc)
|
|
625
|
+
return []
|
|
626
|
+
|
|
627
|
+
# ------------------------------------------------------------------ #
|
|
628
|
+
# Bước 4: Tải ảnh về máy
|
|
629
|
+
# ------------------------------------------------------------------ #
|
|
630
|
+
def download_image(self, image_url: str, output_dir: str) -> str:
|
|
631
|
+
"""Tải một ảnh về thư mục output_dir, trả về đường dẫn file đã lưu."""
|
|
632
|
+
filename = os.path.basename(urlparse(image_url).path) or "image.jpg"
|
|
633
|
+
filepath = os.path.join(output_dir, filename)
|
|
634
|
+
|
|
635
|
+
try:
|
|
636
|
+
resp = self.session.get(image_url, timeout=self.timeout, stream=True)
|
|
637
|
+
resp.raise_for_status()
|
|
638
|
+
except requests.RequestException as exc:
|
|
639
|
+
raise PinGrabberError(f"Không thể tải ảnh: {image_url} ({exc})") from exc
|
|
640
|
+
|
|
641
|
+
with open(filepath, "wb") as f:
|
|
642
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
643
|
+
if chunk:
|
|
644
|
+
f.write(chunk)
|
|
645
|
+
|
|
646
|
+
return filepath
|
|
647
|
+
|
|
648
|
+
# ------------------------------------------------------------------ #
|
|
649
|
+
# Hàm tổng hợp: từ URL board -> tải toàn bộ ảnh gốc về máy
|
|
650
|
+
# ------------------------------------------------------------------ #
|
|
651
|
+
def download(self, url: str, output_dir: str = "downloads") -> List[str]:
|
|
652
|
+
"""
|
|
653
|
+
Quy trình đầy đủ, tự động nhận diện loại URL:
|
|
654
|
+
|
|
655
|
+
- Nếu là URL pin đơn lẻ (pinterest.com/pin/<id>/):
|
|
656
|
+
fetch trực tiếp trang HTML -> lấy ảnh từ og:image -> tải về.
|
|
657
|
+
- Nếu là URL board (pinterest.com/username/boardname/):
|
|
658
|
+
1. Tạo URL RSS từ URL board.
|
|
659
|
+
2. Tải nội dung RSS.
|
|
660
|
+
3. Trích xuất danh sách URL ảnh gốc chất lượng cao.
|
|
661
|
+
4. Tải toàn bộ ảnh về output_dir.
|
|
662
|
+
|
|
663
|
+
Trả về danh sách đường dẫn các file ảnh đã lưu thành công.
|
|
664
|
+
"""
|
|
665
|
+
if self.is_single_pin_url(url):
|
|
666
|
+
logger.info("Phát hiện URL pin đơn lẻ, chuyển sang chế độ tải pin lẻ.")
|
|
667
|
+
return self.download_single_pin(url, output_dir=output_dir)
|
|
668
|
+
|
|
669
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
670
|
+
|
|
671
|
+
rss_url = self.build_rss_url(url)
|
|
672
|
+
logger.info("Đang lấy RSS feed: %s", rss_url)
|
|
673
|
+
|
|
674
|
+
rss_content = self.fetch_rss(rss_url)
|
|
675
|
+
image_urls = self.extract_image_urls(rss_content)
|
|
676
|
+
|
|
677
|
+
if not image_urls:
|
|
678
|
+
logger.warning("Không tìm thấy ảnh nào trong board: %s", url)
|
|
679
|
+
return []
|
|
680
|
+
|
|
681
|
+
logger.info("Tìm thấy %d ảnh. Bắt đầu tải...", len(image_urls))
|
|
682
|
+
|
|
683
|
+
saved_paths: List[str] = []
|
|
684
|
+
for index, img_url in enumerate(image_urls, start=1):
|
|
685
|
+
try:
|
|
686
|
+
path = self.download_image(img_url, output_dir)
|
|
687
|
+
saved_paths.append(path)
|
|
688
|
+
logger.info("(%d/%d) Đã tải: %s", index, len(image_urls), path)
|
|
689
|
+
except PinGrabberError as exc:
|
|
690
|
+
logger.error("(%d/%d) Lỗi tải ảnh: %s", index, len(image_urls), exc)
|
|
691
|
+
|
|
692
|
+
logger.info("Hoàn tất. Đã tải %d/%d ảnh.", len(saved_paths), len(image_urls))
|
|
693
|
+
return saved_paths
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
# ---------------------------------------------------------------------- #
|
|
697
|
+
# Hàm shortcut cấp module để dùng nhanh: pingrabber.download(url)
|
|
698
|
+
# ---------------------------------------------------------------------- #
|
|
699
|
+
def download(url: str, output_dir: str = "downloads", timeout: int = 15) -> List[str]:
|
|
700
|
+
"""
|
|
701
|
+
Hàm tiện ích nhanh, không cần khởi tạo PinGrabber thủ công.
|
|
702
|
+
Tự động nhận diện URL là board hay pin đơn lẻ và xử lý phù hợp.
|
|
703
|
+
|
|
704
|
+
Ví dụ:
|
|
705
|
+
import pingrabber
|
|
706
|
+
|
|
707
|
+
# Board
|
|
708
|
+
pingrabber.download("https://www.pinterest.com/username/boardname/")
|
|
709
|
+
|
|
710
|
+
# Pin đơn lẻ
|
|
711
|
+
pingrabber.download("https://www.pinterest.com/pin/119134352618387326/")
|
|
712
|
+
"""
|
|
713
|
+
grabber = PinGrabber(timeout=timeout)
|
|
714
|
+
return grabber.download(url, output_dir=output_dir)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def search(
|
|
718
|
+
keyword: str,
|
|
719
|
+
max_boards: int = 3,
|
|
720
|
+
max_images_per_board: int = 25,
|
|
721
|
+
max_retries: int = 2,
|
|
722
|
+
delay_seconds: float = 1.5,
|
|
723
|
+
timeout: int = 15,
|
|
724
|
+
) -> List[str]:
|
|
725
|
+
"""
|
|
726
|
+
Hàm tiện ích nhanh: tìm board liên quan đến từ khóa và trả về danh sách
|
|
727
|
+
link ảnh raw (chất lượng gốc), không tải file về máy.
|
|
728
|
+
|
|
729
|
+
Tự động xoay User-Agent, thử lại (retry) và chuyển đổi giữa nhiều
|
|
730
|
+
công cụ tìm kiếm (DuckDuckGo, Bing) nếu bị chặn.
|
|
731
|
+
|
|
732
|
+
Ví dụ:
|
|
733
|
+
import pingrabber
|
|
734
|
+
|
|
735
|
+
links = pingrabber.search("thiên nhiên")
|
|
736
|
+
for url in links:
|
|
737
|
+
print(url)
|
|
738
|
+
|
|
739
|
+
# Tăng số lần thử lại và thời gian nghỉ nếu mạng dễ bị chặn
|
|
740
|
+
links = pingrabber.search("nature", max_retries=4, delay_seconds=3)
|
|
741
|
+
"""
|
|
742
|
+
grabber = PinGrabber(timeout=timeout)
|
|
743
|
+
return grabber.search(
|
|
744
|
+
keyword,
|
|
745
|
+
max_boards=max_boards,
|
|
746
|
+
max_images_per_board=max_images_per_board,
|
|
747
|
+
max_retries=max_retries,
|
|
748
|
+
delay_seconds=delay_seconds,
|
|
749
|
+
)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pingrabber
|
|
3
|
+
Version: 0.1.7
|
|
4
|
+
Summary: A lightweight Python library that scrapes high-quality images from any public Pinterest board using the official RSS feed
|
|
5
|
+
Home-page: https://github.com/VVui-blip/pin_grabber
|
|
6
|
+
Author: VVui-blip
|
|
7
|
+
Author-email: VVui-blip <vuv54581@gmail.com>
|
|
8
|
+
Maintainer-email: VVui-blip <vuv54581@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Project-URL: Homepage, https://github.com/VVui-blip/image-data-scraping-resource-pack-from-Pinterest-
|
|
11
|
+
Project-URL: Repository, https://github.com/VVui-blip/image-data-scraping-resource-pack-from-Pinterest-.git
|
|
12
|
+
Project-URL: Issues, https://github.com/VVui-blip/image-data-scraping-resource-pack-from-Pinterest-/issues
|
|
13
|
+
Project-URL: Documentation, https://github.com/VVui-blip/image-data-scraping-resource-pack-from-Pinterest-/blob/main/README.md
|
|
14
|
+
Keywords: pinterest,scraper,image-downloader,rss,web-scraping
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
25
|
+
Classifier: Topic :: Multimedia :: Graphics :: Capture :: Screen Capture
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Requires-Python: >=3.7
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
Requires-Dist: requests>=2.25.0
|
|
30
|
+
Requires-Dist: beautifulsoup4>=4.9.0
|
|
31
|
+
Requires-Dist: lxml>=4.6.0
|
|
32
|
+
Provides-Extra: search
|
|
33
|
+
Requires-Dist: ddgs>=1.0.0; extra == "search"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
40
|
+
Dynamic: author
|
|
41
|
+
Dynamic: home-page
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
|
|
44
|
+
# PinGrabber
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/pingrabber/)
|
|
47
|
+
[](https://www.python.org/downloads/)
|
|
48
|
+
[](https://opensource.org/licenses/MIT)
|
|
49
|
+
|
|
50
|
+
**PinGrabber** is a lightweight Python library that scrapes high‑quality images from any public Pinterest board using the official RSS feed provided by Pinterest. It extracts the original, full‑resolution images and downloads them to your local machine with minimal effort.
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
> **Important** – Use this tool only with public boards and for personal or educational purposes. Always respect Pinterest’s [Terms of Service](https://www.pinterest.com/terms/) and the copyright of image authors.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Table of Contents
|
|
59
|
+
|
|
60
|
+
- [Features](#features)
|
|
61
|
+
- [Installation](#installation)
|
|
62
|
+
- [Quick Start](#quick-start)
|
|
63
|
+
- [Advanced Usage](#advanced-usage)
|
|
64
|
+
- [How It Works](#how-it-works)
|
|
65
|
+
- [Project Structure](#project-structure)
|
|
66
|
+
- [Dependencies](#dependencies)
|
|
67
|
+
- [License](#license)
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Features
|
|
72
|
+
|
|
73
|
+
- Simple shortcut function – download an entire board or a single pin with one line of code.
|
|
74
|
+
- Automatic conversion of thumbnail URLs to original high‑resolution images.
|
|
75
|
+
- Customizable output directory.
|
|
76
|
+
- Both high‑level wrapper and low‑level class for fine‑grained control.
|
|
77
|
+
- Keyword search that returns raw image URLs without downloading.
|
|
78
|
+
- Built with `requests`, `BeautifulSoup`, and `lxml` for fast and reliable parsing.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
You can install PinGrabber directly from the GitHub repository:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install git+https://github.com/VVui-blip/image-data-scraping-resource-pack-from-Pinterest-.git
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Alternatively, if you have the source code locally:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install -r requirements.txt
|
|
94
|
+
pip install .
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The required dependencies (requests, beautifulsoup4, lxml) will be installed automatically.
|
|
98
|
+
|
|
99
|
+
Optional dependency for better keyword search
|
|
100
|
+
|
|
101
|
+
For a more robust and stable keyword search, we recommend installing the ddgs package (community‑maintained, formerly duckduckgo-search):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install ddgs
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Or install it together with PinGrabber:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install .[search]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
ddgs provides a more reliable way to query search engines (DuckDuckGo, Bing, Google, etc.) and supports proxy configuration right in the code. Without ddgs, the search() function will fall back to direct requests calls to search engines, which are more prone to blocking.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Quick Start
|
|
118
|
+
|
|
119
|
+
pingrabber.download(url) automatically detects whether the given URL is a board or a single pin and handles it accordingly – you don't need to differentiate manually.
|
|
120
|
+
|
|
121
|
+
Download all images from a board
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import pingrabber
|
|
125
|
+
|
|
126
|
+
pingrabber.download("https://www.pinterest.com/username/boardname/")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Download a single pin
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
import pingrabber
|
|
133
|
+
|
|
134
|
+
pingrabber.download("https://www.pinterest.com/pin/119134352618387326/")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
To save images to a custom directory:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import pingrabber
|
|
141
|
+
|
|
142
|
+
pingrabber.download(
|
|
143
|
+
"https://www.pinterest.com/username/boardname/",
|
|
144
|
+
output_dir="my_pinterest_images"
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Search images by keyword (returns raw links, no download)
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
import pingrabber
|
|
152
|
+
|
|
153
|
+
links = pingrabber.search("nature")
|
|
154
|
+
for url in links:
|
|
155
|
+
print(url)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This function does not download any images – it only returns a list of high‑quality raw image URLs. You can preview them, filter, or download them manually with requests if needed.
|
|
159
|
+
|
|
160
|
+
How it works: search() tries multiple search engines (DuckDuckGo HTML, DuckDuckGo Lite, Bing) using the site:pinterest.com <keyword> query. It rotates user‑agents, retries, and adds delays between attempts to reduce the chance of being blocked. If one engine fails (403/429), it automatically switches to the next. It does not scrape Pinterest’s search page directly, because that page requires JavaScript rendering which requests cannot handle.
|
|
161
|
+
|
|
162
|
+
Customise the number of boards to scan, images per board, retries, and delay:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
links = pingrabber.search(
|
|
166
|
+
"nature",
|
|
167
|
+
max_boards=5,
|
|
168
|
+
max_images_per_board=10,
|
|
169
|
+
max_retries=3,
|
|
170
|
+
delay_seconds=2.5
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
If search() always returns empty: check the logged WARNING/ERROR messages. If you see 403/429 errors for all fallback engines, your network/IP is likely being rate‑limited by the search engines (common on cloud servers, VPNs, or IPs that have sent many requests). In that case:
|
|
175
|
+
|
|
176
|
+
· Install ddgs if you haven’t (pip install ddgs) – this is the most significant improvement.
|
|
177
|
+
· If ddgs is installed but still returns nothing, try using a proxy directly with the package:
|
|
178
|
+
```python
|
|
179
|
+
from ddgs import DDGS
|
|
180
|
+
with DDGS(proxy="socks5://127.0.0.1:9050", timeout=15) as ddgs:
|
|
181
|
+
results = ddgs.text("site:pinterest.com nature", max_results=5)
|
|
182
|
+
print(results)
|
|
183
|
+
```
|
|
184
|
+
If that returns results, you can initialise PinGrabber with a similarly proxied session, or simply use the found board URLs with download().
|
|
185
|
+
· Increase max_retries and delay_seconds (this only affects the fallback method).
|
|
186
|
+
· Try a different network/VPN.
|
|
187
|
+
· Alternatively, use the most reliable approach: find a board manually through your browser and call pingrabber.download(board_url) directly – this does not depend on search engines and is always stable.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Advanced Usage
|
|
192
|
+
|
|
193
|
+
For more control, use the PinGrabber class:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from pingrabber import PinGrabber
|
|
197
|
+
|
|
198
|
+
grabber = PinGrabber(timeout=30)
|
|
199
|
+
|
|
200
|
+
# Download all original images
|
|
201
|
+
saved_files = grabber.download(
|
|
202
|
+
"https://www.pinterest.com/username/boardname/",
|
|
203
|
+
output_dir="high_res_pins"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
print(f"Downloaded {len(saved_files)} images")
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
If you only need the image URLs (without downloading):
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from pingrabber import PinGrabber
|
|
213
|
+
|
|
214
|
+
grabber = PinGrabber()
|
|
215
|
+
rss_url = grabber.build_rss_url("https://www.pinterest.com/username/boardname/")
|
|
216
|
+
rss_content = grabber.fetch_rss(rss_url)
|
|
217
|
+
image_urls = grabber.extract_image_urls(rss_content)
|
|
218
|
+
|
|
219
|
+
for url in image_urls:
|
|
220
|
+
print(url)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## How It Works
|
|
226
|
+
|
|
227
|
+
1. Board/Pin URL to RSS Feed – The provided URL is converted to an RSS feed URL (appending .rss for boards, or using the pin’s RSS endpoint).
|
|
228
|
+
2. Fetch RSS – The RSS content is retrieved via a requests GET request.
|
|
229
|
+
3. Parse and Extract – BeautifulSoup with the lxml parser extracts all <img> tags inside the RSS items.
|
|
230
|
+
4. Upgrade to Original – Thumbnail URLs (e.g., 236x) are transformed into originals URLs to fetch the highest available quality.
|
|
231
|
+
5. Download – Each image is downloaded and saved to the specified output directory with a unique filename.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Project Structure
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
pin_grabber/
|
|
239
|
+
├── pingrabber/
|
|
240
|
+
│ ├── __init__.py # Package entry point
|
|
241
|
+
│ └── core.py # Main logic (PinGrabber class + helper functions)
|
|
242
|
+
├── requirements.txt # Python dependencies
|
|
243
|
+
├── README.md # This file
|
|
244
|
+
├── setup.py # Packaging configuration
|
|
245
|
+
└── LICENSE # MIT License
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Dependencies
|
|
251
|
+
|
|
252
|
+
· Python 3.7+
|
|
253
|
+
· requests – HTTP requests.
|
|
254
|
+
· beautifulsoup4 – HTML/XML parsing.
|
|
255
|
+
· lxml – Fast XML/HTML parser.
|
|
256
|
+
|
|
257
|
+
All dependencies are listed in requirements.txt and will be installed when using pip install . or the Git install command.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
This project is released under the MIT License. See the LICENSE file for details.
|
|
264
|
+
|
|
265
|
+
Disclaimer: This tool is provided “as is”. You are solely responsible for ensuring that your usage complies with Pinterest’s Terms of Service and applicable copyright laws.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
Built with attention for developers who need a quick, clean Pinterest image scraper.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pingrabber/__init__.py,sha256=3cNX-r8mewlRs5OPu5Kd8IG7yJFOvbqvodGX9Gun6yw,624
|
|
2
|
+
pingrabber/core.py,sha256=HqbwyfUpQIjXu3f0xFFzldC_VnHYvfkdTLoHvMRJRE4,31453
|
|
3
|
+
pingrabber-0.1.7.dist-info/METADATA,sha256=VN4rHDng8fPo5FGYrNpYPhoAJ86GnFV8l0jqNPxxGvA,9851
|
|
4
|
+
pingrabber-0.1.7.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
pingrabber-0.1.7.dist-info/entry_points.txt,sha256=Q3icyZV7pv0xwDk69vA8nIN-cJN2083fYF714gVj8Eo,51
|
|
6
|
+
pingrabber-0.1.7.dist-info/top_level.txt,sha256=inAN9tNeeGLwL32Yn9wucNDQqIZWuExlW6wb02X2e48,11
|
|
7
|
+
pingrabber-0.1.7.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pingrabber
|