How To | Programming | Web Automation

LinkedOut — Automate LinkedIn

Avoid LinkedIn’s Notorious “Incoming” Season by Posting Your Updates with Selenium 2.0

Winston Robson
May 2 · 10 min read
Via @finance_god on Instagram (profile link)

As many have noticed, any given user’s LinkedIn feed is currently somewhere between dotted and drowning in content related to the nearly 2.3 million (hopefully young) people updating their Headlines with aims of broadcasting to the world that they are “Incoming” to an 8–10 week job (to date, the universe waits for the first “Current” or “Outgoing” to be spotted).

For readers less familiar with this phenomenon, enjoy this sample taken from the results of a LinkedIn search for people with “Incoming” in their Headline:

  • Incoming Product Design Intern at …
  • Incoming Sales Operations Intern @ …
  • Incoming Investment Banking Summer Analyst at … (2019)
  • Incoming Hot Dog Assembly Intern / CEO at Big Buns Truck Stop

Ok, so maybe the last one wasn’t real, but that search generated over 2,250,000 results, so it is not all that unlikely you have seen worse…

This really captures the general mood pretty well. (Top is Comment, Bottom is Original Post)

Let it be clear, I bear no ill will towards these individuals; I just really do not care to spend time scrolling through their nonsense attempts at relevancy, and have instead spent that time building a script to spew my own nonsense attempts at relevancy through the click of a button.

In a lighthearted and easy to comprehend manner, the remainder of this article will outline how you too can get LinkedOut.

Defining — What is being Done? and How?

  1. understand where one is
  2. understand what he or she is trying to accomplish
  3. have some rough estimation of how this will be done

The intro already blew through Step 1 (hell) and Step 2 (spend as little time here as possible); now, how to get the situation sorted?

Well, based on the title, it will involve Selenium WebDriver (Selenium 1.0 + WebDriver = Selenium 2.0), a web automation framework; so what are the steps one would take if posting to LinkedIn via the web?

Rough Estimation

  1. Navigate to
  2. Sign In to an Account
  3. Recognize and click the “Start a post” button
  4. Type up (or otherwise) the content to be displayed
  5. Focus and click the ‘Post’ button

Making it Happen

Not all that differently, as it turns out, there is a bit of fine print humans mask with a GUI that Selenium insists on documenting in the Captain’s Log --remind me to remind you of this later on-- but this is programming, so what did you expect?

LinkedFrom — Imports

from time import sleep

Then it might be a decent idea to call in the package being explored;

from selenium import webdriver

Next, as this tutorial is to make life easier, opt to code with masks (Keys) like CONTROL, ALT , or DELETE instead of more traditional '\ue009' , ‘\ue00a’ , or ‘\ue017' . Here’s the full list of Keys this import grants access to.

from selenium.webdriver.common.keys import Keys

Third, make a separate file to store your account information (username and password), mine is called looks like:

with ‘__OPT-OUT__’ later mentioned (in main file) as an option to input email/phone number and password when the script is called.

Finally, back in the main file, bring in that account info;

from userinfo import user, pwrd

“And they’re off!”

Just kidding, Instagram will be covered next week; we’re here to save you time and frustration when sharing your content, ideas, and abilities to LinkedIn.

As WebDriver is an Object-Oriented API, coding of this script will follow suit and move forward in the OOP paradigm. See “Intro to OOP in Python” if you’d like a 4 minute refresher on OOP.

LinkedSelf __init__(username, password, driver)

Right now, bringing the computer up to speed with a few base assumptions, so it has the data necessary to complete the task, is priority #1.

Of these info bits, the most vital include:

  • Username for logging in
  • Password for logging in
  • Browser being operated

Browser vs Driver

So rather than a Browser, the attribute being passed though should be a Driver (e.g. Gecko, Chrome, Edge), which will call upon its paired Browser for collaboration.

Setting Up Shop

def __init__(self, username, password, driver):

and continue mapping out initialization of username,password, and driver until the current product is akin to:

Note: the post is not yet in play, and the notes and docstrings are optional.

LinkedKey — login()

.get( the Page )

Signing In

# regarding logging in 
def login(self):

# retag driver
driver = self.driver

# load login page

# take a breath (hedge load time)

# identify and send username to user input box
self.uhhhhhhhhh.... oof..
Note: Selenium is unaware of this “Login box” about which you speak

Error — What??

Here the user information input boxes are less like;

And more like;

‘#username’ ,‘#password’ , and ‘.btn__primary — large’ if identifying by CSS Selector

But could also be;

‘//*[@id=”username”]’ ,‘//*[@id=”password”]’ , and ‘/html/body/div/main/div/form/div[3]/button’ if identifying by XPath

While numerous other methods for a Driver to identify these boxes exist, CSS Selector and XPath are by far the most popular, almost always get the job done, and will be all we use today.

XPaths and CSS Selectors

  1. right clicking the GUI representation
  2. then click inspect element
  3. finding the element (now highlighted) in the Inspector
  4. right clicking on it to pull up a new menu
  5. selecting Copy
  6. and choosing either XPath or CSS Selector

The head-to-head pros and cons of these two primary identification methods are hotly debated and a Story for another day. For now, just know we will use whichever is cleaner and more likely to succeed in the future.

In the case of the log in page, neither the CSS Selector nor the XPath of the “Sign in” button are ideal.

Never bet on anything resembling ‘/html/body/div/main/div/form/div[3]/button’ or ‘.btn__primary — large’ to work longer than today (especially if dealing with Facebook --a Story for tomorrow). Both could easily be wiped by the addition of another button, the loading of an unexpected format, or just routine site maintenance.

It is for instances like these that we imported Keys , and will just RETURN or ENTER after sending the self.p attribute (via WebDriver’s send_keys() method) to the password input box.

Obviously, this could also apply to ‘//*[@id=”password”]’, but how often do you think LinkedIn decides it’s a good idea to adjust the id of a password element?

Hey Driver, Find it please

WebDriver can tag these elements for Selenium in a few ways that all basically do the same thing, so go for the easiest to remember; find_element_by_xpath() and find_element_by_css_selector() are those.

Now, with this mess cleaned up, let’s knock logging in out;

Reminder: noting and docstrings are optional

Nice work! Selenium is now through the gates and straight chillin at the LinkedIn homepage. That wasn’t all that difficult. Was it?

LinkedOn —share_the_(post)

Rough Estimation

  1. ̵N̵a̵v̵i̵g̵a̵t̵e̵ ̵t̵o̵ ̵h̵t̵t̵p̵s̵:̵/̵/̵L̵i̵n̵k̵e̵d̵I̵n̵.̵c̵o̵m̵ ̵
  2. ̵S̵i̵g̵n̵ ̵I̵n̵ ̵t̵o̵ ̵a̵n̵ ̵A̵c̵c̵o̵u̵n̵t̵
  3. Recognize and click the “Start a post” button
  4. Type up (or otherwise) the content to be displayed
  5. Focus and click the ‘Post’ button

Ok, we are doing pretty well! For the purposes of readability and functionality, items 4–6 will be group together in the next method.


# css selector of 'start a post' button
start_share_button = '.share-box__open'
# css selector of post input box
to_talk_about = '.mentions-texteditor__contenteditable'
# xpath of 'post' button
post_button = '//*[contains(@class,"share-actions__primary-action")]'

And it is now time to get to posting. First up, opening the content box;

# find button to start a post using css selector
start_post = driver.find_element_by_css_selector(start_share_button)
# and click to start the post

Second, type up the post being shared. The post will be input from the method, so just call it post for now;

# find post input box (now open) via css selector
post_box = driver.find_element_by_css_selector(to_talk_about)
# type out the content (input from method)

Pause, quick note; deriving non-obvious XPaths

You’re likely familiar with the layout of LinkedIn’s posting box, but just in case, this is what the bottom looks like:

Directly pulling the XPath from that little blue “Post” button results in something like //*[@id=”ember683"] , an immediate red flag as any path with numbers at the end is subject to quickly change.

For example, refreshing the page and pulling again returns //*[@id=”ember365"] (same with the CSS Selectors #ember683 and #ember365).

How can we solve this? One way is to examine the HTML (highlighted in the inspector) and find a recognizable label. The HTML in question looks like:

<button data-control-name=”” id=”ember383" class=”share-actions__primary-action artdeco-button artdeco-button — 2 artdeco-button — primary ember-view”><! — →

data-control-name seems decent and short, but something more common, such as class, would better serve the purposes of readable code that can easily be edited in the event of breaking some time in the future. This decision is based on my semi-familiar understanding of HTML, where I know class is commonplace, but have not encountered data-control-name very often.

Using a sweet XPath trick, contains, that class can be summed up to “share-actions__primary-action” and turned into the viable XPath ‘//*[contains(@class,”share-actions__primary-action”)]’ .

While other applications exist, and are stupid cool & handy, they are not this purpose of this Story. Onward;

Posting (continued)

# find post button via xpath
post_now = driver.find_element_by_xpath(post_button)
# and click to post

Now just,

  • Combine each into a their own one-line version (because you’re too good to be wasting space and it’s a simple challenge)
  • Select and them all into a method ; def share_the_(self, post):
  • Tie in the driver, add cool-down sleeps, and self the post

to finalize our posting method to something along the lines (ha!) of:

No. Noting and docstrings are not optional. Anything less than perfection is sin. (jk, lol — or am I?)

LinkedOff — close_out()

Done simply;

LinkedOut— class

The work has been done and a clear understanding of what is going on has been earned. It is finally time to name the class and convert the Python file in to code that can run.

  1. Select everything except the imports
  2. Hit tab
  3. Make a new line below the imports and before __init__
  4. Name the class whatever you want, I use LinkedOut

The following should be added to make the script to be able to run on call and request post to share:

# proper run
if __name__ == "__main__":
# username
user_id = user
# password
pword = pwrd

# option to input email/phone live
if user_id == '__OPT-OUT__':
# ask for user
user_id = input('username: ')
# option to input password live
if pword == '__OPT-OUT__':
# ask for pwrd
pword = input('password: ')

# Firefox/Gecko options setup
options = webdriver.FirefoxOptions()
# deactivate push notifications
options.set_preference('dom.push.enabled', False)

# request post for sharing
status = input('What would you like to share? ')

# tag bot
incoming = LinkedOut(username=user_id,

# get the party started

# share the post

# get the heck outta dodge

# and give reassurance so we don't engage the void
print(f'sent {status}\nto account associated with {user_id}')

Finally, here’s the full .py file and link to the GitHub repo:

Thank you for stopping by and following along! Please do not hesitate to drop a comment or reach out with any question or feedback you may have.

Related Reading

More by Winston

Future Vision

A publication centered around high quality storytelling

Winston Robson

Written by

“Energy may be likened to the bending of a crossbow; decision, to the releasing of a trigger.”

Future Vision

A publication centered around high quality storytelling

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade