Google Patents का कोई आधिकारिक API नहीं है। पैसे वाला रास्ता SerpAPI है, और मुफ़्त आधिकारिक API हर एक सिर्फ एक पेटेंट ऑफिस को कवर करता है, जैसे अमेरिकी पेटेंट के लिए USPTO का PatentsView। लेकिन Google Patents की वेबसाइट अपने नतीजे एक JSON endpoint से लोड करती है, और वह endpoint बिना key के सादे HTTP request का जवाब दे देता है। डेस्कटॉप ऐप osFoundry के Paper workspace का पेटेंट सर्च इसे Rust से इस्तेमाल करता है। नीचे वही request सिर्फ standard library के साथ Python में है, साथ में वे फ़िल्टर जो यह मानता है, response कैसा दिखता है, और आधे मिनट में पाँच query भेजने के बाद क्या होता है।
Request एक query string के अंदर एक query string है
Endpoint है https://patents.google.com/xhr/query। यह एक ही असली parameter लेता है, url, जिसकी value खुद एक encoded query string है: वही जो वेबसाइट पर सर्च करने पर ? के बाद दिखती है। इसलिए सर्च शब्द दो बार encode होते हैं।
import html, json, re, urllib.error, urllib.parse, urllib.request
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
def search(query, **filters):
inner = urllib.parse.urlencode({"q": f"({query})", **filters})
url = "https://patents.google.com/xhr/query?" + urllib.parse.urlencode({"url": inner, "exp": ""})
req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "application/json"})
try:
with urllib.request.urlopen(req, timeout=30) as r:
status, text = r.status, r.read().decode()
except urllib.error.HTTPError as e:
status, text = e.code, e.read().decode(errors="replace")
if status in (429, 503) or text.lstrip().startswith("<"):
raise RuntimeError(f"rate limited ({status})")
return json.loads(text)["results"]
Query के आसपास की parentheses वेबसाइट भी भेजती है। ब्राउज़र user agent के बिना endpoint के मना कर देने की संभावना ज़्यादा है, इसलिए ऐप एक भेजता है, और यह फ़ंक्शन भी। parse करने से पहले की आखिरी जाँच आगे समझाई गई है।
फ़िल्टर
फ़िल्टर अंदर वाली query string में, q के बगल में जाते हैं। ऐप जो फ़िल्टर आगे भेजता है वे ये हैं, water bottle glows reminder to drink के नतीजों की गिनती के साथ:
| अंदर का parameter | उदाहरण | नतीजे |
|---|---|---|
| (कोई नहीं) | 2,757 | |
status |
GRANT |
685 |
status |
APPLICATION |
1,739 |
country |
JP |
157 |
before / after
|
priority:20190101 |
मापा नहीं गया |
sort |
new या old
|
मापा नहीं गया |
num / page
|
20 / 1
|
मापा नहीं गया |
results = search("water bottle glows reminder to drink", status="GRANT")
print(results["total_num_results"]) # 685
इन आँकड़ों में दो बातें गलत समझना आसान है। पहली, 685 granted और 1,739 applications मिलकर 2,424 होते हैं, 2,757 नहीं, यानी 333 दस्तावेज़ न इसमें हैं न उसमें, और दोनों फ़िल्टर पूरे सेट को बाँटते नहीं। दूसरी, गिनती दस्तावेज़ों की है, आविष्कारों की नहीं। एक ही आविष्कार आमतौर पर एक बार published application के रूप में और एक बार grant के रूप में आता है। "Smart container with interactive, colored lights" application की सूची में US20220218137A1 है और granted की सूची में US12082729B2, दोनों का title और priority date एक ही है, 2019-04-30। आविष्कार गिनने हों तो गिनने से पहले priority date और title से, या family से, group कर लें।
तारीख वाले फ़िल्टर priority date से तुलना करते हैं, जो prior art के लिए मायने रखती है। ऐप सिर्फ 8 अंकों की तारीख और ऊपर दिए country, status और sort की सटीक values ही आगे जाने देता है, क्योंकि ये strings URL में जोड़ी जाती हैं:
/// Filter values are spliced into upstream query strings, so only the exact
/// shapes each upstream documents are passed through.
fn filter_params(filters: &PatentFilters) -> Vec<(&'static str, String)> {
Response पढ़ना
नतीजे दो स्तर अंदर नेस्टेड हैं: results.cluster[].result[].patent। हर patent में title, snippet, priority_date, filing_date, grant_date, publication_date, inventor, assignee, publication_number, language, एक thumbnail path और figures की सूची होती है।
टेक्स्ट वाले फ़ील्ड HTML-escaped होते हैं, सादा टेक्स्ट नहीं। Title की शुरुआत में एक space आता है (' Liquid intake management system'), apostrophe ' बनकर आते हैं, और snippet … पर खत्म होते हैं। ऐप का Rust tags भी हटाता है, क्योंकि Google मिले हुए शब्दों को <b> में लपेट सकता है। एक regex और html.unescape सब संभाल लेते हैं:
def plain(v):
return " ".join(html.unescape(re.sub(r"<[^>]*>", "", v or "")).split())
def hits(results):
for cluster in results["cluster"]:
for hit in cluster["result"]:
yield hit["patent"]
for p in hits(search("water bottle glows reminder to drink", status="GRANT")):
print(p["publication_number"], p["priority_date"], plain(p["title"]))
US12564280B2 2015-03-09 Wireless drink container for monitoring hydration
US12082729B2 2019-04-30 Smart container with interactive, colored lights
JP6067764B2 2013-04-03 Liquid intake management system
RU2707893C2 2014-02-28 Personal steam generator for electronic cigarette
US11581099B1 2017-05-24 Automated home system for senior care
...
जापानी और रूसी पेटेंट अंग्रेज़ी title के साथ आते हैं क्योंकि Google उनका machine translation करता है। assignee का अनुवाद नहीं होता: JP6067764B2 का assignee अब भी जापानी में है। चौथा नतीजा इलेक्ट्रॉनिक सिगरेट के लिए steam generator है, जो दिखाता है कि मिलान कितना ढीला है। सादे शब्द exact phrase सर्च नहीं हैं, इसलिए सबसे ऊपर के नतीजे पढ़ें और शब्द बदलकर दोबारा देखें, कुल गिनती को ज्यों का त्यों न मानें।
तस्वीरें https://patentimages.storage.googleapis.com/ के relative path होती हैं। US12082729B2 की दूसरी figure e6/88/67/673b4fcbbe5404/US12082729-20240910-D00001.png पर है, और वह host उसे सादे request पर दे देता है:
Response में total_num_pages और एक summary block भी होता है जो assignees, inventors और CPC classes को साल के हिसाब से गिनता है। दोनों 1,000 दस्तावेज़ों पर रुक जाते हैं: 2,757 नतीजों वाली सर्च के लिए summary का "Total" 1,000 था, और 10 नतीजे प्रति पेज पर total_num_pages 100 था। num के बिना एक पेज में 10 नतीजे आते हैं। ऐप 20 माँगता है और value को 10 से 100 के बीच रखता है।
बहुत ज़्यादा भेजने पर क्या होता है
एक टेस्ट स्क्रिप्ट ने छह query को उनके बीच 4 सेकंड के अंतराल के साथ कतार में लगाया। पहली चार का जवाब हर एक में 2.8 से 4.5 सेकंड में आया। पाँचवीं, पहली के करीब 30 सेकंड बाद, HTML पेज के साथ HTTP 500 बनकर लौटी। एक मिनट बाद दोबारा कोशिश करने पर HTTP 503 और Google का "Sorry..." पेज मिला, और बाद की हर कोशिश पर भी वही, आखिरी कोशिश पहले इनकार के लगभग 14 मिनट बाद की थी। छठी query कभी चली ही नहीं।
यानी यह रोक ऐसी JSON error नहीं है जिसे सिर्फ status code जाँचकर पकड़ा जा सके। यह 503 या 429 हो सकती है, और यहाँ पहली तो 500 भी थी, लेकिन हर मामले में body एक HTML पेज थी। इसीलिए ऊपर का फ़ंक्शन parse करने से पहले देखता है कि body < से शुरू होती है या नहीं। ऐप भी यही करता है, और जब यूज़र के पास SerpAPI key होती है, तो वह फेल होने की जगह वही query SerpAPI से भेज देता है:
// The block page is HTML served as 503 (sometimes 429).
if status.as_u16() == 429 || status.as_u16() == 503 || text.trim_start().starts_with('<') {
return Err(GoogleError::Limited);
}
स्क्रिप्ट के लिए व्यावहारिक नियम ये हैं: हर response को उसकी अंदर वाली query string से cache करें, एक बार में एक ही query भेजें, और पहले HTML response पर रुक जाएँ, लूप में दोबारा कोशिश न करते रहें। Endpoint undocumented है, इसलिए उसका रूप बदल सकता है। नियमित या बड़े काम के लिए आधिकारिक स्रोत (अमेरिका के लिए PatentsView, यूरोप के लिए EPO की Open Patent Services, जापान के लिए जापान पेटेंट ऑफिस की patent information API) या SerpAPI ज़्यादा सुरक्षित विकल्प है।
यह request करने वाला डेस्कटॉप ऐप osFoundry के डाउनलोड पेज से मुफ़्त है।

Top comments (0)