def search_people( self, keywords=None, connection_of=None, network_depths=None, current_company=None, past_companies=None, nonprofit_interests=None, profile_languages=None, regions=None, industries=None, schools=None, contact_interests=None, service_categories=None, include_private_profiles=False, # profiles without a public id, "Linkedin Member" # Keywords filter keyword_first_name=None, keyword_last_name=None, keyword_title=None, # `keyword_title` and `title` are the same. We kept `title` for backward compatibility. Please only use one of them. keyword_company=None, keyword_school=None, network_depth=None, # DEPRECATED - use network_depths title=None, # DEPRECATED - use keyword_title **kwargs, ): """Perform a LinkedIn search for people. :param keywords: Keywords to search on :type keywords: str, optional :param current_company: A list of company URN IDs (str) :type current_company: list, optional :param past_companies: A list of company URN IDs (str) :type past_companies: list, optional :param regions: A list of geo URN IDs (str) :type regions: list, optional :param industries: A list of industry URN IDs (str) :type industries: list, optional :param schools: A list of school URN IDs (str) :type schools: list, optional :param profile_languages: A list of 2-letter language codes (str) :type profile_languages: list, optional :param contact_interests: A list containing one or both of "proBono" and "boardMember" :type contact_interests: list, optional :param service_categories: A list of service category URN IDs (str) :type service_categories: list, optional :param network_depth: Deprecated, use `network_depths`. One of "F", "S" and "O" (first, second and third+ respectively) :type network_depth: str, optional :param network_depths: A list containing one or many of "F", "S" and "O" (first, second and third+ respectively) :type network_depths: list, optional :param include_private_profiles: Include private profiles in search results. If False, only public profiles are included. Defaults to False :type include_private_profiles: boolean, optional :param keyword_first_name: First name :type keyword_first_name: str, optional :param keyword_last_name: Last name :type keyword_last_name: str, optional :param keyword_title: Job title :type keyword_title: str, optional :param keyword_company: Company name :type keyword_company: str, optional :param keyword_school: School name :type keyword_school: str, optional :param connection_of: Connection of LinkedIn user, given by profile URN ID :type connection_of: str, optional :return: List of profiles (minimal data only) :rtype: list """ filters = ["resultType->PEOPLE"] if connection_of: filters.append(f"connectionOf->{connection_of}") if network_depths: filters.append(f'network->{"|".join(network_depth)}') elif network_depth: filters.append(f"network->{network_depth}") if regions: filters.append(f'geoUrn->{"|".join(regions)}') if industries: filters.append(f'industry->{"|".join(industries)}') if current_company: filters.append(f'currentCompany->{"|".join(current_company)}') if past_companies: filters.append(f'pastCompany->{"|".join(past_companies)}') if profile_languages: filters.append(f'profileLanguage->{"|".join(profile_languages)}') if nonprofit_interests: filters.append(f'nonprofitInterest->{"|".join(nonprofit_interests)}') if schools: filters.append(f'schools->{"|".join(schools)}') if service_categories: filters.append(f'serviceCategory->{"|".join(service_categories)}') # `Keywords` filter keyword_title = keyword_title if keyword_title else title if keyword_first_name: filters.append(f"firstName->{keyword_first_name}") if keyword_last_name: filters.append(f"lastName->{keyword_last_name}") if keyword_title: filters.append(f"title->{keyword_title}") if keyword_company: filters.append(f"company->{keyword_company}") if keyword_school: filters.append(f"school->{keyword_school}") params = {"filters": "List({})".format(",".join(filters))} if keywords: params["keywords"] = keywords data = self.search(params, **kwargs) results = [] for item in data: if not include_private_profiles and "publicIdentifier" not in item: continue results.append( { "urn_id": get_id_from_urn(item.get("targetUrn")), "distance": item.get("memberDistance", {}).get("value"), "public_id": item.get("publicIdentifier"), "tracking_id": get_id_from_urn(item.get("trackingUrn")), } ) return results
def get_profile(self, public_id=None, urn_id=None): """Fetch data for a given LinkedIn profile. :param public_id: LinkedIn public ID for a profile :type public_id: str, optional :param urn_id: LinkedIn URN ID for a profile :type urn_id: str, optional :return: Profile data :rtype: dict """ # NOTE this still works for now, but will probably eventually have to be converted to # https://www.linkedin.com/voyager/api/identity/profiles/ACoAAAKT9JQBsH7LwKaE9Myay9WcX8OVGuDq9Uw res = self._fetch(f"/identity/profiles/{public_id or urn_id}/profileView") data = res.json() if data and "status" in data and data["status"] != 200: self.logger.info("request failed: {}".format(data["message"])) return {} # massage [profile] data profile = data["profile"] if "miniProfile" in profile: if "picture" in profile["miniProfile"]: profile["displayPictureUrl"] = profile["miniProfile"]["picture"][ "com.linkedin.common.VectorImage" ]["rootUrl"] images_data = profile["miniProfile"]["picture"][ "com.linkedin.common.VectorImage" ]["artifacts"] for img in images_data: w, h, url_segment = itemgetter( "width", "height", "fileIdentifyingUrlPathSegment" )(img) profile[f"img_{w}_{h}"] = url_segment profile["profile_id"] = get_id_from_urn(profile["miniProfile"]["entityUrn"]) profile["profile_urn"] = profile["miniProfile"]["entityUrn"] profile["member_urn"] = profile["miniProfile"]["objectUrn"] del profile["miniProfile"] del profile["defaultLocale"] del profile["supportedLocales"] del profile["versionTag"] del profile["showEducationOnProfileTopCard"] # massage [experience] data experience = data["positionView"]["elements"] for item in experience: if "company" in item and "miniCompany" in item["company"]: if "logo" in item["company"]["miniCompany"]: logo = item["company"]["miniCompany"]["logo"].get( "com.linkedin.common.VectorImage" ) if logo: item["companyLogoUrl"] = logo["rootUrl"] del item["company"]["miniCompany"] profile["experience"] = experience # massage [education] data education = data["educationView"]["elements"] for item in education: if "school" in item: if "logo" in item["school"]: item["school"]["logoUrl"] = item["school"]["logo"][ "com.linkedin.common.VectorImage" ]["rootUrl"] del item["school"]["logo"] profile["education"] = education # massage [languages] data languages = data["languageView"]["elements"] for item in languages: del item["entityUrn"] profile["languages"] = languages # massage [publications] data publications = data["publicationView"]["elements"] for item in publications: del item["entityUrn"] for author in item.get("authors", []): del author["entityUrn"] profile["publications"] = publications # massage [certifications] data certifications = data["certificationView"]["elements"] for item in certifications: del item["entityUrn"] profile["certifications"] = certifications # massage [volunteer] data volunteer = data["volunteerExperienceView"]["elements"] for item in volunteer: del item["entityUrn"] profile["volunteer"] = volunteer # massage [honors] data honors = data["honorView"]["elements"] for item in honors: del item["entityUrn"] profile["honors"] = honors return profile
def get_profile(self, public_id=None, urn_id=None): """ Return data for a single profile. [public_id] - public identifier i.e. tom-quirk-1928345 [urn_id] - id provided by the related URN """ evade() res = self.client.session.get( f"{self.client.API_BASE_URL}/identity/profiles/{public_id or urn_id}/profileView" ) data = res.json() if data and "status" in data and data["status"] != 200: self.logger.info("request failed: {}".format(data["message"])) return {} # massage [profile] data profile = data["profile"] if "miniProfile" in profile: if "picture" in profile["miniProfile"]: profile["displayPictureUrl"] = profile["miniProfile"][ "picture"]["com.linkedin.common.VectorImage"]["rootUrl"] profile["profile_id"] = get_id_from_urn( profile["miniProfile"]["entityUrn"]) del profile["miniProfile"] del profile["defaultLocale"] del profile["supportedLocales"] del profile["versionTag"] del profile["showEducationOnProfileTopCard"] # massage [experience] data experience = data["positionView"]["elements"] for item in experience: if "company" in item and "miniCompany" in item["company"]: if "logo" in item["company"]["miniCompany"]: logo = item["company"]["miniCompany"]["logo"].get( "com.linkedin.common.VectorImage") if logo: item["companyLogoUrl"] = logo["rootUrl"] del item["company"]["miniCompany"] profile["experience"] = experience # massage [skills] data # skills = [item["name"] for item in data["skillView"]["elements"]] # profile["skills"] = skills profile["skills"] = self.get_profile_skills(public_id=public_id, urn_id=urn_id) # massage [education] data education = data["educationView"]["elements"] for item in education: if "school" in item: if "logo" in item["school"]: item["school"]["logoUrl"] = item["school"]["logo"][ "com.linkedin.common.VectorImage"]["rootUrl"] del item["school"]["logo"] profile["education"] = education return profile
def view_profile( self, target_profile_public_id, target_profile_member_urn_id=None, network_distance=None, ): """View a profile, notifying the user that you "viewed" their profile. Provide [target_profile_member_urn_id] and [network_distance] to save 2 network requests and speed up the execution of this function. :param target_profile_public_id: public ID of a LinkedIn profile :type target_profile_public_id: str :param network_distance: How many degrees of separation exist e.g. 2 :type network_distance: int, optional :param target_profile_member_urn_id: member URN id for target profile :type target_profile_member_urn_id: str, optional :return: Error state. True if error occurred :rtype: boolean """ me_profile = self.get_user_profile() if not target_profile_member_urn_id: profile = self.get_profile(public_id=target_profile_public_id) target_profile_member_urn_id = int(get_id_from_urn(profile["member_urn"])) if not network_distance: profile_network_info = self.get_profile_network_info( public_profile_id=target_profile_public_id ) network_distance = int( profile_network_info["distance"] .get("value", "DISTANCE_2") .split("_")[1] ) viewer_privacy_setting = "F" me_member_id = me_profile["plainId"] client_application_instance = self.client.metadata["clientApplicationInstance"] eventBody = { "viewerPrivacySetting": viewer_privacy_setting, "networkDistance": network_distance, "vieweeMemberUrn": f"urn:li:member:{target_profile_member_urn_id}", "profileTrackingId": self.client.metadata["clientPageInstanceId"], "entityView": { "viewType": "profile-view", "viewerId": me_member_id, "targetId": target_profile_member_urn_id, }, "header": { "pageInstance": { "pageUrn": "urn:li:page:d_flagship3_profile_view_base", "trackingId": self.client.metadata["clientPageInstanceId"], }, "time": int(time()), "version": client_application_instance["version"], "clientApplicationInstance": client_application_instance, }, "requestHeader": { "interfaceLocale": "en_US", "pageKey": "d_flagship3_profile_view_base", "path": f"/in/{target_profile_member_urn_id}/", "referer": "https://www.linkedin.com/feed/", }, } return self.track( eventBody, { "appId": "com.linkedin.flagship3.d_web", "eventName": "ProfileViewEvent", "topicName": "ProfileViewEvent", }, )
def get_profile(self, public_id=None, urn_id=None): """ Return data for a single profile. [public_id] - public identifier i.e. tom-quirk-1928345 [urn_id] - id provided by the related URN """ # NOTE this still works for now, but will probably eventually have to be converted to # https://www.linkedin.com/voyager/api/identity/profiles/ACoAAAKT9JQBsH7LwKaE9Myay9WcX8OVGuDq9Uw res = self._fetch(f"/identity/profiles/{public_id or urn_id}/profileView") data = res.json() if data and "status" in data and data["status"] != 200: self.logger.info("request failed: {}".format(data["message"])) return {} # massage [profile] data profile = data["profile"] if "miniProfile" in profile: if "picture" in profile["miniProfile"]: profile["displayPictureUrl"] = profile["miniProfile"]["picture"][ "com.linkedin.common.VectorImage" ]["rootUrl"] profile["profile_id"] = get_id_from_urn(profile["miniProfile"]["entityUrn"]) del profile["miniProfile"] del profile["defaultLocale"] del profile["supportedLocales"] del profile["versionTag"] del profile["showEducationOnProfileTopCard"] # massage [experience] data experience = data["positionView"]["elements"] for item in experience: if "company" in item and "miniCompany" in item["company"]: if "logo" in item["company"]["miniCompany"]: logo = item["company"]["miniCompany"]["logo"].get( "com.linkedin.common.VectorImage" ) if logo: item["companyLogoUrl"] = logo["rootUrl"] del item["company"]["miniCompany"] profile["experience"] = experience # massage [skills] data # skills = [item["name"] for item in data["skillView"]["elements"]] # profile["skills"] = skills profile["skills"] = self.get_profile_skills(public_id=public_id, urn_id=urn_id) # massage [education] data education = data["educationView"]["elements"] for item in education: if "school" in item: if "logo" in item["school"]: item["school"]["logoUrl"] = item["school"]["logo"][ "com.linkedin.common.VectorImage" ]["rootUrl"] del item["school"]["logo"] profile["education"] = education # massage [languages] data languages = data["languageView"]["elements"] for item in languages: del item["entityUrn"] profile["languages"] = languages # massage [publications] data publications = data["publicationView"]["elements"] for item in publications: del item["entityUrn"] for author in item.get("authors", []): del author["entityUrn"] profile["publications"] = publications # massage [certifications] data certifications = data["certificationView"]["elements"] for item in certifications: del item["entityUrn"] profile["certifications"] = certifications # massage [volunteer] data volunteer = data["volunteerExperienceView"]["elements"] for item in volunteer: del item["entityUrn"] profile["volunteer"] = volunteer # massage [honors] data honors = data["honorView"]["elements"] for item in honors: del item["entityUrn"] profile["honors"] = honors return profile
def search_people( self, keywords=None, connection_of=None, network_depth=None, current_company=None, past_companies=None, nonprofit_interests=None, profile_languages=None, regions=None, industries=None, schools=None, title=None, include_private_profiles=False, # profiles without a public id, "Linkedin Member" limit=None, ): """ Do a people search. """ filters = ["resultType->PEOPLE"] if connection_of: filters.append(f"connectionOf->{connection_of}") if network_depth: filters.append(f"network->{network_depth}") if regions: filters.append(f'geoRegion->{"|".join(regions)}') if industries: filters.append(f'industry->{"|".join(industries)}') if current_company: filters.append(f'currentCompany->{"|".join(current_company)}') if past_companies: filters.append(f'pastCompany->{"|".join(past_companies)}') if profile_languages: filters.append(f'profileLanguage->{"|".join(profile_languages)}') if nonprofit_interests: filters.append(f'nonprofitInterest->{"|".join(nonprofit_interests)}') if schools: filters.append(f'schools->{"|".join(schools)}') if title: filters.append(f"title->{title}") params = {"filters": "List({})".format(",".join(filters))} if keywords: params["keywords"] = keywords data = self.search(params, limit=limit) results = [] for item in data: if "publicIdentifier" not in item: continue results.append( { "urn_id": get_id_from_urn(item.get("targetUrn")), "distance": item.get("memberDistance", {}).get("value"), "public_id": item.get("publicIdentifier"), } ) return results
def search_employees( self, params={}, current_company_urn_id="", title="", max=12, start=0 ): """ Do a search. """ people = [] default_params = { "count": "12", "educationEndYear": "List()", "educationStartYear": "List()", "facetCurrentCompany": f"List({current_company_urn_id})", "facetCurrentFunction": "List()", "facetFieldOfStudy": "List()", "facetGeoRegion": "List()", "facetNetwork": "List()", "facetSchool": "List()", "facetSkillExplicit": "List()", "keywords": f"List({title})", "maxFacetValues": "15", "origin": "organization", "q": "people", "start": f"{start}", "supportedFacets": "List(GEO_REGION,SCHOOL,CURRENT_COMPANY,CURRENT_FUNCTION,FIELD_OF_STUDY,SKILL_EXPLICIT,NETWORK)", } default_params.update(params) n_attempts = 0 while len(people) < max: length_before = len(people) res = self._fetch( f"/search/hits?{urlencode(default_params, safe='(),')}", headers={"accept": "application/vnd.linkedin.normalized+json+2.1"}, ) data = res.json() if not data or not data.get("included"): if data.get("status") == "429": print("Reached limit") return -1 break for person in data["included"]: public_id = person.get("publicIdentifier") if public_id: people.append( { "public_id": public_id, "profile_urn_id": get_id_from_urn(person.get("objectUrn")), "first_name": person.get("firstName"), "last_name": person.get("lastName"), "title": person.get("occupation"), } ) if length_before == len(people): n_attempts += 1 if n_attempts > 3: break default_params["start"] = str(int(default_params["start"]) + 12) return people
def search_jobs(self, params, max=25, start=0): """ Do a search. """ jobs = {} companies = {} default_params = { "count": "25", "start": str(start), "decorationId": "com.linkedin.voyager.deco.jserp.WebJobSearchHit-22", "facetEnabled": "false", "isRequestPrefetch": "true", "keywords": "", "origin": "SEARCH_ON_JOBS_HOME_PREFETCH", "q": "jserpAll", "query": "search", "topNRequestedFlavors": "List(HIDDEN_GEM,IN_NETWORK,SCHOOL_RECRUIT,COMPANY_RECRUIT,SALARY," "JOB_SEEKER_QUALIFIED,PREFERRED_COMMUTE)", } default_params.update(params) n_attempts = 0 while len(jobs) < max: length_before = len(jobs) res = self._fetch( f"/search/hits?{urlencode(default_params, safe='(),')}", headers={"accept": "application/vnd.linkedin.normalized+json+2.1"}, ) data = res.json() if not data or not data["included"]: break for job in data["included"]: company_details = job.get("companyDetails") if company_details: company_urn = company_details.get("company") company_urn_id = get_id_from_urn(company_urn) if company_urn else "" job_urn_id = get_id_from_urn(job["entityUrn"]) if not jobs.get(job_urn_id): jobs[job_urn_id] = { "title": job.get("title"), "companyId": company_urn_id, "location": job.get("formattedLocation"), "listedAt": job.get("listedAt"), "expireAt": job.get("expireAt"), } apply_method = job.get("applyMethod") if apply_method: company_apply_url = apply_method.get("companyApplyUrl") easy_apply_url = apply_method.get("easyApplyUrl") jobs[job_urn_id]["url"] = ( company_apply_url if company_apply_url else easy_apply_url ) elif "fs_normalized_company" in job.get("entityUrn"): company_urn_id = get_id_from_urn(job.get("entityUrn")) if not companies.get(company_urn_id): companies[company_urn_id] = {"name": job.get("name")} if length_before == len(jobs): n_attempts += 1 if n_attempts > 3: break default_params["start"] = str(int(default_params["start"]) + 25) return jobs, companies