├── requirements.txt ├── .gitignore ├── accounts.json ├── Helpers.py ├── Element.py ├── examples.py ├── elements.json ├── README.md └── Lister.py /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abdallah-sameh-ragab/facebook-marketplace-lister/master/requirements.txt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cookies.json 2 | products.json 3 | scrape.py 4 | run.py 5 | "accounts copy.json" 6 | backup/* 7 | _pycache_/* -------------------------------------------------------------------------------- /accounts.json: -------------------------------------------------------------------------------- 1 | { 2 | "accounts" : [ 3 | { 4 | "id" : "...", 5 | "name" : "...", 6 | "email" : "...", 7 | "password" : "..." 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /Helpers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from string import Formatter 3 | 4 | def read_json(file): 5 | file_name = file if file.endswith('.json') else file + '.json' 6 | with open(file_name, 'r') as f: 7 | return json.load(f) 8 | 9 | def write_json(file, content): 10 | file_name = file if file.endswith('.json') else file + '.json' 11 | with open(file_name, 'w') as f: 12 | try: 13 | json.dump(content, f) 14 | return True 15 | except: 16 | return False 17 | 18 | def fstring_keys(fstring): 19 | keys = [part[1] for part in Formatter().parse(fstring) if part[1] is not None] 20 | return keys 21 | 22 | def format_xpath(fstring, vals): 23 | fstring_len = len(fstring_keys(fstring)) 24 | if isinstance(vals, (str, list, tuple)): 25 | if isinstance(vals, str) or len(vals) < fstring_len : 26 | list_of_vals = [vals] if isinstance(vals, str) else [*vals] 27 | difference = fstring_len - len(list_of_vals) 28 | values = list_of_vals + ['' for _ in range(difference)] 29 | elif len(vals) > fstring_len: 30 | values = [*vals][:fstring_len] 31 | else: 32 | values = [*vals] 33 | return fstring.format(*values) 34 | else: 35 | raise TypeError('Must be a string, a list or a tuple') -------------------------------------------------------------------------------- /Element.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.support import expected_conditions as EC 2 | from selenium.webdriver.common.by import By 3 | from selenium.webdriver.support.wait import WebDriverWait 4 | from Helpers import read_json, write_json, format_xpath 5 | 6 | class Element: 7 | def __init__(self, driver, name, values = None): 8 | self.driver = driver 9 | self.name = name 10 | self.values = values 11 | self.pathes = read_json('elements') 12 | @property 13 | def xpath(self): 14 | xpath_format = self.pathes[self.name]['xpath'] 15 | defaults = self.defaults 16 | return format_xpath(xpath_format, self.values) if self.values else format_xpath(xpath_format, defaults) 17 | @property 18 | def defaults(self): 19 | return self.pathes[self.name]['defaults'] 20 | @property 21 | def element(self): 22 | xpath = self.xpath 23 | # print("%s :: %s" % (self.name, xpath)) 24 | # print("%s :: %s" % (self.name, xpath)) 25 | element_type = self.pathes[self.name]['type'] 26 | if element_type == 'button' : 27 | element = WebDriverWait(self.driver, 30).until( 28 | EC.element_to_be_clickable((By.XPATH, xpath)) 29 | ) 30 | else: 31 | element = self.driver.find_element(By.XPATH, xpath) 32 | return element 33 | 34 | -------------------------------------------------------------------------------- /examples.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | test_product = { 4 | 'title': String, # Required 5 | 'price': String, # Required 6 | 'images' : [ 7 | {"file": "xxx.jpg"}و 8 | {"file": "xxx.jpg"}و 9 | ... 10 | ... 11 | ]و # Required 12 | 'location': String, # Optional : Required by facebook. If left empty the default value will be used. Default value can be changed at elements.json file. 13 | 'category': String, # Optional : Required by facebook. If left empty the default value will be used. Default value can be changed at elements.json file. 14 | 'condition': String, # Optional : Required by facebook. If left empty the default value will be used. Default value can be changed at elements.json file. 15 | 'hide_from_friends' : Boolean, # Optional : Optional for facebook. Default value is False. 16 | 'sku': String, # Optional : Optional for facebook. Default value is None. 17 | } 18 | """ 19 | 20 | from Lister import Lister 21 | import json 22 | 23 | my_account_id = '...' 24 | 25 | product = { 26 | 'title': '...', 27 | 'price': '...', 28 | 'images': [ 29 | {'file' : '/image.jpg'}, 30 | ], 31 | } 32 | 33 | def publish_single_product(): 34 | lister = Lister() 35 | if lister.login(my_account_id) : 36 | lister.list(product) 37 | 38 | def publish_multi_products(): 39 | my_json_file = open('products.json', 'r') 40 | products = json.load(my_json_file)['products'] 41 | 42 | lister = Lister() 43 | if lister.login(my_account_id) : 44 | for product in products : 45 | result = lister.list(product) 46 | if result: print('Success!') -------------------------------------------------------------------------------- /elements.json: -------------------------------------------------------------------------------- 1 | {"login_email": {"xpath": "//*[@id=\"email\"]", "type": "input", "defaults": []}, "login_password": {"xpath": "//*[@id=\"pass\"]", "type": "input", "defaults": []}, "login_button": {"xpath": "//*[@id=\"loginbutton\"]", "type": "button", "defaults": []}, "post_image": {"xpath": "//input[@type='file'][contains(@accept,'image')]", "type": "input", "defaults": []}, "post_image_css": {"xpath": "input[type=\"file\"][accept*=\"image\"]", "type": "", "defaults": []}, "post_title": {"xpath": "//div[span/text() = \"Title\"]/input", "type": "input", "defaults": []}, "post_price": {"xpath": "//div[span/text() = \"Price\"]/input", "type": "input", "defaults": []}, "post_sku": {"xpath": "//div[span/text() = \"SKU\"]/input", "type": "input", "defaults": []}, "post_location": {"xpath": "//div[span/text() = \"Location\"]/input", "type": "input", "defaults": []}, "post_location_option": {"xpath": "//ul/li[@role=\"option\"]//*[text() = \"{}\"]", "type": "element", "defaults": "Cairo, Egypt"}, "post_description": {"xpath": "//div[span/text() = \"Description\"]/textarea", "type": "input", "defaults": []}, "post_category": {"xpath": "//span[text() = \"Category\"]/..", "type": "element", "defaults": []}, "post_category_option": {"xpath": "//span//span[text() = \"{}\"]", "type": "element", "defaults": "Electronics & computers"}, "post_condition": {"xpath": "//span[text() = \"Condition\"]/..", "type": "element", "defaults": []}, "post_condition_option": {"xpath": "//div/div/div/div/div/span[text() = \"{}\"]/../../../..", "type": "element", "defaults": "New"}, "post_hide_from_friends": {"xpath": "(//input[@type = \"checkbox\"])[2]", "type": "element", "defaults": []}, "post_next_button": {"xpath": "//span/span[text()=\"Next\"]", "type": "button", "defaults": []}, "post_publish_button": {"xpath": "//div[not(@aria-disabled)]/div/div/div/span/span[text()=\"Publish\"]", "type": "button", "defaults": []}} 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Installation : 4 | 5 | ## 1. Add credentials : 6 | 1. create **accounts.json** file at the main directory. 7 | 2. add your facebook account's credentials as follows: 8 | 9 | "accounts" : [ 10 | { "id" : "...", 11 | "name" : "...", 12 | "email" : "...", 13 | "password" : "..." 14 | }, 15 | ... 16 | ] 17 | 18 | ## 2. Install a driver suitable for your browser : 19 | 1. Follow The [official Instructions by selenium](https://www.selenium.dev/documentation/webdriver/getting_started/install_drivers/) to download a suitable driver for your browser. 20 | 2. Add the driver to the `drivers` folder. 21 | 3. Make sure to add the driver's filename to the `__init__` function of the `Lister Class` like the following : `self.driver_file = 'chromedriver.exe'` and set the driver's options. 22 | 23 | >**NOTICE :** I have set everything for the **chrome driver** so if you are using chrome browser you will have to just download the driver and add it to the **drivers** folder, 24 | ## 3. Install Required Packages : 25 | navigate to the main directory and run `pip install -r requirements.txt` on your terminal. 26 | 27 | # Usage: 28 | you can check `examples.py` file for example usage. 29 | 30 | ## ****IMPORTANT**** | Product Object Structure : 31 | 32 | 1. **title : string | Required | required by facebook.** 33 | example: 34 | `{'title' : 'Cool Product'}` 35 | 2. **price : string | Required | required by facebook.** 36 | example: 37 | `{'price' : '25'}` 38 | 3. **images : list | Required | required by facebook.** 39 | 40 | - at least one image required. 41 | - images must be placed at the `images` directory. 42 | example: 43 | `{'images' : [ 44 | {"file" : "cool_image.png"}, 45 | ... 46 | ]}` 47 | 4. **location : string | Required | required by facebook.** 48 | 49 | - when left empty, default values will be used. 50 | - you can change default values at the `elements.json` file. 51 | 52 | example: 53 | `{'location' : 'Cairo, Egypt'}` 54 | 55 | 5. **category : string | Optional | required by facebook.** 56 | 57 | - when left empty, default values will be used. 58 | - you can change default values at the `elements.json` file. 59 | 60 | example: 61 | `{'category' : 'Electronics & computers'}` 62 | 63 | 6. **condition : string | Optional | required by facebook.** 64 | 65 | - when left empty, default values will be used. 66 | - you can change default values at the `elements.json` file. 67 | 68 | example: 69 | `{'category' : 'New'}` 70 | 71 | 7. **sku : string | Optional.** 72 | example: 73 | `{'sku' : '564646154'}` 74 | 75 | 8. **hide_from_friends : boolean | Optional.** 76 | set to `True` to hide the listing from your facebook friends. 77 | example: 78 | `{'hide_from_friends' : True}` 79 | 80 | ## Listing a single product : 81 | 82 | - create a new instance from `Lister Class` and run `login` function passing the id of one of the accounts registered at `accounts.json` 83 | - `login` function will return `True` on a successful login. 84 | - run list function passing a `product` dictionary. 85 | 86 | 87 | Example : 88 | 89 | from Lister import Lister 90 | 91 | product = { 92 | 'title': '...', 93 | 'price': '...', 94 | 'images': '[ 95 | {'file' : '/image.jpg'}, 96 | ]', 97 | } 98 | 99 | lister = Lister() 100 | my_account_id = 'ma' 101 | if lister.login('ma') : 102 | lister.list(product) 103 | 104 | 105 | ## Listing multiple products : 106 | 107 | - create a new instance from `Lister Class` and run `login` function passing the id of one of the accounts registered at `accounts.json` 108 | - `login` function will return `True` on a successful login. 109 | 110 | 111 | Example : 112 | 113 | from Lister import Lister 114 | import json 115 | 116 | my_json_file = open('products.json', 'r') 117 | products = json.load(my_json_file)['products'] 118 | 119 | lister = Lister() 120 | my_account_id = 'ma' 121 | if lister.login('ma') : 122 | for product in products : 123 | result = lister.list(product) 124 | if result: print('Success!') -------------------------------------------------------------------------------- /Lister.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | import time 3 | import os 4 | from Element import Element 5 | from Helpers import read_json 6 | from datetime import datetime 7 | from colorama import Fore, Style 8 | from selenium.webdriver.common.keys import Keys 9 | from selenium.webdriver.support import expected_conditions as EC 10 | from selenium.webdriver.common.by import By 11 | from selenium.webdriver.support.wait import WebDriverWait 12 | 13 | 14 | 15 | class Lister: 16 | def __init__(self): 17 | self.driver_file = 'chromedriver.exe' 18 | self.sleep_time = 1 19 | chrome_options = webdriver.ChromeOptions() 20 | prefs = {"profile.default_content_setting_values.notifications" : 2} 21 | chrome_options.add_experimental_option("prefs",prefs) 22 | chrome_options.add_experimental_option("detach", True) 23 | chrome_options.add_argument("--start-maximized") 24 | 25 | self.driver = webdriver.Chrome('drivers/%s' % self.driver_file, chrome_options=chrome_options) 26 | self.driver.implicitly_wait(30) 27 | 28 | def read_accounts(self): 29 | return read_json('accounts')['accounts'] 30 | 31 | def login(self, account_id): 32 | registered_accounts = self.read_accounts() 33 | account_info = list(filter(lambda acc: acc['id'] == account_id, registered_accounts))[0] 34 | log('Logging in as "%s" ..' % account_info['name'], 'main') 35 | 36 | self.driver.get('https://www.facebook.com/login') 37 | 38 | 39 | # entering email 40 | email_input = Element(self.driver, 'login_email').element 41 | email_input.clear() 42 | email_input.send_keys(account_info['email']) 43 | 44 | # entering password 45 | password_input = Element(self.driver, 'login_password').element 46 | password_input.clear() 47 | password_input.send_keys(account_info['password']) 48 | 49 | # Submitting 50 | password_button = Element(self.driver, 'login_button').element 51 | password_button.click() 52 | 53 | # Confirm Logged In 54 | logged = WebDriverWait(self.driver, 60).until( 55 | lambda driver: "login" not in driver.current_url 56 | ) 57 | 58 | if logged : log('Logged in Successfully.', 'success') 59 | else : log('Failed To Login.', 'failure') 60 | 61 | return logged 62 | 63 | def list(self, item): 64 | self.driver.get('https://www.facebook.com/marketplace/create/item') 65 | 66 | listing_item = Item(self.driver, item) 67 | 68 | listing_item.upload_images() 69 | time.sleep(self.sleep_time) 70 | 71 | listing_item.enter_title() 72 | time.sleep(self.sleep_time) 73 | 74 | listing_item.enter_price() 75 | time.sleep(self.sleep_time) 76 | 77 | listing_item.choose_category() 78 | time.sleep(self.sleep_time) 79 | 80 | listing_item.choose_condition() 81 | time.sleep(self.sleep_time) 82 | 83 | if 'description' in item.keys() and item['description'] : 84 | listing_item.enter_description() 85 | time.sleep(self.sleep_time) 86 | 87 | if 'sku' in item.keys() and item['sku'] : 88 | listing_item.enter_sku() 89 | time.sleep(self.sleep_time) 90 | 91 | listing_item.choose_location() 92 | time.sleep(self.sleep_time) 93 | 94 | if 'hide_from_friends' in item.keys() and item['hide_from_friends'] : 95 | listing_item.hide_from_friends() 96 | time.sleep(self.sleep_time) 97 | 98 | listing_item.click_next() 99 | time.sleep(self.sleep_time) 100 | 101 | listing_item.click_publish() 102 | time.sleep(self.sleep_time) 103 | 104 | # Check if Posted 105 | posted = WebDriverWait(self.driver, 60).until( 106 | lambda driver: "you" in driver.current_url 107 | ) 108 | 109 | return posted 110 | 111 | class Item : 112 | def __init__(self, driver, item): 113 | self.driver = driver 114 | self.item = item 115 | 116 | def upload_images(self): 117 | try: 118 | log('Uploading Images', 'main') 119 | image_upload = Element(self.driver, 'post_image').element 120 | self.driver.execute_script("document.querySelector('%s').classList = []" % Element(self.driver, 'post_image_css').xpath) 121 | log('Showing image input ..', 'main') 122 | joined_images_path = ' \n '.join([os.path.abspath('images/%s' % image['file']) for image in self.item['images']][:10]) 123 | log('sending images ..', 'main') 124 | image_upload.send_keys(joined_images_path) 125 | log('Uploaded Images Successfully .', 'success') 126 | return True 127 | except : 128 | log('FAILED TO UPLOAD IMAGE', 'failure') 129 | return False 130 | 131 | def enter_title(self): 132 | try: 133 | log('Entering The Title', 'main') 134 | title_input = Element(self.driver, 'post_title').element 135 | title_input.clear() 136 | title_input.send_keys(self.item['title']) 137 | log('Entered Title Successfully .', 'success') 138 | return True 139 | except : 140 | log('FAILED TO ENTER THE TITLE', 'failure') 141 | return False 142 | 143 | def enter_price(self): 144 | try: 145 | log('Entering The Price', 'main') 146 | price_input = Element(self.driver, 'post_price').element 147 | price_input.clear() 148 | price_input.send_keys(self.item['price']) 149 | log('Entered Price Successfully .', 'success') 150 | return True 151 | except : 152 | log('FAILED TO ENTER THE PRICE', 'failure') 153 | return False 154 | 155 | def choose_category(self): 156 | try: 157 | log('Choosing The Category', 'main') 158 | category_dropdown = Element(self.driver, 'post_category').element 159 | category_dropdown.click() 160 | 161 | values = self.item['category'] if 'category' in self.item.keys() and self.item['category'] else None 162 | category_dropdown_option = Element(self.driver, 'post_category_option', values).element 163 | print(Element(self.driver, 'post_category_option', values).xpath) 164 | print(Element(self.driver, 'post_category_option', values).xpath) 165 | print(Element(self.driver, 'post_category_option', values).xpath) 166 | print(Element(self.driver, 'post_category_option', values).xpath) 167 | print(Element(self.driver, 'post_category_option', values).xpath) 168 | log('clicking The Category Dropdown ..', 'sub') 169 | category_dropdown_option.click() 170 | 171 | log('Category Chosen Successfully .', 'success') 172 | return True 173 | except : 174 | log('FAILED TO CHOOSE THE CATEGORY', 'failure') 175 | return False 176 | 177 | def choose_condition(self): 178 | try: 179 | log('Choosing The Condition', 'main') 180 | condition_dropdown = Element(self.driver, 'post_condition').element 181 | condition_dropdown.click() 182 | log('clicking The Condition Dropdown ..', 'sub') 183 | 184 | values = self.item['condition'] if 'condition'in self.item.keys() and self.item['condition'] else None 185 | condition_dropdown_option = Element(self.driver, 'post_condition_option', values).element 186 | condition_dropdown_option.click() 187 | log('Condition Chosen Successfully .', 'success') 188 | return True 189 | except : 190 | log('FAILED TO CHOOSE THE CATEGORY', 'failure') 191 | return False 192 | 193 | def enter_description(self): 194 | try: 195 | log('Entering The Description', 'main') 196 | description_input = Element(self.driver, 'post_description').element 197 | description_input.clear() 198 | description_input.send_keys(self.item['description']) 199 | log('Entered Description Successfully .', 'success') 200 | return True 201 | except : 202 | log('FAILED TO ENTER THE Description', 'failure') 203 | return False 204 | 205 | def enter_sku(self): 206 | try: 207 | log('Entering The SKU', 'main') 208 | sku_input = Element(self.driver, 'post_sku').element 209 | sku_input.clear() 210 | sku_input.send_keys(self.item['sku']) 211 | log('Entered SKU Successfully .', 'success') 212 | return True 213 | except : 214 | log('FAILED TO ENTER THE SKU', 'failure') 215 | return False 216 | 217 | def choose_location(self): 218 | try: 219 | location = self.item['location'] if self.item['location'] else Element(self.driver, 'post_location_option').defaults 220 | log('Choosing The Location', 'main') 221 | location_input = Element(self.driver, 'post_location').element 222 | location_input.click() 223 | log('Searching Locations ..', 'sub') 224 | location_input.send_keys(Keys.DELETE) 225 | location_input.send_keys(location) 226 | 227 | log('Choosing Location ..', 'sub') 228 | values = self.item['location'] if 'location'in self.item.keys() and self.item['location'] else None 229 | location_input_option = Element(self.driver, 'post_location_option', values).element 230 | location_input_option.click() 231 | 232 | log('Location Chosen Successfully .', 'success') 233 | return True 234 | except : 235 | log('FAILED TO CHOOSE THE Location', 'failure') 236 | return False 237 | 238 | def hide_from_friends(self): 239 | try: 240 | log('Checking Hide From Friends', 'main') 241 | self.click_button('post_hide_from_friends') 242 | log('Checked Hide From Friends Successfully .', 'success') 243 | return True 244 | except : 245 | log('FAILED TO Check Hide From Friends', 'failure') 246 | return False 247 | 248 | def click_next(self): 249 | try: 250 | log('Clicking Next', 'main') 251 | self.click_button('post_next_button') 252 | log('Clicked Next Successfully', 'success') 253 | return True 254 | except : 255 | log('FAILED TO click Next', 'failure') 256 | return False 257 | 258 | def click_publish(self): 259 | try: 260 | log('Clicking Publish', 'main') 261 | self.click_button('post_publish_button') 262 | log('Clicked Publish Successfully', 'success') 263 | return True 264 | except : 265 | log('FAILED TO click Publish', 'failure') 266 | return False 267 | 268 | def click_button(self, button): 269 | element = Element(self.driver, button).element 270 | element.click() 271 | 272 | def log(msg, type=None): 273 | now = datetime.now() 274 | current_time = now.strftime("%H:%M:%S") 275 | msg = "[%s] : %s" % (current_time, msg) 276 | if type is not None: 277 | if type == 'failure': 278 | msg = Fore.RED + "\t- " + msg + Style.RESET_ALL 279 | elif type == 'success': 280 | msg = Fore.GREEN + "\t+ " + msg + Style.RESET_ALL 281 | elif type == 'sub': 282 | msg = Fore.WHITE + "\t> " + msg + Style.RESET_ALL 283 | elif type == 'main': 284 | msg = Fore.WHITE + ">> " + msg + Style.RESET_ALL 285 | else: 286 | msg = msg + Style.RESET_ALL 287 | else: 288 | msg = msg + Style.RESET_ALL 289 | print (msg) 290 | 291 | --------------------------------------------------------------------------------