Last active
December 14, 2021 15:25
-
-
Save philipjewell/1090bf397a3965cc7a60497b486b369a to your computer and use it in GitHub Desktop.
Finding which Walmart locations nearby have inventory of a product
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
from datetime import datetime | |
import click | |
import random | |
import requests | |
BASE_URL = "https://www.walmart.com/grocery" | |
# Current Chrome user agent on Mac | |
USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36" | |
item_dict = { | |
'ps5': [987823383, 165545420], # one is the digital, the other is the disc | |
'bblr': [605417669, 355449356, 261945169], | |
} | |
class WalmartGroceryInventory(): | |
def __init__(self, zipcode, number_of_stores=10, user_agent=USER_AGENT, verbose=False): | |
self.zipcode = zipcode | |
self.user_agent = user_agent | |
self.stores = self.get_stores(number_of_stores) | |
self.store_ids = list(self.stores.keys()) | |
self.verbose = verbose | |
def walmart_request(self, endpoint, base_url=BASE_URL): | |
"""Request wrapper for Walmart api requests. | |
:param endpoint: str, the URI for the request | |
:return response: dict | |
""" | |
# TODO: Add retry wrapper that changes the user agent? | |
headers = {'content-type': 'application/json', "User-Agent": self.user_agent} | |
try: | |
response = requests.get(f"{base_url}/{endpoint}", headers=headers).json() | |
except: | |
print("Request did not work, sorry.") | |
return False | |
return response | |
def get_stores(self, n): | |
"""Build a dictionary with store data based on the zipcode. | |
:param n: number of entries we are looking at | |
:return store: dict, {store_id: store_address | |
}""" | |
endpoint = f'v4/api/serviceAvailability?postalCode={self.zipcode}' | |
# TODO: Write results of zipcode to a cache file so subsequent lookups dont require another api request | |
resp = self.walmart_request(endpoint) | |
print( | |
f'Filtering results to the top {n} locations near the zipcode: {self.zipcode} - {resp["geo"]["city"]}, {resp["geo"]["state"]}...') | |
stores = {} | |
for entry in resp['accessPointList'][:n]: | |
stores[entry["dispenseStoreId"]] = entry['address']['line1'] | |
return stores | |
def build_inventory(self, item_id): | |
"""Build inventory based on list of stores.""" | |
inventory = {} | |
no_inventory = {} | |
for store_id in self.store_ids: | |
if self.has_inventory(store_id=store_id, item_id=item_id): | |
inventory[store_id] = item_id | |
else: | |
no_inventory[store_id] = item_id | |
return inventory, no_inventory | |
def has_inventory(self, store_id, item_id): | |
"""Get inventory for an item from individual stores.""" | |
endpoint = f'/v3/api/products/{item_id}?itemFields=all&storeId={store_id}' | |
store_inventory = self.walmart_request(endpoint) | |
result = None | |
try: | |
result = not store_inventory.get('basic').get('isOutOfStock') | |
except AttributeError: | |
# I believe this happens after a few requests from the same IP address | |
if self.verbose: | |
print("Got hit by the captcha, cannot programatically pull inventory.") | |
print(f"Navigate to and check the 'isOutOfStock' key: {BASE_URL}/{endpoint}") | |
return result | |
def rand(n): | |
"""Random number to the hundreds""" | |
return random.randint(0, int("9"*n)) | |
def generate_user_agent(): | |
return f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_{rand(2)}_{rand(1)}) AppleWebKit/{rand(3)}.{rand(2)} (KHTML, like Gecko) Chrome/95.0.{rand(4)}.{rand(2)} Safari/{rand(3)}.{rand(2)}" | |
@click.command() | |
@click.option('-i', '--item', default=None, type=click.Choice(item_dict.keys()), | |
help='Desired item you are searching for') | |
@click.option('-z', '--zipcode', default=78109, type=int, help='Desired zipcode you are searching in') | |
@click.option('-v', '--verbose', default=False, is_flag=True, help='Print more verbose details') | |
@click.option('-r', '--random-agent', default=False, is_flag=True, help='Randomizes your User Agent for the requests') | |
def main(item, zipcode, verbose, random_agent): | |
user_agent = generate_user_agent() if random_agent else USER_AGENT | |
walmart_grocery_inventory = WalmartGroceryInventory(zipcode=zipcode, user_agent=user_agent, verbose=verbose) | |
print(datetime.now().strftime("%Y-%m-%d %H:%M")) | |
for item_id in item_dict[item]: | |
# simplify this, remove the 'without' - just have it be the negative of the 'with'? | |
with_inventory, without_inventory = walmart_grocery_inventory.build_inventory(item_id=item_id) | |
inventory_addresses = [] | |
without_inventory_addresses = [] | |
for store_id in with_inventory.keys(): | |
inventory_addresses.append(walmart_grocery_inventory.stores.get(store_id)) | |
for store_id in without_inventory.keys(): | |
without_inventory_addresses.append(walmart_grocery_inventory.stores.get(store_id)) | |
print(f"Inventory for {item_id}: {inventory_addresses}") | |
if verbose: | |
print(f"No inventory: {without_inventory_addresses}") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Recommend only using this a couple times a day. There is a threshold that their site has: once that threshold is exceeded, will use captcha codes when checking for the inventory - thus removing some of the larger benefits of this script.