linkedin-playwright-scraper 4.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- linkedin_playwright_scraper-4.0.0/.env.example +8 -0
- linkedin_playwright_scraper-4.0.0/.github/workflows/publish.yml +25 -0
- linkedin_playwright_scraper-4.0.0/.gitignore +45 -0
- linkedin_playwright_scraper-4.0.0/CLAUDE.md +86 -0
- linkedin_playwright_scraper-4.0.0/CONTRIBUTING.md +206 -0
- linkedin_playwright_scraper-4.0.0/FORK_CONTEXT.md +73 -0
- linkedin_playwright_scraper-4.0.0/Justfile +87 -0
- linkedin_playwright_scraper-4.0.0/LICENSE +674 -0
- linkedin_playwright_scraper-4.0.0/MANIFEST.in +39 -0
- linkedin_playwright_scraper-4.0.0/PKG-INFO +502 -0
- linkedin_playwright_scraper-4.0.0/README.md +469 -0
- linkedin_playwright_scraper-4.0.0/TESTING.md +207 -0
- linkedin_playwright_scraper-4.0.0/docs/README.md +46 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/001-playwright-over-official-api.md +36 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/002-headless-false-required.md +40 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/003-js-evaluation-over-locators.md +41 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/004-data-urn-stable-anchor.md +45 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/005-playwright-storage-state.md +43 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/006-pydantic-models.md +43 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/007-async-architecture.md +38 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/008-uv-just-toolchain.md +48 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/009-library-samples-separation.md +59 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/010-single-version-source.md +43 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/011-xvfb-linux-deployment.md +56 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/012-lnkd-url-resolution.md +47 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/013-mcp-integration-architecture.md +62 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/014-cdp-existing-browser.md +55 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/015-local-editable-install.md +51 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/016-rate-limit-avoidance.md +47 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/017-remote-cdp-homelab-chromium.md +41 -0
- linkedin_playwright_scraper-4.0.0/docs/adr/README.md +35 -0
- linkedin_playwright_scraper-4.0.0/docs/integration-linkedin-mcp.md +287 -0
- linkedin_playwright_scraper-4.0.0/docs/local-dev-setup.md +118 -0
- linkedin_playwright_scraper-4.0.0/docs/post-mortem/2026-06-25-feed-repost-aria-label.md +70 -0
- linkedin_playwright_scraper-4.0.0/docs/post-mortem/2026-07-22-rate-limit-hang.md +74 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/__init__.py +106 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/callbacks.py +162 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/core/__init__.py +78 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/core/auth.py +314 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/core/browser.py +265 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/core/exceptions.py +39 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/core/permalink_cache.py +49 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/core/rate_limit_guard.py +122 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/core/utils.py +299 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/models/__init__.py +20 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/models/company.py +80 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/models/job.py +59 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/models/person.py +133 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/models/post.py +54 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/scrapers/__init__.py +19 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/scrapers/base.py +271 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/scrapers/company.py +208 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/scrapers/company_posts.py +346 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/scrapers/feed.py +1760 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/scrapers/job.py +205 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/scrapers/job_search.py +145 -0
- linkedin_playwright_scraper-4.0.0/linkedin_scraper/scrapers/person.py +720 -0
- linkedin_playwright_scraper-4.0.0/pyproject.toml +79 -0
- linkedin_playwright_scraper-4.0.0/samples/create_session.py +75 -0
- linkedin_playwright_scraper-4.0.0/samples/scrape_company.py +45 -0
- linkedin_playwright_scraper-4.0.0/samples/scrape_company_posts.py +41 -0
- linkedin_playwright_scraper-4.0.0/samples/scrape_feed.py +53 -0
- linkedin_playwright_scraper-4.0.0/samples/scrape_jobs.py +51 -0
- linkedin_playwright_scraper-4.0.0/samples/scrape_person.py +56 -0
- linkedin_playwright_scraper-4.0.0/samples/scrape_person_contacts.py +60 -0
- linkedin_playwright_scraper-4.0.0/tests/README.md +152 -0
- linkedin_playwright_scraper-4.0.0/tests/conftest.py +103 -0
- linkedin_playwright_scraper-4.0.0/tests/test_auth.py +25 -0
- linkedin_playwright_scraper-4.0.0/tests/test_browser.py +54 -0
- linkedin_playwright_scraper-4.0.0/tests/test_company_scraper.py +79 -0
- linkedin_playwright_scraper-4.0.0/tests/test_feed_scraper.py +360 -0
- linkedin_playwright_scraper-4.0.0/tests/test_job_scraper.py +85 -0
- linkedin_playwright_scraper-4.0.0/tests/test_person_scraper.py +127 -0
- linkedin_playwright_scraper-4.0.0/uv.lock +2047 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# LinkedIn credentials for scraping
|
|
2
|
+
# Copy this file to .env and fill in your credentials
|
|
3
|
+
|
|
4
|
+
# Use either LINKEDIN_EMAIL or LINKEDIN_USERNAME (both work)
|
|
5
|
+
LINKEDIN_EMAIL=your.email@example.com
|
|
6
|
+
# LINKEDIN_USERNAME=your.email@example.com
|
|
7
|
+
|
|
8
|
+
LINKEDIN_PASSWORD=your_password_here
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-and-publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # required for PyPI trusted publishing (OIDC)
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Install uv
|
|
17
|
+
uses: astral-sh/setup-uv@v3
|
|
18
|
+
|
|
19
|
+
- name: Build distribution
|
|
20
|
+
run: uv build
|
|
21
|
+
|
|
22
|
+
- name: Publish to PyPI
|
|
23
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
24
|
+
with:
|
|
25
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
*.swp
|
|
2
|
+
linkedin_user_scraper.egg-info
|
|
3
|
+
linkedin_scraper.egg-info
|
|
4
|
+
dist
|
|
5
|
+
build
|
|
6
|
+
__pycache__
|
|
7
|
+
*.pyc
|
|
8
|
+
chromedriver
|
|
9
|
+
.DS_Store
|
|
10
|
+
.vscode/
|
|
11
|
+
.env/
|
|
12
|
+
scrape.py
|
|
13
|
+
creds.json
|
|
14
|
+
venv
|
|
15
|
+
*.zip
|
|
16
|
+
.env
|
|
17
|
+
|
|
18
|
+
# Test outputs
|
|
19
|
+
*.log
|
|
20
|
+
test_*.db
|
|
21
|
+
test_linkedin.db
|
|
22
|
+
test_summary.json
|
|
23
|
+
results_*.json
|
|
24
|
+
person_*.json
|
|
25
|
+
*_improved.json
|
|
26
|
+
|
|
27
|
+
# Debug scripts (keep debug_connection_selectors.py)
|
|
28
|
+
debug_*.py
|
|
29
|
+
!debug_connection_selectors.py
|
|
30
|
+
|
|
31
|
+
# Backup files
|
|
32
|
+
*_old.py
|
|
33
|
+
*.backup
|
|
34
|
+
|
|
35
|
+
# Session files (sensitive cookies)
|
|
36
|
+
linkedin_session.json
|
|
37
|
+
|
|
38
|
+
# Build artifacts
|
|
39
|
+
MANIFEST
|
|
40
|
+
MANIFEST.ini
|
|
41
|
+
.pytest_cache/
|
|
42
|
+
|
|
43
|
+
# Basic package build artifacts
|
|
44
|
+
build-basic/
|
|
45
|
+
dist-basic/
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install all dependencies and Playwright browsers
|
|
9
|
+
just install
|
|
10
|
+
|
|
11
|
+
# Run unit tests only (no LinkedIn session required)
|
|
12
|
+
just test
|
|
13
|
+
|
|
14
|
+
# Run a single test file
|
|
15
|
+
just test-file tests/test_feed_scraper.py
|
|
16
|
+
|
|
17
|
+
# Run a single test by name
|
|
18
|
+
just test-one tests/test_feed_scraper.py TestFeedScraperUnit::test_parse_count
|
|
19
|
+
|
|
20
|
+
# Run integration tests (requires linkedin_session.json)
|
|
21
|
+
just test-integration
|
|
22
|
+
|
|
23
|
+
# Create a LinkedIn session file (manual login in browser)
|
|
24
|
+
just session
|
|
25
|
+
|
|
26
|
+
# Format, lint, typecheck + unit tests
|
|
27
|
+
just check
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Toolchain: **uv** manages the virtualenv (`.venv/`) and dependencies (`pyproject.toml`). **just** is the task runner (`Justfile`). Run `just` with no arguments to list all recipes.
|
|
31
|
+
|
|
32
|
+
## Architecture
|
|
33
|
+
|
|
34
|
+
This is an async Playwright-based LinkedIn scraper. All scraping is fully async (asyncio).
|
|
35
|
+
|
|
36
|
+
**Entry point:** `linkedin_scraper/__init__.py` re-exports everything public.
|
|
37
|
+
|
|
38
|
+
### Layer structure
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
core/ Browser lifecycle, auth, utils, exceptions
|
|
42
|
+
scrapers/ One scraper class per LinkedIn surface
|
|
43
|
+
models/ Pydantic models for scraped data
|
|
44
|
+
callbacks.py Progress reporting (SilentCallback, ConsoleCallback, JSONLogCallback, MultiCallback)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `core/`
|
|
48
|
+
|
|
49
|
+
- `browser.py` — `BrowserManager`: async context manager wrapping Playwright. Handles launch, session save/load (`linkedin_session.json` = Playwright storage state), cookie injection.
|
|
50
|
+
- `auth.py` — `login_with_credentials`, `login_with_cookie`, `is_logged_in`, `wait_for_manual_login`. Login detection is URL-based (not DOM) to survive LinkedIn A/B tests.
|
|
51
|
+
- `utils.py` — `retry_async` decorator, `detect_rate_limit`, scroll helpers, `click_see_more_buttons`, `extract_text_safe`.
|
|
52
|
+
- `exceptions.py` — exception hierarchy rooted at `LinkedInScraperException`.
|
|
53
|
+
|
|
54
|
+
### `scrapers/`
|
|
55
|
+
|
|
56
|
+
All scrapers inherit `BaseScraper` (`scrapers/base.py`) which wraps `core/` helpers as instance methods.
|
|
57
|
+
|
|
58
|
+
| Scraper | Surface |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `PersonScraper` | Profile page |
|
|
61
|
+
| `CompanyScraper` | Company overview |
|
|
62
|
+
| `CompanyPostsScraper` | Company `/posts/` tab |
|
|
63
|
+
| `FeedScraper` | `/feed/` (authenticated) |
|
|
64
|
+
| `JobScraper` | Individual job posting |
|
|
65
|
+
| `JobSearchScraper` | Job search results |
|
|
66
|
+
|
|
67
|
+
**Scraper pattern:** `scrape(url, limit)` → navigate → wait for content → scroll loop with deduplication by URN → return list of Pydantic models.
|
|
68
|
+
|
|
69
|
+
Post content is extracted via `page.evaluate()` (inline JS) rather than Playwright locators, because LinkedIn's CSS class names are obfuscated and unstable. Activity URNs (`urn:li:activity:XXXXXX`) are the stable anchor — each post element has `data-urn` set to its URN.
|
|
70
|
+
|
|
71
|
+
### `models/`
|
|
72
|
+
|
|
73
|
+
All models are Pydantic `BaseModel`. `Post` is shared between `CompanyPostsScraper` and `FeedScraper`:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
Post: linkedin_url, urn, author_name, author_url, text, posted_date,
|
|
77
|
+
reactions_count, comments_count, reposts_count, image_urls, video_url, article_url
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Key conventions
|
|
81
|
+
|
|
82
|
+
- **Session file:** `linkedin_session.json` at the project root — Playwright storage state (cookies + localStorage). Never commit. Generated by `samples/create_session.py`.
|
|
83
|
+
- **headless=False** is required for LinkedIn — headless browsers get blocked. Integration tests and samples all use `headless=False`.
|
|
84
|
+
- **Feed scraper requires auth:** `FeedScraper.scrape()` calls `ensure_logged_in()` before navigating. Other scrapers do not enforce this.
|
|
85
|
+
- **Noise filtering in FeedScraper:** Sponsored/promoted posts are filtered out in the JS evaluation block by checking for `"Promoted"` / `"Sponsorisé"` labels and `[data-control-name="promoted"]`.
|
|
86
|
+
- **Test markers:** `unit` (no session), `integration` (needs `linkedin_session.json`), `slow`. Unit tests mock the Playwright `Page` object.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Contributing to LinkedIn Scraper
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to LinkedIn Scraper! This document provides guidelines for contributing to the project.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
- Python 3.8 or higher
|
|
10
|
+
- pip package manager
|
|
11
|
+
- Git
|
|
12
|
+
|
|
13
|
+
### Setting Up Development Environment
|
|
14
|
+
|
|
15
|
+
1. **Clone the repository**
|
|
16
|
+
```bash
|
|
17
|
+
git clone https://github.com/joeyism/linkedin_scraper.git
|
|
18
|
+
cd linkedin_scraper
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
2. **Create a virtual environment**
|
|
22
|
+
```bash
|
|
23
|
+
python -m venv venv
|
|
24
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
3. **Install dependencies**
|
|
28
|
+
```bash
|
|
29
|
+
pip install -r requirements.txt
|
|
30
|
+
pip install -r requirements-dev.txt
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
4. **Install Playwright browsers**
|
|
34
|
+
```bash
|
|
35
|
+
playwright install chromium
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
5. **Set up environment variables** (for testing)
|
|
39
|
+
```bash
|
|
40
|
+
cp .env.example .env
|
|
41
|
+
# Edit .env and add your LinkedIn credentials (optional, for integration tests)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Development Workflow
|
|
45
|
+
|
|
46
|
+
### Running Tests
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Run all tests
|
|
50
|
+
pytest
|
|
51
|
+
|
|
52
|
+
# Run specific test file
|
|
53
|
+
pytest tests/test_person.py
|
|
54
|
+
|
|
55
|
+
# Run with verbose output
|
|
56
|
+
pytest -v
|
|
57
|
+
|
|
58
|
+
# Run with coverage
|
|
59
|
+
pytest --cov=linkedin_scraper
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Code Style
|
|
63
|
+
|
|
64
|
+
This project follows these guidelines:
|
|
65
|
+
|
|
66
|
+
- **PEP 8**: Python code style guide
|
|
67
|
+
- **Type hints**: Use type annotations where appropriate
|
|
68
|
+
- **Docstrings**: Document all public functions and classes
|
|
69
|
+
- **Line length**: Maximum 100 characters
|
|
70
|
+
|
|
71
|
+
Before submitting, ensure your code passes linting:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Format code
|
|
75
|
+
black linkedin_scraper/
|
|
76
|
+
|
|
77
|
+
# Check for issues
|
|
78
|
+
flake8 linkedin_scraper/
|
|
79
|
+
mypy linkedin_scraper/
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Testing Your Changes
|
|
83
|
+
|
|
84
|
+
1. Write tests for new functionality
|
|
85
|
+
2. Ensure all existing tests pass
|
|
86
|
+
3. Test manually with the sample scripts in `samples/`
|
|
87
|
+
4. Verify documentation is updated
|
|
88
|
+
|
|
89
|
+
## Making Changes
|
|
90
|
+
|
|
91
|
+
### Branching Strategy
|
|
92
|
+
|
|
93
|
+
- `main` - Stable release branch
|
|
94
|
+
- `feature/your-feature` - New features
|
|
95
|
+
- `fix/your-bugfix` - Bug fixes
|
|
96
|
+
- `docs/your-doc-change` - Documentation updates
|
|
97
|
+
|
|
98
|
+
### Commit Messages
|
|
99
|
+
|
|
100
|
+
Write clear, descriptive commit messages:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Add support for scraping job descriptions
|
|
104
|
+
|
|
105
|
+
- Extract full job description text
|
|
106
|
+
- Parse job requirements section
|
|
107
|
+
- Add tests for job description parsing
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Format:
|
|
111
|
+
- First line: Brief summary (50 chars or less)
|
|
112
|
+
- Blank line
|
|
113
|
+
- Detailed explanation (wrap at 72 chars)
|
|
114
|
+
- List specific changes with bullet points
|
|
115
|
+
|
|
116
|
+
### Pull Request Process
|
|
117
|
+
|
|
118
|
+
1. **Create a new branch**
|
|
119
|
+
```bash
|
|
120
|
+
git checkout -b feature/your-feature-name
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
2. **Make your changes**
|
|
124
|
+
- Write clean, well-documented code
|
|
125
|
+
- Add tests for new functionality
|
|
126
|
+
- Update documentation as needed
|
|
127
|
+
|
|
128
|
+
3. **Commit your changes**
|
|
129
|
+
```bash
|
|
130
|
+
git add .
|
|
131
|
+
git commit -m "Your descriptive commit message"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
4. **Push to your fork**
|
|
135
|
+
```bash
|
|
136
|
+
git push origin feature/your-feature-name
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
5. **Create a Pull Request**
|
|
140
|
+
- Go to the GitHub repository
|
|
141
|
+
- Click "New Pull Request"
|
|
142
|
+
- Select your branch
|
|
143
|
+
- Fill out the PR template
|
|
144
|
+
- Wait for review
|
|
145
|
+
|
|
146
|
+
### Pull Request Guidelines
|
|
147
|
+
|
|
148
|
+
- **Description**: Clearly describe what changes you made and why
|
|
149
|
+
- **Tests**: Include tests that cover your changes
|
|
150
|
+
- **Documentation**: Update README.md or other docs if needed
|
|
151
|
+
- **Small PRs**: Keep changes focused and manageable
|
|
152
|
+
- **Responsive**: Be ready to address feedback
|
|
153
|
+
|
|
154
|
+
## What to Contribute
|
|
155
|
+
|
|
156
|
+
### Good First Issues
|
|
157
|
+
|
|
158
|
+
Look for issues labeled `good first issue` for beginner-friendly tasks:
|
|
159
|
+
|
|
160
|
+
- Documentation improvements
|
|
161
|
+
- Bug fixes
|
|
162
|
+
- Additional test coverage
|
|
163
|
+
- Code refactoring
|
|
164
|
+
|
|
165
|
+
### Feature Requests
|
|
166
|
+
|
|
167
|
+
Before implementing major features:
|
|
168
|
+
|
|
169
|
+
1. Check existing issues to avoid duplication
|
|
170
|
+
2. Open an issue to discuss the feature
|
|
171
|
+
3. Wait for maintainer approval
|
|
172
|
+
4. Implement the feature once approved
|
|
173
|
+
|
|
174
|
+
### Bug Reports
|
|
175
|
+
|
|
176
|
+
When reporting bugs, include:
|
|
177
|
+
|
|
178
|
+
- Python version
|
|
179
|
+
- Operating system
|
|
180
|
+
- Steps to reproduce
|
|
181
|
+
- Expected vs actual behavior
|
|
182
|
+
- Error messages/stack traces
|
|
183
|
+
- Sample code if possible
|
|
184
|
+
|
|
185
|
+
## Code Review Process
|
|
186
|
+
|
|
187
|
+
1. A maintainer will review your PR
|
|
188
|
+
2. They may request changes
|
|
189
|
+
3. Make requested changes and push updates
|
|
190
|
+
4. Once approved, a maintainer will merge your PR
|
|
191
|
+
|
|
192
|
+
## Questions?
|
|
193
|
+
|
|
194
|
+
If you have questions about contributing:
|
|
195
|
+
|
|
196
|
+
- Open an issue on GitHub
|
|
197
|
+
- Check existing issues and discussions
|
|
198
|
+
- Review the README.md for usage examples
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
By contributing, you agree that your contributions will be licensed under the Apache License 2.0.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
Thank you for contributing to LinkedIn Scraper! Your efforts help make this project better for everyone.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Contexte du fork — linkedin_scraper
|
|
2
|
+
|
|
3
|
+
## Origin
|
|
4
|
+
|
|
5
|
+
Fork de [joeyism/linkedin_scraper](https://github.com/joeyism/linkedin_scraper).
|
|
6
|
+
|
|
7
|
+
## Pourquoi ce fork
|
|
8
|
+
|
|
9
|
+
### Problème de départ
|
|
10
|
+
|
|
11
|
+
Ce projet est né d'un constat simple : **l'API officielle LinkedIn ne permet pas de lire les posts**.
|
|
12
|
+
|
|
13
|
+
Un premier projet (`linkedin-mcp`, fork de FilippTrigub) expose des outils MCP pour interagir avec LinkedIn via l'API officielle. Il fonctionne pour :
|
|
14
|
+
- S'authentifier via OAuth2
|
|
15
|
+
- Créer des posts (`POST /v2/ugcPosts`)
|
|
16
|
+
|
|
17
|
+
Mais la **lecture des posts est bloquée** par LinkedIn :
|
|
18
|
+
|
|
19
|
+
| Scope | Usage | Disponibilité |
|
|
20
|
+
|-------|-------|---------------|
|
|
21
|
+
| `w_member_social` | Créer des posts | Apps standard ✅ |
|
|
22
|
+
| `r_member_social` | Lire des posts | Marketing Developer Platform uniquement ❌ |
|
|
23
|
+
|
|
24
|
+
Le scope `r_member_social` est réservé aux partenaires LinkedIn (Marketing Developer Platform). Il est inaccessible aux développeurs individuels. Même avec un code correct, l'API renvoie **403 Forbidden** sur tous les endpoints de lecture (`/v2/ugcPosts`, `/v2/shares`).
|
|
25
|
+
|
|
26
|
+
### Solution retenue : scraping web avec Playwright
|
|
27
|
+
|
|
28
|
+
L'API officielle étant une impasse, la seule approche réaliste est d'automatiser un navigateur authentifié. Ce repo utilise déjà Playwright et expose exactement la structure nécessaire.
|
|
29
|
+
|
|
30
|
+
## Objectif de ce fork
|
|
31
|
+
|
|
32
|
+
**Ajouter un `FeedScraper`** capable de récupérer les N premiers posts du feed LinkedIn de l'utilisateur authentifié.
|
|
33
|
+
|
|
34
|
+
Le feed LinkedIn correspond à la page `linkedin.com/feed/` — ce que l'utilisateur voit quand il se connecte : posts de ses connexions, articles partagés, etc.
|
|
35
|
+
|
|
36
|
+
### Ce qui existe déjà dans le repo
|
|
37
|
+
|
|
38
|
+
- `CompanyPostsScraper` — scrape les posts d'une page entreprise
|
|
39
|
+
- `BrowserManager` — gestion de session Playwright réutilisable
|
|
40
|
+
- Modèle `Post` — structure de données pour un post
|
|
41
|
+
|
|
42
|
+
### Ce qu'il faut ajouter
|
|
43
|
+
|
|
44
|
+
Un `FeedScraper` calqué sur `CompanyPostsScraper` qui :
|
|
45
|
+
1. Navigue sur `https://www.linkedin.com/feed/`
|
|
46
|
+
2. Scrolle N fois pour charger les posts
|
|
47
|
+
3. Parse le DOM pour extraire : auteur, texte, date, reactions, comments
|
|
48
|
+
4. Filtre le bruit : publicités, suggestions "Vous connaissez peut-être...", posts sponsorisés
|
|
49
|
+
5. Retourne une liste de `Post`
|
|
50
|
+
|
|
51
|
+
## Complexité anticipée
|
|
52
|
+
|
|
53
|
+
### Identique à CompanyPostsScraper
|
|
54
|
+
- Session Playwright réutilisée
|
|
55
|
+
- Infinite scroll avec `page.evaluate("window.scrollBy(...)")`
|
|
56
|
+
|
|
57
|
+
### Spécifique au feed
|
|
58
|
+
- **Bruit dans le DOM** : le feed mélange posts réels, pubs, suggestions → filtrage nécessaire
|
|
59
|
+
- **Sélecteurs CSS instables** : LinkedIn obfusque ses class names, ils peuvent changer
|
|
60
|
+
- **Anti-bot** : LinkedIn est légèrement plus vigilant sur le feed que sur les pages publiques
|
|
61
|
+
|
|
62
|
+
## Prochaines étapes
|
|
63
|
+
|
|
64
|
+
1. Lire le code de `CompanyPostsScraper` pour comprendre le pattern exact
|
|
65
|
+
2. Inspecter le DOM du feed LinkedIn pour identifier les sélecteurs stables
|
|
66
|
+
3. Implémenter `FeedScraper` sur le même modèle
|
|
67
|
+
4. Tester avec différents comptes et volumes de posts
|
|
68
|
+
|
|
69
|
+
## Liens
|
|
70
|
+
|
|
71
|
+
- Repo original : https://github.com/joeyism/linkedin_scraper
|
|
72
|
+
- Ce fork : https://github.com/vinzlac/linkedin_scraper
|
|
73
|
+
- Projet MCP LinkedIn associé : https://github.com/vinzlac/linkedin-mcp (contexte origine)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# linkedin_scraper — task runner
|
|
2
|
+
# Requires: uv, just
|
|
3
|
+
|
|
4
|
+
# Default: list available recipes
|
|
5
|
+
default:
|
|
6
|
+
@just --list
|
|
7
|
+
|
|
8
|
+
# Install all dependencies (including dev) and Playwright browsers
|
|
9
|
+
install:
|
|
10
|
+
uv sync --group dev
|
|
11
|
+
uv run playwright install chromium
|
|
12
|
+
|
|
13
|
+
# Run unit tests only (no LinkedIn session required)
|
|
14
|
+
test:
|
|
15
|
+
uv run pytest -m "not integration and not slow"
|
|
16
|
+
|
|
17
|
+
# Run a single test file
|
|
18
|
+
test-file FILE:
|
|
19
|
+
uv run pytest {{ FILE }} -m "not integration"
|
|
20
|
+
|
|
21
|
+
# Run a single test by name
|
|
22
|
+
test-one FILE TEST:
|
|
23
|
+
uv run pytest {{ FILE }}::{{ TEST }}
|
|
24
|
+
|
|
25
|
+
# Run integration tests (requires linkedin_session.json)
|
|
26
|
+
test-integration:
|
|
27
|
+
uv run pytest -m integration
|
|
28
|
+
|
|
29
|
+
# Run all tests with coverage report
|
|
30
|
+
test-cov:
|
|
31
|
+
uv run pytest -m "not integration" --cov=linkedin_scraper --cov-report=term-missing
|
|
32
|
+
|
|
33
|
+
# Format code with black
|
|
34
|
+
fmt:
|
|
35
|
+
uv run black linkedin_scraper tests samples
|
|
36
|
+
|
|
37
|
+
# Check formatting without modifying files
|
|
38
|
+
fmt-check:
|
|
39
|
+
uv run black --check linkedin_scraper tests samples
|
|
40
|
+
|
|
41
|
+
# Lint with flake8
|
|
42
|
+
lint:
|
|
43
|
+
uv run flake8 linkedin_scraper tests
|
|
44
|
+
|
|
45
|
+
# Type check with mypy
|
|
46
|
+
typecheck:
|
|
47
|
+
uv run mypy linkedin_scraper
|
|
48
|
+
|
|
49
|
+
# Run all checks (fmt-check + lint + typecheck + test)
|
|
50
|
+
check: fmt-check lint test
|
|
51
|
+
|
|
52
|
+
# Create a LinkedIn session file (opens browser for manual login)
|
|
53
|
+
session:
|
|
54
|
+
uv run python samples/create_session.py
|
|
55
|
+
|
|
56
|
+
# Scrape N posts from your LinkedIn feed (default: 10)
|
|
57
|
+
run-feed N="10":
|
|
58
|
+
uv run python samples/scrape_feed.py {{ N }}
|
|
59
|
+
|
|
60
|
+
# Scrape N posts from feed using a virtual display (for Linux servers without a GUI)
|
|
61
|
+
# Requires: sudo apt install xvfb
|
|
62
|
+
run-feed-xvfb N="10":
|
|
63
|
+
xvfb-run --server-args="-screen 0 1280x720x24" uv run python samples/scrape_feed.py {{ N }}
|
|
64
|
+
|
|
65
|
+
# Debug DOM structure of LinkedIn feed
|
|
66
|
+
debug-feed:
|
|
67
|
+
uv run python samples/debug_feed.py
|
|
68
|
+
|
|
69
|
+
# Scrape a LinkedIn profile (URL or slug)
|
|
70
|
+
run-person PROFILE:
|
|
71
|
+
uv run python samples/scrape_person.py {{ PROFILE }}
|
|
72
|
+
|
|
73
|
+
# Debug DOM selectors on a profile page (helps fix broken selectors)
|
|
74
|
+
debug-person PROFILE:
|
|
75
|
+
uv run python samples/debug_person.py {{ PROFILE }}
|
|
76
|
+
|
|
77
|
+
# Run the company posts scraper sample
|
|
78
|
+
run-company URL="https://www.linkedin.com/company/microsoft/":
|
|
79
|
+
uv run python samples/scrape_company_posts.py
|
|
80
|
+
|
|
81
|
+
# Build distribution packages
|
|
82
|
+
build:
|
|
83
|
+
uv build
|
|
84
|
+
|
|
85
|
+
# Publish to PyPI (requires TWINE_USERNAME / TWINE_PASSWORD or ~/.pypirc)
|
|
86
|
+
publish:
|
|
87
|
+
uv run twine upload dist/*
|