Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added dist/google-lens-python-2023.3.18.tar.gz
Binary file not shown.
13 changes: 13 additions & 0 deletions google_lens_python.egg-info/PKG-INFO
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions google_lens_python.egg-info/SOURCES.txt
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +1 to +5
google_lens_python.egg-info/requires.txt
google_lens_python.egg-info/top_level.txt
googlelens/__init__.py
googlelens/googlelens.py
1 change: 1 addition & 0 deletions google_lens_python.egg-info/dependency_links.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

2 changes: 2 additions & 0 deletions google_lens_python.egg-info/requires.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
beautifulsoup4>=4.15.0
requests>=2.34.2
1 change: 1 addition & 0 deletions google_lens_python.egg-info/top_level.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
googlelens
139 changes: 81 additions & 58 deletions googlelens/googlelens.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
import json
from typing import Any, Optional
from requests import Session
from bs4 import BeautifulSoup

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
}
Expand All @@ -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': ''
}
Comment on lines +167 to +170

# 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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
requests
beautifulsoup4
requests>=2.34.2
beautifulsoup4>=4.15.0
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down