ForageFacebook 1.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.
- foragefacebook-1.0.0/.github/FUNDING.yml +1 -0
- foragefacebook-1.0.0/.github/dependabot.yml +18 -0
- foragefacebook-1.0.0/.github/workflows/ci.yml +71 -0
- foragefacebook-1.0.0/.github/workflows/publish.yml +79 -0
- foragefacebook-1.0.0/.gitignore +62 -0
- foragefacebook-1.0.0/AGENTS.md +104 -0
- foragefacebook-1.0.0/CHANGELOG.md +38 -0
- foragefacebook-1.0.0/CLAUDE.md +126 -0
- foragefacebook-1.0.0/CONTRIBUTING.md +81 -0
- foragefacebook-1.0.0/LICENSE +373 -0
- foragefacebook-1.0.0/PKG-INFO +337 -0
- foragefacebook-1.0.0/README.md +302 -0
- foragefacebook-1.0.0/SECURITY.md +29 -0
- foragefacebook-1.0.0/pyproject.toml +60 -0
- foragefacebook-1.0.0/scripts/release.sh +61 -0
- foragefacebook-1.0.0/src/forage/__init__.py +3 -0
- foragefacebook-1.0.0/src/forage/auth.py +134 -0
- foragefacebook-1.0.0/src/forage/cli.py +310 -0
- foragefacebook-1.0.0/src/forage/exporter.py +222 -0
- foragefacebook-1.0.0/src/forage/models.py +74 -0
- foragefacebook-1.0.0/src/forage/parser.py +562 -0
- foragefacebook-1.0.0/src/forage/scraper.py +600 -0
- foragefacebook-1.0.0/tests/__init__.py +1 -0
- foragefacebook-1.0.0/tests/conftest.py +161 -0
- foragefacebook-1.0.0/tests/fixtures/comment_simple.html +7 -0
- foragefacebook-1.0.0/tests/fixtures/comment_with_replies.html +23 -0
- foragefacebook-1.0.0/tests/fixtures/post_long_content.html +13 -0
- foragefacebook-1.0.0/tests/fixtures/post_realistic.html +54 -0
- foragefacebook-1.0.0/tests/fixtures/post_simple.html +13 -0
- foragefacebook-1.0.0/tests/fixtures/post_sponsored.html +7 -0
- foragefacebook-1.0.0/tests/fixtures/post_with_emoji.html +12 -0
- foragefacebook-1.0.0/tests/fixtures/post_with_media.html +15 -0
- foragefacebook-1.0.0/tests/fixtures/post_with_see_more.html +12 -0
- foragefacebook-1.0.0/tests/test_cli.py +114 -0
- foragefacebook-1.0.0/tests/test_exporter.py +299 -0
- foragefacebook-1.0.0/tests/test_models.py +155 -0
- foragefacebook-1.0.0/tests/test_parser.py +255 -0
- foragefacebook-1.0.0/tests/test_scraper.py +222 -0
- foragefacebook-1.0.0/uv.lock +499 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
github: [jwmoss]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "pip"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "weekly"
|
|
7
|
+
open-pull-requests-limit: 5
|
|
8
|
+
labels:
|
|
9
|
+
- "dependencies"
|
|
10
|
+
|
|
11
|
+
- package-ecosystem: "github-actions"
|
|
12
|
+
directory: "/"
|
|
13
|
+
schedule:
|
|
14
|
+
interval: "weekly"
|
|
15
|
+
open-pull-requests-limit: 5
|
|
16
|
+
labels:
|
|
17
|
+
- "dependencies"
|
|
18
|
+
- "ci"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master, main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
+
run: uv python install ${{ matrix.python-version }}
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: uv sync --all-extras
|
|
27
|
+
|
|
28
|
+
- name: Install Playwright browsers
|
|
29
|
+
run: uv run playwright install chromium
|
|
30
|
+
|
|
31
|
+
- name: Run tests
|
|
32
|
+
run: uv run pytest tests/ -v --tb=short
|
|
33
|
+
|
|
34
|
+
type-check:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
|
|
40
|
+
- name: Install uv
|
|
41
|
+
uses: astral-sh/setup-uv@v4
|
|
42
|
+
|
|
43
|
+
- name: Set up Python
|
|
44
|
+
run: uv python install 3.12
|
|
45
|
+
|
|
46
|
+
- name: Install dependencies
|
|
47
|
+
run: uv sync --all-extras
|
|
48
|
+
|
|
49
|
+
- name: Run type checker
|
|
50
|
+
run: uv run ty check src/forage/
|
|
51
|
+
|
|
52
|
+
lint:
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
|
|
58
|
+
- name: Install uv
|
|
59
|
+
uses: astral-sh/setup-uv@v4
|
|
60
|
+
|
|
61
|
+
- name: Set up Python
|
|
62
|
+
run: uv python install 3.12
|
|
63
|
+
|
|
64
|
+
- name: Install ruff
|
|
65
|
+
run: uv tool install ruff
|
|
66
|
+
|
|
67
|
+
- name: Run ruff check
|
|
68
|
+
run: ruff check src/forage/ tests/
|
|
69
|
+
|
|
70
|
+
- name: Run ruff format check
|
|
71
|
+
run: ruff format --check src/forage/ tests/
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
test_pypi:
|
|
9
|
+
description: 'Publish to Test PyPI instead'
|
|
10
|
+
required: false
|
|
11
|
+
default: 'false'
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v4
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
run: uv python install 3.12
|
|
25
|
+
|
|
26
|
+
- name: Build package
|
|
27
|
+
run: uv build
|
|
28
|
+
|
|
29
|
+
- name: Upload artifacts
|
|
30
|
+
uses: actions/upload-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: dist
|
|
33
|
+
path: dist/
|
|
34
|
+
|
|
35
|
+
publish-test-pypi:
|
|
36
|
+
needs: build
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
if: github.event_name == 'workflow_dispatch' && github.event.inputs.test_pypi == 'true'
|
|
39
|
+
|
|
40
|
+
environment:
|
|
41
|
+
name: test-pypi
|
|
42
|
+
url: https://test.pypi.org/project/forage/
|
|
43
|
+
|
|
44
|
+
permissions:
|
|
45
|
+
id-token: write
|
|
46
|
+
|
|
47
|
+
steps:
|
|
48
|
+
- name: Download artifacts
|
|
49
|
+
uses: actions/download-artifact@v4
|
|
50
|
+
with:
|
|
51
|
+
name: dist
|
|
52
|
+
path: dist/
|
|
53
|
+
|
|
54
|
+
- name: Publish to Test PyPI
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
56
|
+
with:
|
|
57
|
+
repository-url: https://test.pypi.org/legacy/
|
|
58
|
+
|
|
59
|
+
publish-pypi:
|
|
60
|
+
needs: build
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
if: github.event_name == 'release'
|
|
63
|
+
|
|
64
|
+
environment:
|
|
65
|
+
name: pypi
|
|
66
|
+
url: https://pypi.org/project/forage/
|
|
67
|
+
|
|
68
|
+
permissions:
|
|
69
|
+
id-token: write
|
|
70
|
+
|
|
71
|
+
steps:
|
|
72
|
+
- name: Download artifacts
|
|
73
|
+
uses: actions/download-artifact@v4
|
|
74
|
+
with:
|
|
75
|
+
name: dist
|
|
76
|
+
path: dist/
|
|
77
|
+
|
|
78
|
+
- name: Publish to PyPI
|
|
79
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
|
|
28
|
+
# IDE
|
|
29
|
+
.idea/
|
|
30
|
+
.vscode/
|
|
31
|
+
*.swp
|
|
32
|
+
*.swo
|
|
33
|
+
|
|
34
|
+
# Testing
|
|
35
|
+
.pytest_cache/
|
|
36
|
+
.coverage
|
|
37
|
+
htmlcov/
|
|
38
|
+
|
|
39
|
+
# Type checking
|
|
40
|
+
.ty_cache/
|
|
41
|
+
|
|
42
|
+
# OS
|
|
43
|
+
.DS_Store
|
|
44
|
+
Thumbs.db
|
|
45
|
+
|
|
46
|
+
# Project specific
|
|
47
|
+
*.json
|
|
48
|
+
!package.json
|
|
49
|
+
|
|
50
|
+
# Session data (contains auth cookies - NEVER commit!)
|
|
51
|
+
session/
|
|
52
|
+
*.session
|
|
53
|
+
~/.config/forage/
|
|
54
|
+
|
|
55
|
+
# Output files (may contain scraped data)
|
|
56
|
+
*.db
|
|
57
|
+
output/
|
|
58
|
+
|
|
59
|
+
# Environment variables (may contain secrets)
|
|
60
|
+
.env
|
|
61
|
+
.env.local
|
|
62
|
+
.env.*.local
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Agent Context for Forage
|
|
2
|
+
|
|
3
|
+
This document provides context for AI agents working on this codebase.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
**Forage** is a Python CLI tool for scraping private Facebook groups. It uses Playwright for browser automation with saved session cookies for authentication.
|
|
8
|
+
|
|
9
|
+
## Key Architecture Decisions
|
|
10
|
+
|
|
11
|
+
### Browser Automation (Not API)
|
|
12
|
+
Facebook doesn't provide a public API for group data. We use Playwright to automate a real browser, which:
|
|
13
|
+
- Looks like legitimate user activity
|
|
14
|
+
- Works with private groups
|
|
15
|
+
- Handles JavaScript-heavy modern Facebook UI
|
|
16
|
+
|
|
17
|
+
### Session-Based Auth
|
|
18
|
+
Users log in manually once via `forage login`, which saves browser cookies to `~/.config/forage/session/`. This avoids storing credentials and handles 2FA.
|
|
19
|
+
|
|
20
|
+
### Anti-Detection
|
|
21
|
+
- Random delays between actions (not fixed timing)
|
|
22
|
+
- Random viewport sizes from common resolutions
|
|
23
|
+
- Real browser fingerprint (Playwright uses actual Chromium)
|
|
24
|
+
|
|
25
|
+
## File Structure
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
src/forage/
|
|
29
|
+
├── cli.py # Entry point, Click commands
|
|
30
|
+
├── auth.py # Login flow, session persistence
|
|
31
|
+
├── scraper.py # Main scraping orchestration
|
|
32
|
+
├── parser.py # HTML parsing (posts, comments)
|
|
33
|
+
└── models.py # Pydantic models for data
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Common Tasks
|
|
37
|
+
|
|
38
|
+
### Adding a New CLI Flag
|
|
39
|
+
1. Add to `ScrapeOptions` dataclass in `scraper.py`
|
|
40
|
+
2. Add Click option in `cli.py` scrape command
|
|
41
|
+
3. Pass to `ScrapeOptions` when constructing
|
|
42
|
+
|
|
43
|
+
### Fixing Broken Selectors
|
|
44
|
+
Facebook frequently changes their HTML. When scraping breaks:
|
|
45
|
+
1. Run with `--no-headless -v` to see the browser
|
|
46
|
+
2. Use browser DevTools to inspect current HTML
|
|
47
|
+
3. Update selectors in `parser.py` (look for `query_selector`)
|
|
48
|
+
|
|
49
|
+
### Adding New Data Fields
|
|
50
|
+
1. Add field to model in `models.py`
|
|
51
|
+
2. Extract in appropriate `parse_*` function in `parser.py`
|
|
52
|
+
3. JSON output updates automatically (Pydantic)
|
|
53
|
+
|
|
54
|
+
## Known Fragile Areas
|
|
55
|
+
|
|
56
|
+
1. **Post selectors** (`[data-pagelet^="FeedUnit"]`) - Facebook changes these
|
|
57
|
+
2. **Timestamp parsing** - Many formats, relative times ("2h", "Yesterday")
|
|
58
|
+
3. **Comment expansion** - "View more comments" button selectors change
|
|
59
|
+
4. **Reaction counts** - Multiple ways reactions appear in HTML
|
|
60
|
+
|
|
61
|
+
## Testing
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Quick test (3 posts, no comments)
|
|
65
|
+
uv run forage -v scrape GROUP_SLUG --limit 3 --skip-comments
|
|
66
|
+
|
|
67
|
+
# Debug mode (watch browser)
|
|
68
|
+
uv run forage -v scrape GROUP_SLUG --limit 1 --no-headless
|
|
69
|
+
|
|
70
|
+
# Type check
|
|
71
|
+
uv run ty check src/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Dependencies
|
|
75
|
+
|
|
76
|
+
- **click**: CLI framework
|
|
77
|
+
- **playwright**: Browser automation
|
|
78
|
+
- **pydantic**: Data validation and JSON serialization
|
|
79
|
+
- **rich**: Terminal output formatting
|
|
80
|
+
|
|
81
|
+
## Anti-Detection Strategy
|
|
82
|
+
|
|
83
|
+
The scraper uses several techniques to appear human:
|
|
84
|
+
|
|
85
|
+
1. **Random delays**: `human_delay()` adds variance to wait times
|
|
86
|
+
2. **Viewport randomization**: Picks from common screen sizes
|
|
87
|
+
3. **Real browser**: Playwright runs actual Chromium, not headless-only
|
|
88
|
+
4. **Session persistence**: Uses real logged-in session, not API tokens
|
|
89
|
+
|
|
90
|
+
## Rate Limiting
|
|
91
|
+
|
|
92
|
+
Default delay is 2 seconds between actions. For large scrapes:
|
|
93
|
+
- Use `--delay 5.0` or higher
|
|
94
|
+
- Consider running in batches with breaks
|
|
95
|
+
- Facebook may temporarily block if too aggressive
|
|
96
|
+
|
|
97
|
+
## Session Management
|
|
98
|
+
|
|
99
|
+
Sessions are stored in `~/.config/forage/session/storage_state.json`. This includes:
|
|
100
|
+
- Cookies
|
|
101
|
+
- Local storage
|
|
102
|
+
- Session storage
|
|
103
|
+
|
|
104
|
+
Sessions typically expire after ~30 days or if Facebook detects unusual activity.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2025-01-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial stable release
|
|
15
|
+
- `forage login` command for interactive Facebook authentication
|
|
16
|
+
- `forage scrape` command for scraping posts, comments, and reactions
|
|
17
|
+
- JSON output format (default)
|
|
18
|
+
- SQLite export format (`-f sqlite`)
|
|
19
|
+
- CSV export format (`-f csv`)
|
|
20
|
+
- Date range filtering (`--days`, `--since`, `--until`)
|
|
21
|
+
- Comment filtering (`--min-reactions`, `--top-comments`, `--skip-comments`)
|
|
22
|
+
- Rate limiting with configurable delay (`--delay`)
|
|
23
|
+
- Retry logic with exponential backoff for network errors
|
|
24
|
+
- Anti-detection features (random delays, viewport rotation)
|
|
25
|
+
- Stdin support for group input (`echo "group" | forage scrape -`)
|
|
26
|
+
- GitHub Actions CI with tests, type checking, and linting
|
|
27
|
+
- PyPI publishing workflow
|
|
28
|
+
- Comprehensive test suite (96 tests)
|
|
29
|
+
- Documentation (README, CONTRIBUTING, SECURITY, AGENTS, CLAUDE)
|
|
30
|
+
|
|
31
|
+
### Security
|
|
32
|
+
|
|
33
|
+
- Session data stored securely in `~/.config/forage/session/`
|
|
34
|
+
- Sensitive files excluded via `.gitignore`
|
|
35
|
+
- Security guidelines in SECURITY.md
|
|
36
|
+
|
|
37
|
+
[Unreleased]: https://github.com/jwmoss/forage/compare/v1.0.0...HEAD
|
|
38
|
+
[1.0.0]: https://github.com/jwmoss/forage/releases/tag/v1.0.0
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Claude Context for Forage
|
|
2
|
+
|
|
3
|
+
This file provides context for Claude (or other AI assistants) when working on this project.
|
|
4
|
+
|
|
5
|
+
## Project Purpose
|
|
6
|
+
|
|
7
|
+
Forage is a personal tool for scraping private Facebook groups to analyze community discussions. The owner uses it to:
|
|
8
|
+
- Understand what's popular in their local community
|
|
9
|
+
- Identify trends and common topics
|
|
10
|
+
- Gather data for potential app ideas
|
|
11
|
+
|
|
12
|
+
## Technology Choices
|
|
13
|
+
|
|
14
|
+
| Choice | Why |
|
|
15
|
+
|--------|-----|
|
|
16
|
+
| Python | Fast iteration, good scraping ecosystem |
|
|
17
|
+
| Playwright | Best browser automation, handles modern SPAs |
|
|
18
|
+
| Click | Clean CLI framework |
|
|
19
|
+
| Pydantic | Type-safe data models, automatic JSON |
|
|
20
|
+
| uv | Fast Python package management |
|
|
21
|
+
| ty | Astral's type checker (fast, modern) |
|
|
22
|
+
|
|
23
|
+
## Development Workflow
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Always use uv, not python directly
|
|
27
|
+
uv sync # Install deps
|
|
28
|
+
uv run forage --help # Run CLI
|
|
29
|
+
uv run ty check src/ # Type check
|
|
30
|
+
uv run pytest # Tests
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Code Style
|
|
34
|
+
|
|
35
|
+
- Type hints on all functions
|
|
36
|
+
- Docstrings for public functions
|
|
37
|
+
- Use `from __future__ import annotations` for forward refs
|
|
38
|
+
- Prefer explicit over implicit
|
|
39
|
+
- Handle exceptions gracefully (scraping is brittle)
|
|
40
|
+
|
|
41
|
+
## Key Implementation Details
|
|
42
|
+
|
|
43
|
+
### The Scraping Flow
|
|
44
|
+
|
|
45
|
+
1. `cli.py:scrape()` - Entry point, builds options
|
|
46
|
+
2. `scraper.py:scrape_group()` - Main loop
|
|
47
|
+
3. For each post in feed:
|
|
48
|
+
- `parser.py:parse_modern_post()` - Extract post data
|
|
49
|
+
- If comments enabled: `scraper.py:scrape_post_comments()` or `scrape_comments_from_post_page()`
|
|
50
|
+
4. Apply filters (min_reactions, top_comments)
|
|
51
|
+
5. Return `ScrapeResult` (serializes to JSON)
|
|
52
|
+
|
|
53
|
+
### Authentication Flow
|
|
54
|
+
|
|
55
|
+
1. `cli.py:login()` - Opens browser
|
|
56
|
+
2. `auth.py:login()` - Waits for user to log in
|
|
57
|
+
3. Saves `storage_state.json` (cookies + storage)
|
|
58
|
+
4. Future runs load this state into browser context
|
|
59
|
+
|
|
60
|
+
### HTML Parsing Strategy
|
|
61
|
+
|
|
62
|
+
Facebook's React UI has deeply nested, frequently-changing HTML. Strategy:
|
|
63
|
+
1. Find stable containers (`[data-pagelet^="FeedUnit"]`)
|
|
64
|
+
2. Extract text content
|
|
65
|
+
3. Use heuristics to identify author, content, timestamps
|
|
66
|
+
4. Fall back gracefully when selectors fail
|
|
67
|
+
|
|
68
|
+
## Common Issues & Solutions
|
|
69
|
+
|
|
70
|
+
### "Session expired" errors
|
|
71
|
+
- Session cookies expire (~30 days)
|
|
72
|
+
- Run `forage login` again
|
|
73
|
+
- CLI auto-prompts for re-login
|
|
74
|
+
|
|
75
|
+
### Scraper finds 0 posts
|
|
76
|
+
- Facebook changed their HTML structure
|
|
77
|
+
- Run with `--no-headless -v` to debug
|
|
78
|
+
- Check console for selector matches
|
|
79
|
+
- Update selectors in `parser.py`
|
|
80
|
+
|
|
81
|
+
### Rate limiting / blocked
|
|
82
|
+
- Increase `--delay` (try 5-10 seconds)
|
|
83
|
+
- Take breaks between large scrapes
|
|
84
|
+
- May need to wait 24h if blocked
|
|
85
|
+
|
|
86
|
+
### Comments not loading
|
|
87
|
+
- Facebook loads comments lazily
|
|
88
|
+
- The scraper tries to click "View more comments"
|
|
89
|
+
- May need to navigate to post page
|
|
90
|
+
|
|
91
|
+
## Future Improvements
|
|
92
|
+
|
|
93
|
+
- [ ] Async scraping for performance
|
|
94
|
+
- [ ] Better timestamp parsing
|
|
95
|
+
- [ ] Individual reaction types (like/love/haha)
|
|
96
|
+
- [ ] Media URL extraction
|
|
97
|
+
- [ ] Export to CSV/SQLite
|
|
98
|
+
- [ ] Retry logic with exponential backoff
|
|
99
|
+
|
|
100
|
+
## Testing Tips
|
|
101
|
+
|
|
102
|
+
When making changes:
|
|
103
|
+
1. Test with a small limit first: `--limit 3`
|
|
104
|
+
2. Use `--skip-comments` for faster iteration
|
|
105
|
+
3. Use `--no-headless -v` to see what's happening
|
|
106
|
+
4. Check the JSON output structure is correct
|
|
107
|
+
|
|
108
|
+
## Important Files
|
|
109
|
+
|
|
110
|
+
| File | Purpose |
|
|
111
|
+
|------|---------|
|
|
112
|
+
| `cli.py` | All CLI commands and options |
|
|
113
|
+
| `scraper.py` | Scraping logic, browser control |
|
|
114
|
+
| `parser.py` | HTML parsing, data extraction |
|
|
115
|
+
| `models.py` | Pydantic models (Post, Comment, etc.) |
|
|
116
|
+
| `auth.py` | Session management |
|
|
117
|
+
|
|
118
|
+
## Fragile Code Warnings
|
|
119
|
+
|
|
120
|
+
These areas break frequently due to Facebook changes:
|
|
121
|
+
|
|
122
|
+
1. **`parser.py:parse_modern_post()`** - Post extraction heuristics
|
|
123
|
+
2. **`scraper.py` selectors** - `[data-pagelet^="FeedUnit"]` etc.
|
|
124
|
+
3. **Timestamp parsing** - Many edge cases
|
|
125
|
+
|
|
126
|
+
When something breaks, check these first.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Contributing to Forage
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to Forage!
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
1. Clone the repository:
|
|
8
|
+
```bash
|
|
9
|
+
git clone https://github.com/jwmoss/forage.git
|
|
10
|
+
cd forage
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. Install dependencies using uv:
|
|
14
|
+
```bash
|
|
15
|
+
uv sync --all-extras
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
3. Install Playwright browsers:
|
|
19
|
+
```bash
|
|
20
|
+
uv run playwright install chromium
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
4. Run tests:
|
|
24
|
+
```bash
|
|
25
|
+
uv run pytest tests/ -v
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
5. Run type checker:
|
|
29
|
+
```bash
|
|
30
|
+
uv run ty check src/forage/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Code Style
|
|
34
|
+
|
|
35
|
+
- Use [ruff](https://docs.astral.sh/ruff/) for linting and formatting
|
|
36
|
+
- Follow PEP 8 style guidelines
|
|
37
|
+
- Add type hints to all function signatures
|
|
38
|
+
- Write docstrings for public functions
|
|
39
|
+
|
|
40
|
+
## Commit Messages
|
|
41
|
+
|
|
42
|
+
We use [Conventional Commits](https://www.conventionalcommits.org/):
|
|
43
|
+
|
|
44
|
+
- `feat:` new feature
|
|
45
|
+
- `fix:` bug fix
|
|
46
|
+
- `docs:` documentation changes
|
|
47
|
+
- `test:` adding or updating tests
|
|
48
|
+
- `refactor:` code refactoring
|
|
49
|
+
- `chore:` maintenance tasks
|
|
50
|
+
|
|
51
|
+
Example: `feat: add SQLite export option`
|
|
52
|
+
|
|
53
|
+
## Pull Request Process
|
|
54
|
+
|
|
55
|
+
1. Fork the repository
|
|
56
|
+
2. Create a feature branch (`git checkout -b feat/my-feature`)
|
|
57
|
+
3. Make your changes
|
|
58
|
+
4. Ensure tests pass (`uv run pytest tests/`)
|
|
59
|
+
5. Ensure type checking passes (`uv run ty check src/forage/`)
|
|
60
|
+
6. Commit with a descriptive message
|
|
61
|
+
7. Push and open a pull request
|
|
62
|
+
|
|
63
|
+
## Testing
|
|
64
|
+
|
|
65
|
+
- Write tests for new functionality
|
|
66
|
+
- Use pytest fixtures for common test data
|
|
67
|
+
- Mock external dependencies (Playwright, network)
|
|
68
|
+
- Aim for good coverage of edge cases
|
|
69
|
+
|
|
70
|
+
## Reporting Issues
|
|
71
|
+
|
|
72
|
+
When reporting bugs, please include:
|
|
73
|
+
- Python version (`python --version`)
|
|
74
|
+
- Operating system
|
|
75
|
+
- Steps to reproduce
|
|
76
|
+
- Expected vs actual behavior
|
|
77
|
+
- Any error messages
|
|
78
|
+
|
|
79
|
+
## Questions?
|
|
80
|
+
|
|
81
|
+
Feel free to open a GitHub issue for questions or discussions.
|