Documentation
¶
Overview ¶
Package parser implements the w_popularity LinkedIn adapter.
LinkedIn aggressively blocks unauthenticated scraping. A logged-out HTTP GET of https://www.linkedin.com/in/<handle>/ almost always returns HTTP 999 (LinkedIn's own "no scraping" status) and a body that JS-redirects to /authwall. The schema.org Person LD-JSON block is also empty on the logged-out path: there is no follower count to scrape.
Strategy (in priority order):
**Authenticated HTML scrape via `li_at` session cookie.** When Config.LIATCookie is set we attach `Cookie: li_at=...` (and an optional JSESSIONID) to the request. LinkedIn then serves the real profile page HTML, which embeds structured data in three possible shapes:
a. BPR datalets:
<code id="datalet-bpr-guid-…" style="display:none"> <!--{"data":{"data": {...}, "included": [{...}]}}--> </code>
Each datalet body is an HTML comment around a JSON object. For profile pages, the `included` array contains nodes with follower / connection counters and headline/location fields. This is LinkedIn's own browser-proxy payload format and is the most reliable extraction surface today.
b. window.__APOLLO_STATE__: occasionally a different render path emits an Apollo cache as a JS global. Less common, but worth trying as a secondary.
c. <script type="application/ld+json"> with @type=Person: when authenticated the LD-JSON `interactionStatistic` array carries a real `followers` counter (zero for logged-out fetches, which is why the previous implementation always returned 0).
The extractor is intentionally tolerant: it deep-walks every decoded JSON tree looking for known keys (followerCount, connectionsCount, headline, location, industryName, …) and stops at the first hit. We never want a LinkedIn UI rotation to break the whole pipeline; missing fields downgrade to 0 plus a diagnostic marker in Raw["fetch_note"].
**camoufox via Playwright (skeleton).** Config.CamoufoxURL is the CDP endpoint of a running camoufox instance (same shape as the facebook/instagram adapters). fetchViaCamoufox is currently a stub — the branching is in place so a real implementation is a drop-in replacement.
When both LIATCookie and CamoufoxURL are empty we return ErrAuth with the hint "set LI_AT cookie or CAMOUFOX_URL". When LIATCookie is set but LinkedIn still answers 999 we treat the cookie as expired and return ErrAuth with "li_at cookie expired".
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// LIATCookie is the `li_at` session cookie of a logged-in LinkedIn
// browser session. This is the primary auth surface. Pulled from
// the LINKEDIN_LI_AT env var by callers. Empty disables the
// authenticated path.
LIATCookie string
// JSESSIONID is an optional companion cookie. Some LinkedIn server
// endpoints check it; the profile page generally doesn't, but we
// forward it when present.
JSESSIONID string
// HTTPClient is optional; one with HTTPTimeout is constructed
// otherwise. The default client does not auto-follow LinkedIn's
// 301-to-authwall chain — we want to inspect the first response.
HTTPClient *http.Client
// HTTPTimeout caps every outbound call. Defaults to 15s.
HTTPTimeout time.Duration
// UserAgent overrides the default desktop Chrome UA. LinkedIn is
// pickier about UAs than most sites; a realistic value is required.
UserAgent string
// CamoufoxURL is the CDP endpoint of a camoufox instance. When set,
// and LIATCookie is not, the parser falls back to it. Empty
// disables the fallback.
CamoufoxURL string
// BaseURL overrides https://www.linkedin.com. Test hook.
BaseURL string
}
Config controls runtime behaviour.
type LinkedInParser ¶
type LinkedInParser struct {
// contains filtered or unexported fields
}
LinkedInParser is the public entry point.
func (*LinkedInParser) FetchChannel ¶
func (p *LinkedInParser) FetchChannel(ctx context.Context, handle string) (shared.ChannelSnapshot, error)
FetchChannel returns the latest snapshot for handle. The handle may be a bare vanity name (`suenot`) or a full profile URL.
func (*LinkedInParser) FetchRecentPosts ¶
func (p *LinkedInParser) FetchRecentPosts(ctx context.Context, handle string, since time.Time) ([]shared.PostSnapshot, error)
FetchRecentPosts is best-effort. Profile pages don't expose a post stream without hitting /detail/recent-activity/shares/ as the logged-in user — which costs a second request and a second cookie surface. For now we extract whatever Article items happen to appear in LD-JSON (rare, but free) and return (nil, nil) for everything else.
func (*LinkedInParser) Platform ¶
func (p *LinkedInParser) Platform() shared.Platform
Platform implements shared.Parser.