### Install Project Dependencies Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Install the necessary Python packages for both general use and development. ```bash pip install -r requirements.txt pip install -r requirements-dev.txt ``` -------------------------------- ### Install Test Dependencies Source: https://github.com/joeyism/linkedin_scraper/blob/master/tests/README.md Install necessary Python packages for running tests. Ensure requirements.txt is available. ```bash pip install -r requirements.txt ``` -------------------------------- ### Set Up Environment Variables for Testing Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Copy the example environment file and edit it to include LinkedIn credentials for integration tests. ```bash cp .env.example .env # Edit .env and add your LinkedIn credentials (optional, for integration tests) ``` -------------------------------- ### Create and Activate Virtual Environment Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Create a Python virtual environment to isolate project dependencies. Activate it before installing packages. ```bash python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate ``` -------------------------------- ### Install linkedin_scraper and Playwright Browsers Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Install the library using pip and download the necessary Chromium browser binaries for Playwright. ```bash pip install linkedin-scraper playwright install chromium ``` -------------------------------- ### Install Playwright Browsers Source: https://github.com/joeyism/linkedin_scraper/blob/master/tests/README.md Install the necessary browser binaries for Playwright, which is used by the scraper. This is crucial if Playwright is not found. ```bash playwright install chromium ``` -------------------------------- ### Example Commit Message Structure Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Follow this structure for clear and informative commit messages, including a summary and detailed changes. ```text Add support for scraping job descriptions - Extract full job description text - Parse job requirements section - Add tests for job description parsing ``` -------------------------------- ### Install Old Selenium Version Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Command to install a specific older version (2.11.2) of the library if the Selenium-based functionality is still required. ```bash pip install linkedin-scraper==2.11.2 ``` -------------------------------- ### Example Integration Test Source: https://github.com/joeyism/linkedin_scraper/blob/master/tests/README.md An example of an integration test using pytest. It requires the 'browser_with_session' and 'silent_callback' fixtures and tests the PersonScraper. ```python import pytest from linkedin_scraper import PersonScraper @pytest.mark.integration @pytest.mark.asyncio async def test_my_scraper(browser_with_session, silent_callback): scraper = PersonScraper(browser_with_session.page, callback=silent_callback) person = await scraper.scrape("https://linkedin.com/in/profile") assert person.name is not None ``` -------------------------------- ### GitHub Actions Workflow for Tests Source: https://github.com/joeyism/linkedin_scraper/blob/master/TESTING.md Example GitHub Actions workflow snippets for running unit and integration tests in a CI/CD pipeline. Unit tests are run without integration tests, while integration tests require secrets. ```yaml # Example GitHub Actions workflow - name: Run unit tests run: pytest -m "not integration" -v # Integration tests should be run separately with secrets - name: Run integration tests run: pytest -m integration -v env: LINKEDIN_SESSION: ${{ secrets.LINKEDIN_SESSION }} ``` -------------------------------- ### Utility Functions for Custom Scraper Extensions in Python Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Provides examples of using low-level utility functions from `linkedin_scraper.core.utils` for custom scraper extensions. Demonstrates the `@retry_async` decorator for handling transient errors and various page interaction utilities like scrolling, clicking 'see more' buttons, closing modals, and safely extracting text. It also shows how to use `detect_rate_limit` which raises a `RateLimitError` if the page is throttled. ```python import asyncio from playwright.async_api import async_playwright from linkedin_scraper.core.utils import ( detect_rate_limit, extract_text_safe, scroll_to_bottom, scroll_to_half, click_see_more_buttons, handle_modal_close, retry_async, ) # retry_async decorator @retry_async(max_attempts=3, backoff=2.0, exceptions=(Exception,)) async def flaky_operation(): raise ValueError("transient error") # Usage in a custom scraper async def example(page): await scroll_to_half(page) await scroll_to_bottom(page, pause_time=1.0, max_scrolls=5) clicked = await click_see_more_buttons(page, max_attempts=10) closed = await handle_modal_close(page) name = await extract_text_safe(page, "h1", default="Unknown", timeout=3000) await detect_rate_limit(page) # raises RateLimitError if throttled print(name, clicked, closed) ``` -------------------------------- ### Create LinkedIn Session via Manual Login Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Initiates a browser session for manual LinkedIn login and saves it to a file. This is useful for initial authentication setup. Ensure the timeout is sufficient for login. ```python from linkedin_scraper import BrowserManager, wait_for_manual_login async def create_session(): async with BrowserManager(headless=False) as browser: # Navigate to LinkedIn await browser.page.goto("https://www.linkedin.com/login") # Wait for manual login (opens browser) print("Please log in to LinkedIn...") await wait_for_manual_login(browser.page, timeout=300) # Save session await browser.save_session("session.json") print("✓ Session saved!") asyncio.run(create_session()) ``` -------------------------------- ### Write New Unit and Integration Tests Source: https://github.com/joeyism/linkedin_scraper/blob/master/TESTING.md Example of writing new tests, including a basic unit test for a Person model and an integration test for actual LinkedIn profile scraping. Integration tests should be marked with `@pytest.mark.integration`. ```python import pytest def test_person_model(): """Test Person model serialization""" person = Person(name="John Doe", location="New York") assert person.name == "John Doe" assert person.to_dict()["name"] == "John Doe" @pytest.mark.integration async def test_person_scraper_real(): """Test actual LinkedIn profile scraping""" # Requires valid session async with BrowserManager() as browser: await browser.load_session("linkedin_session.json") scraper = PersonScraper(browser.page) person = await scraper.scrape("https://linkedin.com/in/...") assert person.name is not None ``` -------------------------------- ### Define Custom Progress Callback Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Provides a template for creating custom progress callbacks by inheriting from ProgressCallback. Allows defining actions for start, progress, completion, and errors. ```python from linkedin_scraper import ProgressCallback class MyCallback(ProgressCallback): async def on_start(self, scraper_type: str, url: str): print(f"Starting {scraper_type} scraping: {url}") async def on_progress(self, message: str, percent: int): print(f"[{percent}%] {message}") async def on_complete(self, scraper_type: str, url: str): print(f"Completed {scraper_type}: {url}") async def on_error(self, error: Exception): print(f"Error: {error}") ``` -------------------------------- ### LinkedIn Scraper Data Models in Python Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Illustrates the usage of Pydantic v2 data models provided by the library for representing LinkedIn entities like Person, Job, and Post. Shows how to instantiate these models with data and utilize their `.to_dict()` and `.to_json()` helper methods. Includes examples for Person, Experience, Education, Accomplishment, Interest, Contact, Company, CompanySummary, Employee, Job, and Post. ```python from linkedin_scraper.models import ( Person, Experience, Education, Accomplishment, Interest, Contact, Company, CompanySummary, Employee, Job, Post, ) # Person fields p = Person( linkedin_url="https://www.linkedin.com/in/example/", name="Jane Doe", location="New York", about="Software engineer.", open_to_work=True, experiences=[ Experience( position_title="Staff Engineer", institution_name="Acme", linkedin_url="https://www.linkedin.com/company/acme/", from_date="Jan 2020", to_date="Present", duration="4 yrs", location="Remote", description="Led platform team.", ) ], educations=[ Education(institution_name="MIT", degree="B.S. Computer Science", from_date="2014", to_date="2018") ], accomplishments=[ Accomplishment(category="certification", title="AWS Solutions Architect", issuer="Amazon", issued_date="Jan 2023") ], contacts=[Contact(type="email", value="jane@example.com")], ) print(p.company) # "Acme" print(p.job_title) # "Staff Engineer" print(p.to_json(indent=2)) # full JSON # Job fields j = Job( linkedin_url="https://www.linkedin.com/jobs/view/123/", job_title="Data Scientist", company="DataCo", location="Chicago, IL", posted_date="1 week ago", applicant_count="54 applicants", job_description="We are looking for..." ) print(j.to_dict()) # Post fields post = Post( linkedin_url="https://www.linkedin.com/feed/update/urn:li:activity:123/", urn="urn:li:activity:123", text="Excited to announce...", posted_date="2d", reactions_count=542, comments_count=28, reposts_count=11, image_urls=["https://media.licdn.com/..."] ) print(repr(post)) ``` -------------------------------- ### Refresh LinkedIn Session Source: https://github.com/joeyism/linkedin_scraper/blob/master/TESTING.md Command to refresh your LinkedIn session if integration tests fail due to authentication errors. Refer to README for detailed setup instructions. ```bash # Refresh your session (see README for setup instructions) ``` -------------------------------- ### Clone Repository and Navigate Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Clone the project repository and navigate into the project directory to begin development. ```bash git clone https://github.com/joeyism/linkedin_scraper.git cd linkedin_scraper ``` -------------------------------- ### Configure Browser Options Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Set browser options like headless mode, slow motion, viewport size, and user agent. ```python browser = BrowserManager( headless=False, # Show browser window slow_mo=100, # Slow down operations (ms) viewport={"width": 1920, "height": 1080}, user_agent="Custom User Agent" ) ``` -------------------------------- ### Programmatic Login with Credentials Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Log in programmatically using email and password, optionally reading from environment variables or a .env file. Includes an option to warm up the browser by visiting other sites first. ```python import asyncio, os from linkedin_scraper import BrowserManager, login_with_credentials async def main(): async with BrowserManager(headless=False) as browser: await login_with_credentials( browser.page, email=os.getenv("LINKEDIN_EMAIL"), # or set in .env password=os.getenv("LINKEDIN_PASSWORD"), # or set in .env timeout=30000, warm_up=True # visits google.com, wikipedia.org, github.com first ) await browser.save_session("session.json") print("Logged in and session saved") asyncio.run(main()) ``` -------------------------------- ### Run All Tests Source: https://github.com/joeyism/linkedin_scraper/blob/master/tests/README.md Execute all tests in the project. This includes both unit and integration tests. ```bash pytest ``` -------------------------------- ### Run Quick Unit Tests Source: https://github.com/joeyism/linkedin_scraper/blob/master/TESTING.md Execute fast unit tests that do not require LinkedIn authentication. This is recommended for rapid development cycles. ```bash pytest tests/ -v -m "not integration" ``` -------------------------------- ### Run Full Test Suite Source: https://github.com/joeyism/linkedin_scraper/blob/master/TESTING.md Execute all tests, including integration tests, which require a valid LinkedIn session. Ensure your session is set up as per README instructions. ```bash # First, create a valid session # (See README for session setup instructions) # Run all tests including integration tests pytest tests/ -v ``` -------------------------------- ### Run Unit Tests with Short Traceback Source: https://github.com/joeyism/linkedin_scraper/blob/master/TESTING.md Execute unit tests with a shortened traceback for a faster feedback loop during development. This command is useful for quickly identifying test failures. ```bash pytest -m "not integration" --tb=short ``` -------------------------------- ### load_credentials_from_env Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Read LINKEDIN_EMAIL / LINKEDIN_USERNAME and LINKEDIN_PASSWORD from a .env file, returning a (email, password) tuple. ```APIDOC ## load_credentials_from_env Read `LINKEDIN_EMAIL` / `LINKEDIN_USERNAME` and `LINKEDIN_PASSWORD` from a `.env` file, returning a `(email, password)` tuple. ```python # .env file: # LINKEDIN_EMAIL=me@example.com # LINKEDIN_PASSWORD=s3cr3t from linkedin_scraper import load_credentials_from_env email, password = load_credentials_from_env() print(email) # me@example.com print(password) # s3cr3t ``` ``` -------------------------------- ### Stage and Commit Changes Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Stage all modified files and commit them with a descriptive message. ```bash git add . git commit -m "Your descriptive commit message" ``` -------------------------------- ### Run Tests with Coverage Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Execute tests and generate a code coverage report for the `linkedin_scraper` module. ```bash pytest --cov=linkedin_scraper ``` -------------------------------- ### Pytest Commands Reference Source: https://github.com/joeyism/linkedin_scraper/blob/master/TESTING.md A collection of common pytest commands for running specific test suites, files, or individual tests, with options for coverage and verbose output. ```bash # Run only unit tests (fast) pytest -m "not integration" -v ``` ```bash # Run specific test file pytest tests/test_person_scraper.py -v ``` ```bash # Run specific test pytest tests/test_person_scraper.py::test_person_model_to_dict -v ``` ```bash # Run with coverage pytest --cov=linkedin_scraper -v ``` ```bash # Run with verbose output pytest -v -s ``` -------------------------------- ### Safe Scraping with Error Handling in Python Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Demonstrates how to use a try-except block to gracefully handle various exceptions that may occur during LinkedIn scraping, such as authentication errors, rate limiting, profile not found, network issues, and general scraping failures. It suggests refreshing session data for authentication errors and notes the `suggested_wait_time` attribute for rate limit errors. ```python import asyncio from linkedin_scraper import ( BrowserManager, PersonScraper, AuthenticationError, RateLimitError, ProfileNotFoundError, NetworkError, ScrapingError ) async def safe_scrape(url: str): try: async with BrowserManager(headless=True) as browser: await browser.load_session("session.json") scraper = PersonScraper(browser.page) return await scraper.scrape(url) except AuthenticationError as e: print(f"Not authenticated — refresh session.json. Detail: {e}") except RateLimitError as e: print(f"Rate limited. Suggested wait: {e.suggested_wait_time}s. Detail: {e}") except ProfileNotFoundError as e: print(f"Profile/page not found (private or removed): {e}") except NetworkError as e: print(f"Browser/network error: {e}") except ScrapingError as e: print(f"Scraping failed (page structure changed?): {e}") asyncio.run(safe_scrape("https://www.linkedin.com/in/williamhgates/")) ``` -------------------------------- ### Load LinkedIn Credentials from .env Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Reads LINKEDIN_EMAIL and LINKEDIN_PASSWORD from a .env file. Ensure the .env file is correctly formatted with these variables. ```python # .env file: # LINKEDIN_EMAIL=me@example.com # LINKEDIN_PASSWORD=s3cr3t from linkedin_scraper import load_credentials_from_env email, password = load_credentials_from_env() print(email) # me@example.com print(password) # s3cr3t ``` -------------------------------- ### Authenticate Using a Pre-obtained Session Cookie Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Authenticate by providing the value of the `li_at` session cookie directly. ```python import asyncio from linkedin_scraper import BrowserManager, login_with_cookie async def main(): async with BrowserManager(headless=True) as browser: await login_with_cookie( browser.page, cookie_value="AQEDATxxxxxxxxxxxxxx" # value of li_at cookie ) await browser.save_session("session_from_cookie.json") print("Authenticated via cookie") asyncio.run(main()) ``` -------------------------------- ### BrowserManager Usage Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Demonstrates how to use the BrowserManager async context manager to handle the Playwright browser lifecycle, access browser components, and manage additional pages and cookies. ```APIDOC ## BrowserManager Async context manager wrapping the full Playwright Chromium lifecycle. Exposes `.page`, `.context`, and `.browser` properties and handles clean teardown on exit. ```python import asyncio from linkedin_scraper import BrowserManager async def main(): # headless=False shows the window; slow_mo adds ms delay between actions async with BrowserManager( headless=False, slow_mo=50, viewport={"width": 1920, "height": 1080}, user_agent="Mozilla/5.0 (compatible; MyBot/1.0)" ) as browser: print(browser.page) # print(browser.context) # print(browser.browser) # # Open an additional tab extra_page = await browser.new_page() # Set a raw cookie manually await browser.set_cookie("custom_cookie", "value123", domain=".linkedin.com") asyncio.run(main()) ``` ``` -------------------------------- ### Run Tests with Verbose Output Source: https://github.com/joeyism/linkedin_scraper/blob/master/tests/README.md Execute all tests with verbose output, showing each test name and its status. ```bash pytest -v ``` -------------------------------- ### Basic Person Profile Scraping Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Demonstrates how to initialize the browser, load a session, and scrape a person's profile using PersonScraper. Accesses basic profile attributes like name, headline, and location. ```python import asyncio from linkedin_scraper import BrowserManager, PersonScraper async def main(): # Initialize browser async with BrowserManager(headless=False) as browser: # Load authenticated session await browser.load_session("session.json") # Create scraper scraper = PersonScraper(browser.page) # Scrape a profile person = await scraper.scrape("https://linkedin.com/in/williamhgates/") # Access data print(f"Name: {person.name}") print(f"Headline: {person.headline}") print(f"Location: {person.location}") print(f"Experiences: {len(person.experiences)}") print(f"Education: {len(person.educations)}") asyncio.run(main()) ``` -------------------------------- ### Custom Progress Callback for linkedin_scraper Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Demonstrates creating a custom progress callback by subclassing `ProgressCallback` and integrating it with a scraper using `MultiCallback` to combine it with built-in callbacks. ```python import asyncio, json from linkedin_scraper import ( BrowserManager, PersonScraper, ConsoleCallback, JSONLogCallback, MultiCallback, ProgressCallback ) class SlackCallback(ProgressCallback): async def on_start(self, scraper_type: str, url: str) -> None: print(f"[Slack] Starting {scraper_type}: {url}") async def on_progress(self, message: str, percent: int) -> None: if percent in (25, 50, 75, 100): print(f"[Slack] {percent}% — {message}") async def on_complete(self, scraper_type: str, result) -> None: print(f"[Slack] Done scraping {scraper_type}") async def on_error(self, error: Exception) -> None: print(f"[Slack] ERROR: {error}") async def main(): callback = MultiCallback( ConsoleCallback(verbose=False), # prints milestones to stdout JSONLogCallback("scrape_log.jsonl"), # appends JSON lines to file SlackCallback(), # custom callback ) async with BrowserManager(headless=True) as browser: await browser.load_session("session.json") scraper = PersonScraper(browser.page, callback=callback) person = await scraper.scrape("https://www.linkedin.com/in/williamhgates/") # scrape_log.jsonl now contains entries like: # {"timestamp": "2024-01-15T10:00:00", "event_type": "start", "scraper_type": "person", "url": "..."} # {"timestamp": "2024-01-15T10:00:05", "event_type": "progress", "message": "Got name: Bill Gates", "percent": 20} # {"timestamp": "2024-01-15T10:00:30", "event_type": "complete", "scraper_type": "person"} asyncio.run(main()) ``` -------------------------------- ### Login to LinkedIn Programmatically Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Logs into LinkedIn using provided credentials stored as environment variables. Saves the session for subsequent use. Ensure LINKEDIN_EMAIL and LINKEDIN_PASSWORD are set. ```python from linkedin_scraper import BrowserManager, login_with_credentials import os async def login(): async with BrowserManager(headless=False) as browser: # Login with credentials await login_with_credentials( browser.page, username=os.getenv("LINKEDIN_EMAIL"), password=os.getenv("LINKEDIN_PASSWORD") ) # Save session for reuse await browser.save_session("session.json") asyncio.run(login()) ``` -------------------------------- ### Progress Callbacks Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Allows users to hook into the scraping process by providing callback functions. This enables custom handling of progress, completion, and errors during scraping operations. ```APIDOC ## Progress Callbacks ### Description All scrapers accept an optional `callback` parameter. Four built-in implementations are provided; custom callbacks subclass `ProgressCallback`. ### Callback Interface `ProgressCallback` provides the following methods: - `on_start(scraper_type: str, url: str)`: Called when a scrape operation begins. - `on_progress(message: str, percent: int)`: Called periodically to report progress. - `on_complete(scraper_type: str, result)`: Called when a scrape operation finishes successfully. - `on_error(error: Exception)`: Called if an error occurs during scraping. ### Built-in Callbacks - `ConsoleCallback`: Prints milestones to standard output. - `JSONLogCallback`: Appends JSON lines to a specified file. - `MultiCallback`: Allows chaining multiple callbacks. ### Custom Callback Example (SlackCallback) ```python import asyncio, json from linkedin_scraper import ( BrowserManager, PersonScraper, ConsoleCallback, JSONLogCallback, MultiCallback, ProgressCallback ) class SlackCallback(ProgressCallback): async def on_start(self, scraper_type: str, url: str) -> None: print(f"[Slack] Starting {scraper_type}: {url}") async def on_progress(self, message: str, percent: int) -> None: if percent in (25, 50, 75, 100): print(f"[Slack] {percent}% — {message}") async def on_complete(self, scraper_type: str, result) -> None: print(f"[Slack] Done scraping {scraper_type}") async def on_error(self, error: Exception) -> None: print(f"[Slack] ERROR: {error}") async def main(): callback = MultiCallback( ConsoleCallback(verbose=False), # prints milestones to stdout JSONLogCallback("scrape_log.jsonl"), # appends JSON lines to file SlackCallback(), # custom callback ) async with BrowserManager(headless=True) as browser: await browser.load_session("session.json") scraper = PersonScraper(browser.page, callback=callback) person = await scraper.scrape("https://www.linkedin.com/in/williamhgates/") # scrape_log.jsonl now contains entries like: # {"timestamp": "2024-01-15T10:00:00", "event_type": "start", "scraper_type": "person", "url": "..."} # {"timestamp": "2024-01-15T10:00:05", "event_type": "progress", "message": "Got name: Bill Gates", "percent": 20} # {"timestamp": "2024-01-15T10:00:30", "event_type": "complete", "scraper_type": "person"} asyncio.run(main()) ``` ``` -------------------------------- ### Run Specific Test for Debugging Source: https://github.com/joeyism/linkedin_scraper/blob/master/TESTING.md Isolate and run a single, specific test case to focus on debugging a particular failure. This command helps in quickly pinpointing and resolving issues in a targeted manner. ```bash pytest tests/test_person_scraper.py::test_person_model_to_dict -v ``` -------------------------------- ### login_with_credentials Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Programmatically logs into LinkedIn using email and password. It can read credentials from environment variables and optionally warms up the browser by visiting other sites first. ```APIDOC ## login_with_credentials Programmatically log in using an email/password pair. Reads credentials from `.env` if not passed directly. Optionally warms up the browser by visiting benign sites before hitting LinkedIn. ```python import asyncio, os from linkedin_scraper import BrowserManager, login_with_credentials async def main(): async with BrowserManager(headless=False) as browser: await login_with_credentials( browser.page, email=os.getenv("LINKEDIN_EMAIL"), # or set in .env password=os.getenv("LINKEDIN_PASSWORD"), # or set in .env timeout=30000, warm_up=True # visits google.com, wikipedia.org, github.com first ) await browser.save_session("session.json") print("Logged in and session saved") asyncio.run(main()) ``` ``` -------------------------------- ### Run Only Unit Tests Source: https://github.com/joeyism/linkedin_scraper/blob/master/tests/README.md Execute only unit tests, which do not require a LinkedIn session. This is faster and useful for CI/CD. ```bash pytest -m unit ``` -------------------------------- ### Format Code with Black Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Apply the Black code formatter to the `linkedin_scraper` directory to ensure consistent code style. ```bash black linkedin_scraper/ ``` -------------------------------- ### Create a New Feature Branch Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Create a new Git branch for developing a feature, following the `feature/your-feature-name` naming convention. ```bash git checkout -b feature/your-feature-name ``` -------------------------------- ### Save and Load Browser Session for Authentication Persistence Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Persist and restore the browser's storage state, including cookies and localStorage, to avoid re-authentication in subsequent runs. ```python import asyncio from linkedin_scraper import BrowserManager async def create_and_reuse_session(): # --- First run: authenticate and save --- async with BrowserManager(headless=False) as browser: await browser.page.goto("https://www.linkedin.com/login") from linkedin_scraper import wait_for_manual_login await wait_for_manual_login(browser.page, timeout=300000) await browser.save_session("linkedin_session.json") # Saved: {"cookies": [...], "origins": [...]} # --- Subsequent runs: restore session --- async with BrowserManager(headless=True) as browser: await browser.load_session("linkedin_session.json") # browser._is_authenticated is now True print("Authenticated:", browser.is_authenticated) # True asyncio.run(create_and_reuse_session()) ``` -------------------------------- ### Run Specific Test File Source: https://github.com/joeyism/linkedin_scraper/blob/master/tests/README.md Execute all tests within a specific test file, such as test_person_scraper.py. ```bash pytest tests/test_person_scraper.py ``` -------------------------------- ### Create LinkedIn Session Source: https://github.com/joeyism/linkedin_scraper/blob/master/tests/README.md Generate a linkedin_session.json file required for integration tests. This script authenticates with LinkedIn. ```bash python setup_session.py ``` -------------------------------- ### Run Specific Test File with Pytest Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Execute tests from a particular file, such as `test_person.py`. ```bash pytest tests/test_person.py ``` -------------------------------- ### Scrape Person Profile with Progress Callback Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Scrapes a person's LinkedIn profile and displays progress using a ConsoleCallback. Requires a session file for authentication. Custom callbacks can be implemented for more control. ```python from linkedin_scraper import ConsoleCallback, PersonScraper async def scrape_with_progress(): callback = ConsoleCallback() # Prints progress to console async with BrowserManager(headless=False) as browser: await browser.load_session("session.json") scraper = PersonScraper(browser.page, callback=callback) person = await scraper.scrape("https://linkedin.com/in/williamhgates/") asyncio.run(scrape_with_progress()) ``` -------------------------------- ### login_with_cookie Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Authenticates the user by providing a pre-obtained `li_at` session cookie value. ```APIDOC ## login_with_cookie Authenticate using a pre-obtained `li_at` session cookie value. ```python import asyncio from linkedin_scraper import BrowserManager, login_with_cookie async def main(): async with BrowserManager(headless=True) as browser: await login_with_cookie( browser.page, cookie_value="AQEDATxxxxxxxxxxxxxx" # value of li_at cookie ) await browser.save_session("session_from_cookie.json") print("Authenticated via cookie") asyncio.run(main()) ``` ``` -------------------------------- ### Migrate from Selenium to Playwright Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Illustrates the code changes required to migrate from the older Selenium-based version to the new Playwright-based version of the scraper. Note the use of async/await and BrowserManager. ```python from linkedin_scraper import Person person = Person("https://linkedin.com/in/username", driver=driver) print(person.name) ``` ```python import asyncio from linkedin_scraper import BrowserManager, PersonScraper async def main(): async with BrowserManager() as browser: await browser.load_session("session.json") scraper = PersonScraper(browser.page) person = await scraper.scrape("https://linkedin.com/in/username") print(person.name) asyncio.run(main()) ``` -------------------------------- ### Check for Type Issues with Mypy Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Run Mypy to perform static type checking on the `linkedin_scraper` directory. ```bash mypy linkedin_scraper/ ``` -------------------------------- ### Run Specific Test Source: https://github.com/joeyism/linkedin_scraper/blob/master/tests/README.md Execute a single, specific test function within a test file, identified by its path and function name. ```bash pytest tests/test_person_scraper.py::test_person_scraper_basic ``` -------------------------------- ### Skip Slow Tests Source: https://github.com/joeyism/linkedin_scraper/blob/master/tests/README.md Execute tests, excluding those marked as 'slow'. This is useful for quick feedback loops. ```bash pytest -m "not slow" ``` -------------------------------- ### Run Only Integration Tests Source: https://github.com/joeyism/linkedin_scraper/blob/master/tests/README.md Execute only integration tests, which require a valid LinkedIn session. These tests interact with the live LinkedIn website. ```bash pytest -m integration ``` -------------------------------- ### Debug Test Failures with Verbose Output Source: https://github.com/joeyism/linkedin_scraper/blob/master/TESTING.md Run tests with the `-s` flag to enable verbose output, allowing you to see print statements and debug test failures more effectively. This is useful when a test fails unexpectedly. ```bash pytest tests/test_person_scraper.py -v -s ``` -------------------------------- ### Search Jobs with LinkedIn Scraper Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Scrapes job listings based on keywords and location. Requires a session file for authentication. Ensure BrowserManager is properly initialized. ```python from linkedin_scraper import JobSearchScraper async def search_jobs(): async with BrowserManager(headless=False) as browser: await browser.load_session("session.json") scraper = JobSearchScraper(browser.page) jobs = await scraper.search( keywords="Python Developer", location="San Francisco", limit=10 ) for job in jobs: print(f"{job.title} at {job.company}") print(f"Location: {job.location}") print(f"Link: {job.linkedin_url}") print("---") asyncio.run(search_jobs()) ``` -------------------------------- ### Check for Issues with Flake8 Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Use Flake8 to check the `linkedin_scraper` directory for PEP 8 style violations and programming errors. ```bash flake8 linkedin_scraper/ ``` -------------------------------- ### Manage Playwright Browser Lifecycle with BrowserManager Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Use BrowserManager as an async context manager to handle the Playwright Chromium lifecycle, including opening new pages and setting cookies. ```python import asyncio from linkedin_scraper import BrowserManager async def main(): # headless=False shows the window; slow_mo adds ms delay between actions async with BrowserManager( headless=False, slow_mo=50, viewport={"width": 1920, "height": 1080}, user_agent="Mozilla/5.0 (compatible; MyBot/1.0)" ) as browser: print(browser.page) # print(browser.context) # print(browser.browser) # # Open an additional tab extra_page = await browser.new_page() # Set a raw cookie manually await browser.set_cookie("custom_cookie", "value123", domain=".linkedin.com") asyncio.run(main()) ``` -------------------------------- ### Implement Request Delay Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Add delays between requests using asyncio.sleep to avoid rate limiting. ```python import asyncio await asyncio.sleep(2) # 2 second delay ``` -------------------------------- ### wait_for_manual_login Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Blocks until LinkedIn navigation elements appear, indicating the user has completed login, 2FA, or CAPTCHA manually in the browser window. ```APIDOC ## wait_for_manual_login Blocks until LinkedIn navigation elements appear (i.e., the user has completed login, 2FA, or CAPTCHA manually in the browser window). ```python import asyncio from linkedin_scraper import BrowserManager, wait_for_manual_login async def main(): async with BrowserManager(headless=False) as browser: await browser.page.goto("https://www.linkedin.com/login") print("Please log in manually, including any 2FA...") await wait_for_manual_login(browser.page, timeout=300000) # 5 minutes await browser.save_session("session.json") print("Session saved") asyncio.run(main()) ``` ``` -------------------------------- ### Wait for Manual LinkedIn Login Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Blocks execution until LinkedIn navigation elements appear, indicating the user has completed manual login, 2FA, or CAPTCHA. Use when automated login is not feasible. ```python import asyncio from linkedin_scraper import BrowserManager, wait_for_manual_login async def main(): async with BrowserManager(headless=False) as browser: await browser.page.goto("https://www.linkedin.com/login") print("Please log in manually, including any 2FA...") await wait_for_manual_login(browser.page, timeout=300000) # 5 minutes await browser.save_session("session.json") print("Session saved") asyncio.run(main()) ``` -------------------------------- ### Job Data Model Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Defines the Pydantic model for a scraped LinkedIn job posting. Includes fields for title, company, location, description, employment type, and seniority level. ```python class Job(BaseModel): title: str company: str location: Optional[str] description: Optional[str] employment_type: Optional[str] seniority_level: Optional[str] linkedin_url: str ``` -------------------------------- ### Company Page Scraping Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Shows how to use CompanyScraper to extract information from a LinkedIn company page, including name, industry, size, and a snippet of the 'About Us' section. ```python from linkedin_scraper import CompanyScraper async def scrape_company(): async with BrowserManager(headless=False) as browser: await browser.load_session("session.json") scraper = CompanyScraper(browser.page) company = await scraper.scrape("https://linkedin.com/company/microsoft/") print(f"Company: {company.name}") print(f"Industry: {company.industry}") print(f"Size: {company.company_size}") print(f"About: {company.about_us[:200]}...") asyncio.run(scrape_company()) ``` -------------------------------- ### Push Changes to Remote Fork Source: https://github.com/joeyism/linkedin_scraper/blob/master/CONTRIBUTING.md Push the local feature branch to your remote fork on GitHub. ```bash git push origin feature/your-feature-name ``` -------------------------------- ### BrowserManager.save_session / load_session Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Enables persisting and restoring the browser's storage state (cookies and localStorage) to avoid re-authentication in subsequent runs. ```APIDOC ## BrowserManager.save_session / load_session Persist and restore the full browser storage state (cookies + localStorage) so repeated runs avoid re-authenticating. ```python import asyncio from linkedin_scraper import BrowserManager async def create_and_reuse_session(): # --- First run: authenticate and save --- async with BrowserManager(headless=False) as browser: await browser.page.goto("https://www.linkedin.com/login") from linkedin_scraper import wait_for_manual_login await wait_for_manual_login(browser.page, timeout=300000) await browser.save_session("linkedin_session.json") # Saved: {"cookies": [...], "origins": [...]} # --- Subsequent runs: restore session --- async with BrowserManager(headless=True) as browser: await browser.load_session("linkedin_session.json") # browser._is_authenticated is now True print("Authenticated:", browser.is_authenticated) # True asyncio.run(create_and_reuse_session()) ``` ``` -------------------------------- ### Search LinkedIn Jobs with linkedin_scraper Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Searches LinkedIn jobs by keyword and location, returning a list of job posting URLs. It then iterates through a subset of these URLs to scrape individual job details. ```python import asyncio from linkedin_scraper import BrowserManager, JobSearchScraper, JobScraper async def main(): async with BrowserManager(headless=True) as browser: await browser.load_session("session.json") search = JobSearchScraper(browser.page) job_urls = await search.search( keywords="Python Developer", location="Remote", limit=10 ) # ["https://www.linkedin.com/jobs/view/111.../", ...] job_scraper = JobScraper(browser.page) for url in job_urls[:3]: await asyncio.sleep(2) # polite delay job = await job_scraper.scrape(url) print(f"{job.job_title} @ {job.company} — {job.location}") asyncio.run(main()) ``` -------------------------------- ### Post Data Model Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Defines the Pydantic model for a scraped LinkedIn post. Includes fields for URL, text, posted date, reaction counts, and image URLs. ```python class Post(BaseModel): linkedin_url: Optional[str] urn: Optional[str] text: Optional[str] posted_date: Optional[str] reactions_count: Optional[int] comments_count: Optional[int] reposts_count: Optional[int] image_urls: List[str] ``` -------------------------------- ### Person Data Model Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Defines the Pydantic model for a scraped LinkedIn person profile. Includes fields for name, headline, location, experiences, education, and skills. ```python class Person(BaseModel): name: str headline: Optional[str] location: Optional[str] about: Optional[str] linkedin_url: str experiences: List[Experience] educations: List[Education] skills: List[str] accomplishments: Optional[Accomplishment] ``` -------------------------------- ### Scrape Company Posts with LinkedIn Scraper Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Extracts posts from a specified company LinkedIn page. Requires a session file for authentication. The limit parameter controls the number of posts to scrape. ```python from linkedin_scraper import BrowserManager, CompanyPostsScraper async def scrape_company_posts(): async with BrowserManager(headless=False) as browser: await browser.load_session("session.json") scraper = CompanyPostsScraper(browser.page) posts = await scraper.scrape( "https://linkedin.com/company/microsoft/", limit=10 ) for post in posts: print(f"Posted: {post.posted_date}") print(f"Text: {post.text[:200]}...") print(f"Reactions: {post.reactions_count}") print(f"Comments: {post.comments_count}") print(f"URL: {post.linkedin_url}") print("---") asyncio.run(scrape_company_posts()) ``` -------------------------------- ### Scrape Single Job Posting with linkedin_scraper Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Scrapes a single LinkedIn job posting. Requires a loaded browser session. Returns a Job object with details like title, company, location, description, and applicant count. ```python import asyncio from linkedin_scraper import BrowserManager, JobScraper async def main(): async with BrowserManager(headless=True) as browser: await browser.load_session("session.json") scraper = JobScraper(browser.page) job = await scraper.scrape("https://www.linkedin.com/jobs/view/3987654321/") print(job.job_title) # "Senior Python Engineer" print(job.company) # "Acme Corp" print(job.company_linkedin_url) print(job.location) # "San Francisco, CA" print(job.posted_date) # "2 days ago" print(job.applicant_count) # "142 applicants" print(job.job_description[:300]) # Export print(job.to_json(indent=2)) asyncio.run(main()) ``` -------------------------------- ### Company Data Model Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Defines the Pydantic model for a scraped LinkedIn company profile. Includes fields for name, industry, company size, headquarters, and specialties. ```python class Company(BaseModel): name: str industry: Optional[str] company_size: Optional[str] headquarters: Optional[str] founded: Optional[str] specialties: List[str] about: Optional[str] linkedin_url: str ``` -------------------------------- ### Handle LinkedIn Scraper Errors Source: https://github.com/joeyism/linkedin_scraper/blob/master/README.md Catch specific exceptions like AuthenticationError, RateLimitError, and ProfileNotFoundError during scraping. ```python from linkedin_scraper import ( AuthenticationError, RateLimitError, ProfileNotFoundError ) try: person = await scraper.scrape(url) except AuthenticationError: print("Not logged in - session expired") except RateLimitError: print("Rate limited by LinkedIn") except ProfileNotFoundError: print("Profile not found or private") ``` -------------------------------- ### Scrape Company Posts with linkedin_scraper Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Scrapes posts from a company's LinkedIn feed. Requires a loaded browser session. Returns Post objects with details like text, engagement, date, and image URLs. ```python import asyncio from linkedin_scraper import BrowserManager, CompanyPostsScraper async def main(): async with BrowserManager(headless=True) as browser: await browser.load_session("session.json") scraper = CompanyPostsScraper(browser.page) posts = await scraper.scrape( "https://www.linkedin.com/company/microsoft/", limit=5 ) for post in posts: print(post.urn) # "urn:li:activity:7234567890123456789" print(post.linkedin_url) # "https://www.linkedin.com/feed/update/urn:li:activity:..." print(post.posted_date) # "3d" / "1w" / etc. print(post.reactions_count) # 1420 print(post.comments_count) # 87 print(post.reposts_count) # 34 print(post.text[:100]) print(post.image_urls) # list of CDN image URLs asyncio.run(main()) ``` -------------------------------- ### CompanyScraper.scrape Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Scrape a LinkedIn company page for name, about, website, headquarters, industry, company size, type, founded year, and specialties. ```APIDOC ## CompanyScraper.scrape Scrape a LinkedIn company page for name, about, website, headquarters, industry, company size, type, founded year, and specialties. ```python import asyncio from linkedin_scraper import BrowserManager, CompanyScraper async def main(): async with BrowserManager(headless=True) as browser: await browser.load_session("session.json") scraper = CompanyScraper(browser.page) company = await scraper.scrape("https://www.linkedin.com/company/microsoft/") print(company.name) # "Microsoft" print(company.industry) # "Software Development" print(company.company_size) # "10K+ employees" print(company.headquarters) # "Redmond, Washington" print(company.website) # "https://www.microsoft.com" print(company.about_us[:120]) # Serialise data = company.to_dict() print(data["founded"]) asyncio.run(main()) ``` ``` -------------------------------- ### CompanyPostsScraper.scrape Source: https://context7.com/joeyism/linkedin_scraper/llms.txt Scrapes posts from a company's LinkedIn feed. It returns a list of Post objects, each containing details like text, engagement counts, post date, and image URLs. ```APIDOC ## CompanyPostsScraper.scrape ### Description Scrape posts from a company's LinkedIn feed, returning a list of `Post` objects with text, engagement counts, post date, and image URLs. ### Method Signature `CompanyPostsScraper.scrape(url: str, limit: int = None)` ### Parameters - **url** (str) - Required - The URL of the LinkedIn company page. - **limit** (int) - Optional - The maximum number of posts to scrape. ### Returns - `List[Post]` - A list of Post objects. ### Example ```python import asyncio from linkedin_scraper import BrowserManager, CompanyPostsScraper async def main(): async with BrowserManager(headless=True) as browser: await browser.load_session("session.json") scraper = CompanyPostsScraper(browser.page) posts = await scraper.scrape( "https://www.linkedin.com/company/microsoft/", limit=5 ) for post in posts: print(post.urn) print(post.linkedin_url) print(post.posted_date) print(post.reactions_count) print(post.comments_count) print(post.reposts_count) print(post.text[:100]) print(post.image_urls) asyncio.run(main()) ``` ```