Google Patents tidak punya API resmi. Jalur berbayarnya adalah SerpAPI, sedangkan yang resmi dan gratis masing-masing hanya mencakup satu kantor paten, seperti PatentsView milik USPTO untuk paten AS. Tetapi situs Google Patents memuat hasilnya dari endpoint JSON, dan endpoint itu menjawab permintaan HTTP biasa tanpa key. Pencarian paten di workspace Paper pada aplikasi desktop osFoundry memakainya dari Rust. Di bawah ini permintaan yang sama dalam Python dengan library standar saja, filter yang diterimanya, seperti apa responsnya, dan apa yang terjadi setelah lima query dalam setengah menit.

Request-nya adalah query string di dalam query string

Endpoint-nya adalah https://patents.google.com/xhr/query. Ia hanya menerima satu parameter sungguhan, url, yang nilainya sendiri adalah query string yang sudah di-encode: sama dengan yang terlihat setelah tanda ? saat kamu mencari di situs. Jadi kata kunci pencarian di-encode dua kali.

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"] 

Tanda kurung di sekitar query juga dikirim oleh situsnya. Tanpa user agent browser, endpoint lebih mungkin menolak, jadi aplikasi mengirimkannya, dan fungsi ini juga. Pemeriksaan terakhir sebelum parsing dijelaskan di bagian bawah.

Filter

Filter dimasukkan ke query string bagian dalam, di samping q. Ini filter yang diteruskan aplikasi, beserta jumlah hasil untuk water bottle glows reminder to drink:

parameter dalam contoh hasil
(tanpa) 2757
status GRANT 685
status APPLICATION 1739
country JP 157
before / after priority:20190101 tidak diukur
sort new atau old tidak diukur
num / page 20 / 1 tidak diukur
results = search("water bottle glows reminder to drink", status="GRANT") print(results["total_num_results"]) # 685 

Ada dua hal pada angka-angka itu yang mudah salah dipahami. Pertama, 685 paten yang diberikan ditambah 1739 permohonan adalah 2424, bukan 2757, jadi 333 dokumen bukan keduanya dan kedua filter itu tidak membagi seluruh himpunan. Kedua, angkanya menghitung dokumen, bukan penemuan. Penemuan yang sama biasanya muncul sekali sebagai permohonan yang dipublikasikan dan sekali lagi sebagai paten yang diberikan. "Smart container with interactive, colored lights" ada di daftar permohonan sebagai US20220218137A1 dan di daftar yang diberikan sebagai US12082729B2, dengan judul yang sama dan tanggal prioritas yang sama, 2019-04-30. Kalau ingin menghitung penemuan, kelompokkan menurut tanggal prioritas dan judul, atau menurut keluarga paten, sebelum menghitung.

Filter tanggal membandingkan dengan tanggal prioritas, yaitu tanggal yang penting untuk prior art. Aplikasi hanya meloloskan tanggal 8 digit, dan hanya nilai persis untuk country, status, dan sort di atas, karena string ini disisipkan ke dalam 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)> { 

Membaca respons

Hasilnya bersarang dua tingkat: results.cluster[].result[].patent. Setiap patent punya title, snippet, priority_date, filing_date, grant_date, publication_date, inventor, assignee, publication_number, language, path thumbnail, dan daftar figures.

Field teksnya di-escape sebagai HTML, bukan teks biasa. Judul datang dengan spasi di depan (' Liquid intake management system'), apostrof datang sebagai &#39;, dan snippet berakhir dengan &hellip;. Kode Rust aplikasi juga membuang tag, karena Google bisa membungkus kata yang cocok dengan <b>. Satu regex dan html.unescape menyelesaikan semuanya:

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 ... 

Paten Jepang dan Rusia datang dengan judul berbahasa Inggris karena Google menerjemahkannya secara otomatis. Nama pemegang paten tidak diterjemahkan: pemegang JP6067764B2 masih dalam bahasa Jepang. Hasil keempat adalah generator uap untuk rokok elektrik, yang menunjukkan betapa longgarnya pencocokan. Kata biasa bukan pencarian frasa persis, jadi bacalah hasil teratas dan perbaiki kata kuncinya, jangan menelan angka totalnya begitu saja.

Gambar berupa path relatif terhadap https://patentimages.storage.googleapis.com/. Gambar kedua dari US12082729B2 ada di e6/88/67/673b4fcbbe5404/US12082729-20240910-D00001.png, dan host itu menyajikannya untuk request biasa:

FIG. 1A dari US12082729B2, diunduh dari patentimages.storage.googleapis.com

Respons juga memuat total_num_pages dan blok summary yang menghitung pemegang paten, inventor, dan kelas CPC per tahun. Keduanya berhenti di 1000 dokumen: "Total" pada summary adalah 1000 untuk pencarian dengan 2757 hasil, dan total_num_pages adalah 100 dengan 10 hasil per halaman. Tanpa num, satu halaman berisi 10 hasil. Aplikasi meminta 20 dan membatasi nilainya antara 10 dan 100.

Apa yang terjadi kalau terlalu banyak mengirim

Skrip uji mengantrekan enam query dengan jeda 4 detik di antaranya. Empat yang pertama dijawab dalam 2.8 sampai 4.5 detik masing-masing. Yang kelima, sekitar 30 detik setelah yang pertama, kembali sebagai HTTP 500 dengan halaman HTML. Percobaan ulang semenit kemudian mendapat HTTP 503 dan halaman "Sorry..." dari Google, begitu juga semua percobaan berikutnya, yang terakhir hampir 14 menit setelah penolakan pertama. Query keenam tidak pernah dijalankan.

Jadi pemblokirannya bukan error JSON yang bisa ditangkap hanya dengan memeriksa status code. Bisa 503 atau 429, dan di sini yang pertama bahkan 500, tetapi dalam semua kasus isinya halaman HTML. Itulah sebabnya fungsi di atas memeriksa apakah isi respons diawali < sebelum di-parse. Aplikasi melakukan hal yang sama, dan bila pengguna punya key SerpAPI, aplikasi mengirim query yang sama lewat SerpAPI alih-alih gagal:

// 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); } 

Untuk sebuah skrip, aturan praktisnya: simpan setiap respons di cache berdasarkan query string dalamnya, kirim satu query dalam satu waktu, dan berhenti pada respons HTML pertama alih-alih mengulang dalam loop. Endpoint ini tidak terdokumentasi, jadi bentuknya bisa berubah. Untuk keperluan rutin atau besar, sumber resmi (PatentsView untuk AS, Open Patent Services milik EPO untuk Eropa, API informasi paten Kantor Paten Jepang untuk Jepang) atau SerpAPI adalah pilihan yang lebih aman.

Aplikasi desktop yang membuat request ini gratis di halaman unduh osFoundry.