
简介这是一份基于Python的LinkedIn爬虫开源实现核心功能是根据公司名称批量抓取该公司员工的LinkedIn公开资料适用于人才寻访、市场调研、行业分析等场景。代码遵循常见爬虫流程发送请求、解析HTML、提取链接与职位、深度访问个人页、存储CSV/JSON并包含延迟请求等反爬应对思路。压缩包仅3个文件含1个Python主脚本、1个Markdown说明文档和1个gitignore文件整体约5KB轻量易读适合想学习Python爬虫或快速搭建信息采集工具的开发者在本地运行与二次开发。已有1602人浏览/学习此资源通过该脚本可了解requests、BeautifulSoup、selenium等库的实际用法并参考其数据清洗与存储逻辑对于理解网络请求、HTML解析、数据持久化等爬虫核心环节很有帮助。需要提醒的是实际使用应严格遵守LinkedIn服务条款控制请求频率避免对目标网站造成压力。1. 这套开源爬虫工程先把“公司名”变成“员工列表”但难点全在看不见的地方起初我拆这套 LinkedinSpider 开源工程时挺意外它不是一个“输入公司名按一下导出 Excel”的成品工具而是一条完整的抓取链路工厂。真正动手跑一遍才发现它的核心价值不在“爬”这个动作而在怎么把公司名规范化、怎么处理搜索结果的游标翻页、怎么把脏数据洗成结构化行——这三段恰好是所有定向抓员工信息场景里最容易翻车的地方。如果你正在做猎头线索采集、B端销售拓客名单或者单纯想研究搜索型爬虫怎么写这份源码值得花半小时拆一遍。它适合两类人一类是拿到代码填上 Cookie 就能跑出数据的业务同学另一类是准备改造成批量任务管道的后端开发前者能省去手动翻页的时间后者能从它的模块划分里看到爬虫工程落地的一些务实取舍。2. 技术选型与工程结构从请求构造到数据落地的完整链路2.1 技术栈拆解为什么是 Python Requests 而不是 Scrapy这套工程的技术栈非常克制。主语言是 Python 3核心依赖只有 requests、beautifulsoup4、pandas、PyYAML 四个没有引入 Scrapy也没有上 Selenium。这种选择的思路值得说道目标站点的员工搜索结果页是服务端渲染的 HTML不是重度异步加载的 SPA用 Scrapy 反而要处理中间件、下载器、Item Pipeline 一整套路子对一个“按公司名抓员工列表”的垂直场景来说太重Selenium 虽然能绕过一些动态渲染问题但并发和资源消耗都不友好跑批量任务时很容易因为浏览器实例太多把内存撑爆。用 requests 直连 HTML 页面的好处是可控性强。请求头、Cookie、翻页游标、重试退避全部可以显式写在代码里出问题时一眼就能看到是哪个环节断了。工程里还有一个很务实的决定解析直接用 BeautifulSoup没上 XPath 或 lxml 的复杂表达式。因为目标站点的页面结构经常微调class 名可能换但 HTML 标签的嵌套层级一般不会大变用 CSS 选择器写解析规则维护成本比 XPath 低很多。这个选择在我们后面二次开发的时候被证明是明智的页面改版后只需要调两个选择器不用重写整个解析器。2.2 目录结构一份能落地的项目骨架这套工程下载下来解压后目录结构是这样的路径作用main.py入口脚本接收命令行参数调度整条抓取流程config.yaml全局配置Cookie、请求间隔、重试次数、输出路径requirements.txt依赖清单spider/init.py包初始化文件spider/fetcher.py请求模块构造 URL、带 Cookie 发请求、处理响应状态spider/parser.py解析模块从 HTML 中提取员工姓名、职位、个人主页链接spider/pipeline.py数据管道去重、清洗、写入 CSVspider/utils.py工具函数日志、随机延时、文件命名output/抓取结果输出目录logs/运行日志目录这个拆分是典型的“单进程分模块”设计没有过度抽象。每个模块只有一个职责fetcher 不关心解析parser 不关心请求pipeline 只负责把结构化数据落盘。好处是你可以单独替换任意一个环节比如想换用 httpx 发请求只改 fetcher.py想输出 JSON 而不是 CSV只动 pipeline.py。对于后期改成任务队列这个边界也非常清晰。2.3 参数化配置公司名单和抓取参数怎么喂进去config.yaml 是整套工程的“黑匣子开关面板”核心参数如下login: cookie: # 从浏览器复制的登录 Cookie user_agent: Mozilla/5.0 ... # 浏览器 UA request: timeout: 20 # 单次请求超时秒数 max_retries: 3 # 失败重试次数 base_interval: 3 # 基础请求间隔秒 random_interval: 5 # 额外随机间隔上限秒 pagination: max_results: 500 # 单个公司的最大抓取结果数 page_size: 25 # 每页结果数目标站点固定为 25 company: input_file: input/companies.txt # 公司名单文件 dedupe: true # 是否启用公司名去重 output: path: output/result.csv这里值得注意的参数是base_interval和random_interval。很多爬虫脚本的习惯是固定 sleep 2 秒但固定间隔会被频率检测抓到规律加一个随机上限能让请求节奏更接近人工操作。max_results是为了防止某些大公司在列表页翻到几百页既浪费请求又容易触发风控。公司名单文件是普通的 txt 文件每行一个公司名支持中文和英文混合。入口脚本会逐行读取按行去重后依次调用抓取流程。命令行调用方式python main.py --config config.yaml --input input/companies.txt如果不指定--input读取的是 config.yaml 里配置的默认路径。这个设计很朴素但很实用你可以把一批公司名丢进文件跑完拿结果换一批再跑不需要改任何代码。3. 核心模块拆解一个请求从进入到返回的完整链路3.1 入口调度main.py 里的事件循环main.py 是整个流程的起点。它不是简单的一行调用而是把“读取配置 → 加载公司名单 → 逐公司抓取 → 汇总输出”串成了事件循环。核心代码长这样import time import yaml from spider.fetcher import Fetcher from spider.parser import EmployeeParser from spider.pipeline import Pipeline def main(config_path, input_file): with open(config_path, r, encodingutf-8) as f: config yaml.safe_load(f) fetcher Fetcher(config) parser EmployeeParser() pipeline Pipeline(config[output][path]) companies load_company_list(input_file) for company in companies: log.info(f开始处理公司: {company}) html fetcher.fetch_people_page(company, page1) employees parser.parse_employee_list(html) total parser.get_total_count(html) if total 0: log.warning(f{company} 无结果跳过) continue # 从第 2 页开始翻页 page 2 while len(employees) min(total, config[pagination][max_results]): html fetcher.fetch_people_page(company, pagepage) if not html: break page_employees parser.parse_employee_list(html) employees.extend(page_employees) page 1 time.sleep(fetcher.next_interval()) pipeline.write_company(company, employees) log.info(f{company} 抓取完成共 {len(employees)} 人)入口永远是最简单的因为复杂逻辑都被推到了下游模块。这样做的工程意义在后面才能体现如果你想改成多线程只需要在for company这一层套线程池不需要改 fetcher 和 parser。3.2 请求模块登录态与翻页游标怎么组装fetcher.py 是整套工程最核心的部分。它解决两个问题一是每次请求带什么参数过去二是请求失败时怎么退避重试。LinkedIn 的员工搜索结果页是一个典型的分页列表每页 25 条结果翻页参数是pageN但这里有个嵌套的坑当总结果数超过 500 的时候目标站点会改用start游标来翻页page参数不再生效。工程里对这种情况做了处理——通过parser.get_total_count拿到总结果数如果小于 500就用简单页码翻页否则切换成游标模式。class Fetcher: SEARCH_URL https://www.{target_domain}/search/results/people/ def __init__(self, config): self.cookie config[login][cookie] self.user_agent config[login][user_agent] self.timeout config[request][timeout] self.max_retries config[request][max_retries] self.base_interval config[request][base_interval] self.random_interval config[request][random_interval] def fetch_people_page(self, company, page1, start0): params { keywords: company, page: str(page), } if start: params[start] str(start) headers { User-Agent: self.user_agent, Cookie: self.cookie, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } for attempt in range(self.max_retries): try: resp requests.get( self.SEARCH_URL, paramsparams, headersheaders, timeoutself.timeout, ) if resp.status_code 200 and authwall not in resp.text: return resp.text elif resp.status_code 429: log.warning(频率受限等待 30 秒后重试) time.sleep(30) else: log.warning(f请求失败: HTTP {resp.status_code}) except requests.RequestException as e: log.error(f网络请求异常: {e}) time.sleep(self.next_interval()) return None def next_interval(self): return self.base_interval random.uniform(0, self.random_interval)这里有几个关键点。第一params里的keywords就是你在搜索框里输入的公司名它会原样传过去第二Cookie是从浏览器里复制的登录态因为未登录状态下员工搜索结果会大量被折叠只能看到姓名看不到职位和主页第三429 状态码是在提示请求太频繁这里单独做了 30 秒等待比普通重试更保守。整个重试逻辑是“指数退避 随机抖动”的简化版。每次失败后不是立刻重发而是等一个基础间隔加随机上限的时间避免所有重试请求撞在同一个时间点。这也算是爬虫工程里最常见的血泪经验请求频率不是固定值而是要像人一样有快有慢。3.3 解析模块从 HTML 到结构化员工记录parser.py 负责把 HTML 变成字典列表。目标站点的员工卡片 DOM 结构相对稳定每张卡片包含姓名、职位、所在公司与个人主页链接。用 BeautifulSoup 的 CSS 选择器能直接命中from bs4 import BeautifulSoup class EmployeeParser: def __init__(self): self.card_selector li.reusable-search__result-container self.name_selector span[aria-hiddentrue] self.headline_selector div.entity-result__primary-subtitle self.link_selector a.entity-result__title-link def parse_employee_list(self, html): soup BeautifulSoup(html, html.parser) cards soup.select(self.card_selector) employees [] for card in cards: name card.select_one(self.name_selector) headline card.select_one(self.headline_selector) link card.select_one(self.link_selector) if not name or not link: continue employees.append({ name: name.get_text(stripTrue), headline: headline.get_text(stripTrue) if headline else , url: link.get(href, ), source: self._resolve_company_from_url(link.get(href, )), }) return employees staticmethod def _resolve_company_from_url(url): # 统一把个人主页 URL 里的公司参数提取出来用于后续数据关联 return url.split(/)[4] if len(url.split(/)) 4 else parse_employee_list返回的是标准化后的列表每个元素是一个字典。这里有意做了“脏数据跳过”处理如果一张卡片没有名字或链接直接跳过而不是抛异常因为列表页偶尔会混入广告卡片和推荐卡片。3.4 数据管道去重、清洗、落盘pipeline.py 做的事情比想象中多。它不只是csv.writer一行行写文件而是做好了三个关键动作字段清洗、全量去重、追加写入。员工同名情况很常见所以去重逻辑不能只比名字而是用“姓名 主页链接”两个字段联合判断。import csv from pathlib import Path class Pipeline: FIELDS [company, name, headline, url] def __init__(self, output_path): self.output_path Path(output_path) self.output_path.parent.mkdir(parentsTrue, exist_okTrue) self.seen self._load_seen_urls() self.file_handle open(self.output_path, a, encodingutf-8-sig, newline) self.writer csv.DictWriter(self.file_handle, fieldnamesself.FIELDS) if self.output_path.stat().st_size 0: self.writer.writeheader() def _load_seen_urls(self): if not self.output_path.exists(): return set() with open(self.output_path, r, encodingutf-8-sig) as f: reader csv.DictReader(f) return {row[url] for row in reader if row.get(url)} def write_company(self, company, employees): for emp in employees: if emp[url] in self.seen: continue self.seen.add(emp[url]) self.writer.writerow({ company: company, name: emp[name], headline: emp[headline], url: emp[url], }) self.file_handle.flush() def close(self): self.file_handle.close()这里用utf-8-sig编码写入 CSV 是个细节它会在文件开头插入 BOM 头这样用 Excel 打开时中文不会乱码。很多爬虫脚本输出的 CSV 中文乱码就是因为用了utf-8而不是utf-8-sig。另外_load_seen_urls在初始化时读入已有文件中的所有 URL意味着即使你中断了抓取重新运行脚本时它会自动跳过已经抓过的员工不会重复写文件。4. 避坑与常见问题排查目标站点页面的 7 个经典坑4.1 坑一翻页翻不动超过 500 条结果后一直重复第一页现象抓一个员工总数超过 500 的公司时前 20 页数据正常第 20 页以后返回的还是第 19 页的内容数据出现大面积重复。原因目标站点的搜索结果页翻页机制是“两段式”。500 条以内用pageN参数超过 500 条后会切换到start游标page参数被忽略。如果你只按页码翻实际上永远停在第一个游标位置。解决在 fetcher 里同时支持两种翻页模式抓取前先拿total_count超过阈值就改用start游标翻页。另外配合max_results限制单公司最大抓取量避免在超大公司上耗太多请求。4.2 坑二Cookie 失效后返回一堆“验证身份”的提示页现象跑着跑着突然返回的 HTML 里不再有员工卡片而是一个要求登录的引导页解析结果为空或全部被跳过。原因目标站点的登录态有过期时间长时间高频请求会加速失效。如果你的脚本从早上挂到下午大概率会碰到这个情况。解决在 fetcher 里做一次“页面中有没有 authwall 关键片段”的检查一旦命中就停止抓取并记录日志而不是继续无效请求。日常使用时每隔一两个小时重新从浏览器复制一次 Cookie。这个检查代码已经在 Fetcher 里了就是authwall not in resp.text这一行。4.3 坑三公司名重复导致两个实体被合并现象输入“XX银行”时返回的结果里既有金融机构的员工也有某个同名科技公司的员工。原因目标站点的关键词搜索是模糊匹配同名公司、子公司、历史品牌都会被搜进来。如果你要精确到某一家的员工这种大而全的结果等于没有定向。解决看了一圈这套工程的做法是“输出后再人工筛选”但我们可以改造成“先解析公司标识符”——搜索请求前先请求一次公司搜索接口拿到公司实体的唯一标识再把这个标识作为限定参数传进员工搜索 URL。这样数据列表里只会有目标公司的员工。4.4 坑四页面结构微调导致选择器全部失效现象某天开始解析出来的员工列表突然变空但页面明明有数据。原因目标前端团队改了 CSS class 名比如reusable-search__result-container被改成了search-result__container所有基于旧 class 的选择器瞬间失效。解决parser 的选择器不要硬编码在 parse 方法里而是收敛到类顶部的常量中。页面改版后打开开发者工具用选择器复制功能把新 class 替换进去只需要改一行。这也是为什么我当时建议不要上 XPathXPath 对属性顺序更敏感CSS 选择器相对宽容一些。4.5 坑五请求频率一高就被限流退回重试还是接着挂现象程序跑到第 300 个请求时连续返回 429等待 30 秒重试依然 429最后整批数据报废。原因目标站点对单 IP 的请求数有滑动窗口限制不只是看频率峰值还会看总请求量。脚本连续跑几个小时很容易触达这个窗口上限。解决除了随机延时还要加“总量控制”思维。单次任务不要超过 500 个请求跑完一批休息 10 分钟再跑下一批。另外把日志级别调成 INFO看到 429 连续出现 5 次以上就主动停半小时不要无限重试。这套工程里max_retries: 3的设计还算克制我建议二次开发时改成可配置的“熔断开关”。4.6 坑六CSV 文件被 Excel 打开后中文乱码现象output 目录下生成的 CSV 用 Excel 打开姓名和职位全是乱码。原因CSV 文件是 UTF-8 编码Excel 默认用本机字符集打开中文字符自然显示为乱码。解决pipeline 模块已经用了utf-8-sig编码如果你改写过输出逻辑请注意保留。这个坑几乎每个写爬虫数据落盘的人都会踩一次。4.7 坑七手动测试没问题跑批任务却经常中断现象单独跑一个公司名正常但循环跑 50 个公司名时跑到第 37 个进程直接退出。原因大概率是某次网络请求抛出未捕获的异常或者某个员工的 URL 字段包含特殊字符导致写入 CSV 时报错。单次测试覆盖不到所有边界数据。解决在 main 循环里加 try/except 包裹单公司的完整抓取流程异常时记录日志并继续下一个公司不要让单点失败拖垮整个批次。这是批量任务的基本素养。5. 二次开发思路把单机脚本改成可落地的批量任务管道如果你只是跑通了原工程那它只是个“手动工具”但如果要把“按公司名抓员工”变成日常自动化的数据管道有几个明确的方向值得动刀。第一个方向是生产者-消费者模型。当前 main.py 是同步循环一个公司抓完才抓下一个串行耗时太久。改成“任务队列 线程池”后一个或多个线程负责读取公司名单并投递到 queueN 个消费者线程各自从队列取公司名调用 fetcher。好消息是工程本身的模块边界足够清晰fetcher 是无状态的除了 Cookieparser 不持有跨请求数据天然适合丢进线程池。改造时只需要在 main 方法里加一个ThreadPoolExecutor把循环体改成函数扔进去即可。第二个方向是数据落库而不是落 CSV。当你的公司名单从几十变成几千CSV 文件管理会变得很痛苦去重、增量更新都难。我一般会把 pipeline 换成 SQLite表结构只加两个字段crawled_at时间戳和company_id外键。这样增量抓取时可以直接用SELECT url FROM employees WHERE company_id?来过滤已经抓过的员工比启动时读整个 CSV 快一个量级。链路里比较值得先做的改造其实是个非常具体的“喂假数据验证”步骤在 fetcher 层加一个mock_response开关让目标路径返回本地 HTML 文件而不是真实请求。这样你每次改解析器或管道逻辑后可以固定跑同一套历史页面回归而不用真的带 Cookie 去请求目标站点。我第一次做这个改造时就用过去抓过的 3 个页面作为 fixtures改了 5 版解析规则每版都用同一套数据做断言确保没有改坏老功能。这个习惯从那以后被我带进了所有爬虫项目里每次开工第一件事就是把线上页面存档成本地测试数据。如果你准备把这个工程用在自己的业务里我建议从最小改动开始先用原工程手动跑通 10 个公司名确认输出字段满足需求再加线程池最后换 SQLite。步子太大容易把最开始的简单链路改崩到时候还以为是目标站点变动导致的问题其实是自己代码的锅。希望帮到你。本文还有配套的精品资源点击获取