diff --git a/dist/google-lens-python-2023.3.18.tar.gz b/dist/google-lens-python-2023.3.18.tar.gz new file mode 100644 index 0000000..c8955b8 Binary files /dev/null and b/dist/google-lens-python-2023.3.18.tar.gz differ diff --git a/google_lens_python.egg-info/PKG-INFO b/google_lens_python.egg-info/PKG-INFO new file mode 100644 index 0000000..50c37ce --- /dev/null +++ b/google_lens_python.egg-info/PKG-INFO @@ -0,0 +1,13 @@ +Metadata-Version: 2.1 +Name: google-lens-python +Version: 2023.3.18 +Summary: A Python package to reverse image search in Google Lens +Author: Anhy Krishna Fitiavana +Author-email: fitiavana.krishna@gmail.com +Keywords: python,google,scraping +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python :: 3 +Classifier: Operating System :: OS Independent + +A Python package to reverse image search in Google Lens, with the ability to search by file path or by url. diff --git a/google_lens_python.egg-info/SOURCES.txt b/google_lens_python.egg-info/SOURCES.txt new file mode 100644 index 0000000..320fb3d --- /dev/null +++ b/google_lens_python.egg-info/SOURCES.txt @@ -0,0 +1,9 @@ +README.md +setup.py +google_lens_python.egg-info/PKG-INFO +google_lens_python.egg-info/SOURCES.txt +google_lens_python.egg-info/dependency_links.txt +google_lens_python.egg-info/requires.txt +google_lens_python.egg-info/top_level.txt +googlelens/__init__.py +googlelens/googlelens.py \ No newline at end of file diff --git a/google_lens_python.egg-info/dependency_links.txt b/google_lens_python.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/google_lens_python.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/google_lens_python.egg-info/requires.txt b/google_lens_python.egg-info/requires.txt new file mode 100644 index 0000000..166da77 --- /dev/null +++ b/google_lens_python.egg-info/requires.txt @@ -0,0 +1,2 @@ +beautifulsoup4>=4.15.0 +requests>=2.34.2 diff --git a/google_lens_python.egg-info/top_level.txt b/google_lens_python.egg-info/top_level.txt new file mode 100644 index 0000000..87f42b5 --- /dev/null +++ b/google_lens_python.egg-info/top_level.txt @@ -0,0 +1 @@ +googlelens diff --git a/googlelens/googlelens.py b/googlelens/googlelens.py index f8f2e0c..74b31ac 100644 --- a/googlelens/googlelens.py +++ b/googlelens/googlelens.py @@ -1,5 +1,6 @@ import re import json +from typing import Any, Optional from requests import Session from bs4 import BeautifulSoup @@ -32,22 +33,30 @@ def __get_prerender_script(self, page: str): soup = BeautifulSoup(page, 'html.parser') # Find the script containing 'AF_initDataCallback' with specific key and hash values - prerender_script = list(filter( - lambda s: ( - 'AF_initDataCallback(' in s.text and - re.search(r"key: 'ds:(\d+)'", s.text).group(1) == "0"), - soup.find_all('script') - ))[0].text + prerender_script: Optional[str] = None + for script in soup.find_all('script'): + if script.text is None or 'AF_initDataCallback(' not in script.text: + continue + key_match = re.search(r"key: 'ds:(\d+)'", script.text) + if key_match and key_match.group(1) == "0": + prerender_script = script.text + break + + if prerender_script is None: + raise ValueError("Unable to find Google Lens prerender data in response.") # Clean up the script content to prepare it for JSON parsing prerender_script = prerender_script.replace( "AF_initDataCallback(", "").replace(");", "") # Extract hash value and replace the corresponding fields in the script for JSON formatting - hash = re.search(r"hash: '(\d+)'", prerender_script).group(1) + hash_match = re.search(r"hash: '(\d+)'", prerender_script) + if hash_match is None: + raise ValueError("Unable to parse Google Lens prerender hash.") + script_hash = hash_match.group(1) prerender_script = prerender_script.replace( - f"key: 'ds:0', hash: '{hash}', data:", - f"\"key\": \"ds:0\", \"hash\": \"{hash}\", \"data\":" + f"key: 'ds:0', hash: '{script_hash}', data:", + f"\"key\": \"ds:0\", \"hash\": \"{script_hash}\", \"data\":" ).replace("sideChannel:", "\"sideChannel\":") # Parse the cleaned prerender script into a JSON object @@ -56,6 +65,17 @@ def __get_prerender_script(self, page: str): # Return the relevant data section for further processing return prerender_script['data'][1] + @staticmethod + def __safe_get(value: Any, path: list[int]) -> Any: + current = value + for index in path: + if not isinstance(current, list) or not isinstance(index, int): + return None + if index < 0 or index >= len(current): + return None + current = current[index] + return current + def __parse_prerender_script(self, prerender_script): """ Parses the prerendered script to extract match and similar items. @@ -73,58 +93,58 @@ def __parse_prerender_script(self, prerender_script): } # Extract the best match information if available - try: + match_title = self.__safe_get(prerender_script, [0, 1, 8, 12, 0, 0, 0]) + match_thumbnail = self.__safe_get(prerender_script, [0, 1, 8, 12, 0, 2, 0, 0]) + match_page_url = self.__safe_get(prerender_script, [0, 1, 8, 12, 0, 2, 0, 4]) + if isinstance(match_title, str) and isinstance(match_thumbnail, str) and isinstance(match_page_url, str): data["match"] = { - "title": prerender_script[0][1][8][12][0][0][0], # Extract item title - "thumbnail": prerender_script[0][1][8][12][0][2][0][0], # Extract thumbnail URL - "pageURL": prerender_script[0][1][8][12][0][2][0][4] # Extract page URL + "title": match_title, # Extract item title + "thumbnail": match_thumbnail, # Extract thumbnail URL + "pageURL": match_page_url # Extract page URL } - except IndexError: - # If data is unavailable, continue without a match - pass # Determine which section to use for extracting visual matches if data["match"] is not None: - visual_matches = prerender_script[1][1][8][8][0][12] + visual_matches = self.__safe_get(prerender_script, [1, 1, 8, 8, 0, 12]) else: - try: - visual_matches = prerender_script[0][1][8][8][0][12] - except IndexError: - return data + visual_matches = self.__safe_get(prerender_script, [0, 1, 8, 8, 0, 12]) + + if not isinstance(visual_matches, list): + return data # Iterate through the visual matches and extract relevant details for match in visual_matches: # Safely extract thumbnail URL if available - thumbnail_url = match[0][0] if ( - isinstance(match[0], list) and len(match[0]) > 0 and - isinstance(match[0][0], str) - ) else None + thumbnail_url = self.__safe_get(match, [0, 0]) + if not isinstance(thumbnail_url, str): + thumbnail_url = None # Safely extract price if available - price = match[0][7][1] if ( - isinstance(match[0], list) and len(match[0]) > 7 and - isinstance(match[0][7], list) and len(match[0][7]) > 1 and - isinstance(match[0][7][1], str) - ) else None + price = self.__safe_get(match, [0, 7, 1]) + if not isinstance(price, str): + price = None # Clean price by removing any special characters (e.g., currency signs) price = re.sub(r"[^\d.]", "", price) if price is not None else None # Safely extract currency if available - currency = match[0][7][5] if ( - isinstance(match[0], list) and len(match[0]) > 7 and - isinstance(match[0][7], list) and len(match[0][7]) > 5 and - isinstance(match[0][7][5], str) - ) else None + currency = self.__safe_get(match, [0, 7, 5]) + if not isinstance(currency, str): + currency = None + + title = self.__safe_get(match, [3]) + similarity_score = self.__safe_get(match, [1]) + page_url = self.__safe_get(match, [5]) + source_website = self.__safe_get(match, [14]) # Append the extracted information to the "similar" matches list data["similar"].append( { - "title": match[3], # Extract item title - "similarity score": match[1], # Extract similarity (?) score + "title": title, # Extract item title + "similarity score": similarity_score, # Extract similarity (?) score "thumbnail": thumbnail_url, # Thumbnail URL - "pageURL": match[5], # Extract page URL - "sourceWebsite": match[14], # Extract source website name + "pageURL": page_url, # Extract page URL + "sourceWebsite": source_website, # Extract source website name "price": price, # Price (cleaned) "currency": currency # Currency symbol } @@ -143,24 +163,26 @@ def search_by_file(self, file_path: str): Returns: The parsed search results after extracting and processing the response. """ - multipart = { - 'encoded_image': (file_path, open(file_path, 'rb')), - 'image_content': '' - } + with open(file_path, 'rb') as image_file: + multipart = { + 'encoded_image': (file_path, image_file), + 'image_content': '' + } - # Build the parameter dictionary - params = { - "hl": "en", # Adjust host language here - "gl": "us", # Adjust the geolocation parameter here - } - - # Send a POST request to upload the file - response = self.session.post( - self.url + "/upload", - files=multipart, - params=params, - allow_redirects=False # Must be false to capture the 302 response - ) + # Build the parameter dictionary + params = { + "hl": "en", # Adjust host language here + "gl": "us", # Adjust the geolocation parameter here + } + + # Send a POST request to upload the file + response = self.session.post( + self.url + "/upload", + files=multipart, + params=params, + allow_redirects=False, # Must be false to capture the 302 response + timeout=30 + ) # Check if the request was successful if response.status_code != 302: # Expecting a 302 for redirect @@ -177,7 +199,7 @@ def search_by_file(self, file_path: str): return None # Or handle the error appropriately # Proceed with the redirect - response = self.session.get(search_url) + response = self.session.get(search_url, timeout=30) # Extract the prerendered JavaScript content for further parsing. prerender_script = self.__get_prerender_script(response.text) @@ -205,7 +227,8 @@ def search_by_url(self, url: str): response = self.session.get( self.url + "/uploadbyurl", params=params, - allow_redirects=True + allow_redirects=True, + timeout=30 ) # Extract the prerendered JavaScript content for further parsing diff --git a/requirements.txt b/requirements.txt index a98ae43..a7fa2d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -requests -beautifulsoup4 \ No newline at end of file +requests>=2.34.2 +beautifulsoup4>=4.15.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 99c89b1..63cc93e 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ description=DESCRIPTION, long_description=LONG_DESCRIPTION, packages=find_packages(), - install_requires=['requests', 'bs4'], + install_requires=['requests>=2.34.2', 'beautifulsoup4>=4.15.0'], keywords=['python', 'google', 'scraping'], classifiers=[ "Development Status :: 5 - Production/Stable",