Metadata-Version: 2.4
Name: lp-auto
Version: 0.1.1
Summary: 基于 DrissionPage 的自动化工具包：浏览器复用、登录、搜索筛选、翻页与简历信息导出
Author-email: Chandler <275737875@qq.com>
License: MIT
Keywords: drissionpage,automation,browser,cli,typer
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typer>=0.9.0
Requires-Dist: DrissionPage>=4.0.0
Requires-Dist: requests>=2.25.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: build>=1.0.0; extra == "dev"
Requires-Dist: twine>=4.0.0; extra == "dev"
Dynamic: license-file

# lp-auto

> 基于 [DrissionPage](https://github.com/g1879/DrissionPage) 的 liepin 自动化工具包。

`lp-auto` 把「打开浏览器 → 登录 → 搜索筛选 → 翻页 → 导出简历」这一整套重复劳动，封装成了一个 **既能命令行调用、也能当作 Python 库导入** 的工具包。它最大的特点是：**复用同一个 Chrome 浏览器**，登录一次、长期有效，后续脚本直接“接管”而不必反复扫码/输入密码。

---

## 目录

- [它能做什么](#它能做什么)
- [安装](#安装)
- [30 秒快速开始](#30-秒快速开始)
- [命令行用法](#命令行用法)
- [Python 函数用法](#python-函数用法)
- [示例脚本](#示例脚本)
- [配置说明](#配置说明)
- [项目结构](#项目结构)
- [如何扩展](#如何扩展)
- [核心原理解释](#核心原理解释)
- [常见问题](#常见问题)
- [许可证](#许可证)

---

## 它能做什么

| 能力 | 说明 |
| --- | --- |
| **浏览器复用** | 首次启动 Chrome，之后直接接管，登录态、标签页全保留 |
| **人工登录** | 打开登录页挂起，等你扫码/输密码，登录后回车继续 |
| **登录校验** | 自动访问首页，判断是否被重定向回 `/login` |
| **搜索筛选** | 按 placeholder 定位输入框，填入关键词、职位、公司 |
| **翻页** | 循环点击「下一页」，直到最后一页 |
| **简历导出** | 点开卡片详情 → 读取文本 → 存成 `.txt` 文件 → 关闭 |

---

## 安装

直接使用 pip 安装：

```bash
pip install lp-auto
```

> 依赖会自动安装：`typer`、`DrissionPage`、`requests`。安装完成后，`lp-auto` 命令即可在终端使用。


---

## 30 秒快速开始

**第一步：登录一次（只需做一次）**

```bash
lp-auto login
```

程序会自动启动一个 Chrome 浏览器，停在 liepin 登录页。你在浏览器里完成登录，页面跳转到工作台后，回到终端按 **回车**。

> 关掉终端也没关系——浏览器会保留，登录态也在。

**第二步：导出简历（以后每天都这样）**

先确认浏览器已打开并登录，然后进入搜索结果页后运行：

```bash
lp-auto collect --save-dir ./resumes
```

它会把当前页所有简历卡片保存下来，存到 `./resumes/` 里。

**第三步：一键搜索 + 逐页导出**

```bash
lp-auto run --save-dir ./resumes --max-pages 20
```

它会自动：打开搜索页 → 读取搜索词 → 搜索 → 导出第 1 页 → 翻页 → 导出第 2 页…… 直到最后一页或达到 `--max-pages`。

就这么简单。

---

## 命令行用法

所有命令都带 `--help`，随时查看：

```bash
lp-auto --help
lp-auto login --help
```

### `login` — 登录

```bash
lp-auto login
lp-auto login --no-verify        # 登录后不做自动校验
```

### `verify` — 校验登录态

```bash
lp-auto verify
```

成功（未被重定向）退出码为 `0`，失败为 `1`，方便写脚本判断。

### `search` — 搜索筛选

支持两种方式：命令行直接传参，或 JSON 配置。

**方式一：直接传参数（最方便）**

```bash
lp-auto search --keyword Python --job 后端开发 --company 字节跳动
```

三个参数都可选，省略即不筛选该字段：

```bash
lp-auto search --keyword Python          # 只按关键词
lp-auto search --job 后端开发            # 只按职位
```

**方式二：JSON 配置**

```bash
lp-auto search --config ./search_config.json
```

`--config` 指向一个 JSON 文件，格式如下（字段名固定，值为空则跳过）：

```json
{
  "keyword": "Python",
  "job": "后端开发",
  "company": ""
}
```

不传 `--config` 也不传参数时，默认读 `~/.lp_auto/search_config.json`。

### `collect` — 导出当前页简历

```bash
lp-auto collect                          # 全部导出
lp-auto collect --start 3                # 从第 3 张开始
lp-auto collect --limit 10               # 最多 10 张
lp-auto collect --step 2                 # 隔一张导出一张
lp-auto collect --delay 1.5              # 每张之间歇 1.5 秒
lp-auto collect --save-dir ./resumes     # 保存目录
```

### `paginate` — 翻页

```bash
lp-auto paginate --max-pages 20 --delay 2
```

### `identify` — 调试：识别页面输入框

页面改版导致输入框定位不到时，用它看看当前页面有哪些输入框、placeholder 是什么：

```bash
lp-auto identify
```

### `run` — 一键任务

```bash
lp-auto run --save-dir ./resumes --max-pages 20 --config ./search_config.json
lp-auto run --save-dir ./resumes --keyword Python --job 后端开发   # 也可直接传参
```

等效于：进搜索页 → 搜索 → 逐页导出。

### 公共参数

以下参数对所有命令生效：

| 参数 | 说明 | 默认 |
| --- | --- | --- |
| `--port` | Chrome 调试端口 | `46517` |
| `--chrome` | Chrome 可执行文件路径 | 系统默认路径 |
| `--user-data` | Chrome 用户数据目录 | `D:\selenium_profile` |
| `--save-dir` | 简历保存目录 | `~/.lp_auto/resumes` |

---

## Python 函数用法

### 最简示例

```python
from lp_auto import AppConfig, BrowserManager, Collector

cfg = AppConfig(chrome_exe=r"D:\Chrome\chrome.exe")  # 可选，覆盖默认配置
page = BrowserManager(cfg).get_page()                 # 接管浏览器
count = Collector(cfg).collect_page(page, limit=10)       # 导出 10 份简历
print(f"共保存 {count} 份")
```

### 完整业务流程

```python
from lp_auto import (
    AppConfig, BrowserManager, LoginFlow,
    SearchController, Pagination, Collector,
)

cfg = AppConfig(save_dir="./resumes")
manager = BrowserManager(cfg)

# 1. 登录（首次需要人工扫码）
page = manager.get_page(open_url="https://lpt.liepin.com/login")
LoginFlow().wait_manual_login(page)
LoginFlow().verify(page)

# 2. 打开搜索页并输入条件（用返回值接住新标签页）
search_tab = page.new_tab("https://lpt.liepin.com/search")
search = SearchController(cfg)
search.search(search_tab, {"keyword": "Python", "job": "后端开发"})

# 3. 在搜索标签页上逐页导出
pagination = Pagination()
collector = Collector(cfg)
total = 0
while True:
    total += collector.collect_page(search_tab)
    if pagination.is_last_page(search_tab):
        break
    pagination.go_next_page(search_tab)

print(f"总共导出 {total} 份")
```

### 异常处理

所有异常继承自 `lp_auto.exceptions.LpAutoError`，可以统一捕获：

```python
from lp_auto import BrowserManager, AppConfig
from lp_auto.exceptions import BrowserLaunchError, LoginExpiredError

try:
    page = BrowserManager(AppConfig()).get_page()
except BrowserLaunchError:
    print("Chrome 启动失败，检查路径配置")
except LoginExpiredError:
    print("登录态失效，请重新登录")
```

---

## 示例脚本

`examples/` 目录下提供了可直接运行的示例，覆盖从登录到完整流水线的常见场景。所有示例都假设你已 `pip install lp-auto`，并（如需登录）先用 `lp-auto login` 完成一次登录。

### `quickstart.py` — 最简导出

接管已登录的浏览器，导出当前页前 10 份简历。运行前请确保浏览器已停在你想导出的搜索结果页。

```bash
python examples/quickstart.py
```

```python
"""最简示例：接管浏览器，导出当前页前 10 份简历。

运行前请确保已经用 `lp-auto login` 完成过登录，
并且浏览器当前停在你想要导出的搜索结果页。
"""
from lp_auto import AppConfig, BrowserManager, Collector


def main() -> None:
    cfg = AppConfig(save_dir="./resumes")  # 覆盖默认保存目录
    page = BrowserManager(cfg).get_page()  # 接管已登录的浏览器

    print(f"当前页: {page.url}")
    saved = Collector(cfg).collect_page(page, limit=10)
    print(f"完成，共保存 {saved} 份")


if __name__ == "__main__":
    main()
```

### `login_demo.py` — 登录与校验

打开登录页 -> 等待人工登录 -> 校验登录态。

```bash
python examples/login_demo.py
```

```python
"""登录示例：打开登录页 -> 等待人工登录 -> 校验登录态。

    python examples/login_demo.py
"""
from lp_auto import AppConfig, BrowserManager, LoginFlow


def main() -> None:
    cfg = AppConfig()
    manager = BrowserManager(cfg)
    flow = LoginFlow()

    page = flow.open_login(manager)   # 启动/接管浏览器并打开登录页
    flow.wait_manual_login(page)      # 提示人工登录，按回车继续
    ok = flow.verify(page)            # 访问首页校验是否被重定向

    if not ok:
        raise SystemExit("登录态校验失败，请重试")


if __name__ == "__main__":
    main()
```

### `run_pipeline.py` — 完整流水线

搜索 -> 逐页导出简历，直到最后一页或达到最大页数。

```bash
python examples/run_pipeline.py
```

```python
"""完整流水线示例：搜索 -> 逐页导出简历。

    python examples/run_pipeline.py
"""
from lp_auto import (
    AppConfig,
    BrowserManager,
    Collector,
    Pagination,
    SearchController,
)


def main() -> None:
    cfg = AppConfig(save_dir="./resumes")
    page = BrowserManager(cfg).get_page()

    # 1. 打开搜索页并输入筛选条件（用返回值接住新标签页）
    search_tab = page.new_tab("https://lpt.liepin.com/search")

    search = SearchController(cfg)
    search.search(search_tab, {"keyword": "Python", "job": "后端开发"})

    # 2. 在搜索标签页上逐页导出
    pagination = Pagination()
    collector = Collector(cfg)

    total = 0
    max_pages = 20
    for _ in range(max_pages):
        total += collector.collect_page(search_tab)
        if pagination.is_last_page(search_tab):
            print("已到最后一页，结束")
            break
        pagination.go_next_page(search_tab)

    print(f"流水线完成，共导出 {total} 份简历")


if __name__ == "__main__":
    main()
```

### `search_config.json` — 搜索词配置

供 `lp-auto search --config` 或 `SearchController.load_config()` 使用的搜索词文件，字段名固定、值为空则跳过该字段：

```json
{
  "keyword": "Python",
  "job": "后端开发",
  "company": ""
}
```

把它放到 `~/.lp_auto/search_config.json` 即可被默认读取，或用 `--config` 指定任意路径。

---

## 配置说明

三种配置方式，优先级从高到低：

### 1. 代码里直接传参

```python
cfg = AppConfig(
    debug_port=47100,
    chrome_exe=r"D:\Chrome\chrome.exe",
    user_data=r"D:\chrome_profile\lp",
    save_dir="./resumes",
)
```

### 2. 环境变量（前缀 `LP_AUTO_`）

```bash
# Windows (PowerShell)
$env:LP_AUTO_CHROME_EXE = "D:\Chrome\chrome.exe"
$env:LP_AUTO_DEBUG_PORT = "47100"
$env:LP_AUTO_SAVE_DIR = "D:\resumes"
```

```python
cfg = AppConfig.from_env()   # 自动读取上述环境变量
```

支持的环境变量：`LP_AUTO_DEBUG_PORT`、`LP_AUTO_USER_DATA`、`LP_AUTO_CHROME_EXE`、`LP_AUTO_DATA_DIR`、`LP_AUTO_SAVE_DIR`、`LP_AUTO_MIN_CARD_WIDTH`。

### 3. 命令行参数

```bash
lp-auto collect --chrome "D:\Chrome\chrome.exe" --save-dir ./resumes
```

### 配置文件位置

| 文件 | 路径 |
| --- | --- |
| 会话记录（自动生成） | `~/.lp_auto/lp_session.json` |
| 搜索词（可用 `--config` 覆盖） | `~/.lp_auto/search_config.json` |
| 简历默认保存目录 | `~/.lp_auto/resumes/` |

---

## 项目结构

采用 **src 布局**，把“易变的选择器/URL”和“稳定的业务逻辑”分离开来：

```
lp_auto/
├── pyproject.toml              # 打包与依赖配置
├── README.md
├── LICENSE
├── examples/                   # 可运行的示例脚本
│   ├── quickstart.py
│   ├── login_demo.py
│   └── run_pipeline.py
├── tests/                      # 单元测试
│   └── test_config.py
└── src/
    └── lp_auto/
        ├── __init__.py         # 导出公共 API
        ├── __main__.py         # 支持 python -m lp_auto
        ├── cli.py              # Typer 命令入口（只做分发，无业务逻辑）
        ├── config.py           # 统一配置 AppConfig
        ├── constants.py        # URL、选择器、超时等常量
        ├── exceptions.py       # 异常体系
        └── core/
            ├── browser.py      # 浏览器复用管理器
            ├── login.py        # 登录与校验
            ├── search.py       # 搜索筛选
            ├── pagination.py   # 翻页
            ├── collector.py      # 简历导出
            └── tabs.py         # 标签页管理
```

**分层原则（高内聚、低耦合）：**

- `core/` 每个模块只做一件事，彼此**不相互 import**，只共享 `config` 和 `constants`。
- 业务逻辑都通过 `page` 对象参数传递，而不是在模块内部偷偷创建浏览器。
- 选择器、URL 集中放在 `constants.py`；页面改版时**只改这一处**。
- `cli.py` 只是一层薄薄的“翻译”，把命令行参数转成对 `core` 的调用。

---

## 如何扩展

### 场景 1：页面改版，选择器失效

打开 `src/lp_auto/constants.py`，修改对应的选择器列表即可。例如「下一页」按钮的 class 变了，改 `NEXT_SELECTORS`：

```python
NEXT_SELECTORS = [
    "css:.ant-pagination-next",
    "css:.my-new-next-button",   # 新增一个候选
]
```

多个候选会按顺序尝试，第一个命中的生效。

### 场景 2：新增一个搜索字段

在 `constants.py` 的 `FIELD_HINTS` 里加一条即可：

```python
FIELD_HINTS = {
    "keyword": ["搜职位/公司"],
    "job": ["搜索职位"],
    "company": ["搜索公司"],
    "city": ["搜索城市"],        # 新增字段
}
```

然后搜索词 JSON 里就能写 `"city": "北京"` 了。

### 场景 3：新增一条命令

在 `cli.py` 里用 `@app.command()` 加一个函数，内部调用 `core` 即可，例如：

```python
@app.command()
def export_all(
    save_dir: Optional[str] = _SAVE_DIR,
    max_pages: int = typer.Option(50),
):
    """导出所有页简历。"""
    cfg = _build_config(save_dir=save_dir)
    page = _manager(cfg).get_page()
    # ... 调用 core 完成逻辑
```

### 场景 4：把流程接进更大的系统

不要用 CLI，直接 `import lp_auto`，像「Python 函数用法」里那样组装 `BrowserManager` / `SearchController` / `Pagination` / `Collector`，完全自由。

---

## 核心原理解释

> 这一节用大白话讲清楚「为什么这样设计」，方便你判断和二次开发。

### 1. 为什么要“复用浏览器”而不是每次都开新的？

常规自动化（比如 Selenium）每次运行都开一个“干净”的新浏览器，没有登录、没有历史，于是每次都得重新登录。liepin 这类网站登录很麻烦（要扫码或验证码）。

`lp-auto` 的做法是：**用一个固定的 Chrome + 固定的调试端口启动一次**，然后把「端口号」记在一个小文件里。以后再运行，先去这个端口“敲门”（发个 HTTP 请求探测），如果浏览器还活着，就直接**接管**它——相当于你回到之前那个已经登录好的浏览器继续操作。于是登录一次，长期有效。

### 2. 什么是“接管”？它和“打开”有什么区别？

- **打开**：`subprocess` 拉起 Chrome 进程，带 `--remote-debugging-port=46517` 参数，告诉 Chrome「给我留一个 46517 端口的遥控接口」。
- **接管**：DrissionPage 通过 `set_local_port(46517)` 连上这个已经存在的遥控接口，而不是新建浏览器。

两者的差别就像：

> 「打开」= 买一台新手机并开机；「接管」= 拿起你桌子上那台已经登录好的手机继续用。

### 3. 选择器为什么用“候选列表”而不是“写死一个”？

网页的前端框架（这里是 antd）会自动生成一些带随机后缀的 class，比如 `ant-pagination-next` 在不同版本可能叫法略有差异。所以每个“要找的东西”都配了一串候选选择器，按顺序尝试，第一个能找到且可见的就用。这就像打开一扇门时，兜里揣了好几把钥匙，一把一把试。

### 4. 为什么输入框用 placeholder 定位而不是 id？

页面上的 `id`（如 `rc_select_1`）是前端框架自动编号的，产品一改版数字就可能变。而 `placeholder`（灰色提示文字）是产品经理写死的文案，相对稳定得多。所以我们用「搜索职位」「搜索公司」这类文字去匹配输入框，更抗改版。

### 5. 为什么点击“下一页”前后要做那么多判断？

翻页是“异步”的：点完按钮，新页面内容要过一会儿才渲染出来。所以流程是：

1. 记录翻页前的页码；
2. 点击按钮；
3. **轮询等待**页码发生变化（最多等 10 秒）；
4. 若按钮是「禁用/灰色」状态，说明到最后一页了，停止。

这一点细微的「等待 + 判断」，是自动化脚本稳定性的关键。

---

## 常见问题

**Q1：提示 `Chrome 启动失败`？**

检查 `--chrome` 和 `--user-data` 两个路径是否正确。`chrome_exe` 默认是 `C:\Program Files\Google\Chrome\Application\chrome.exe`，`user_data` 默认是 `D:\selenium_profile`，请改成你机器上真实存在（或想让其创建的）路径。

**Q2：运行时把浏览器也一起关了？**

不要调用页面的 `quit()`。本包所有接口都不会主动关闭浏览器，接管只是“借用”，退出脚本后浏览器仍然保留。

**Q3：登录态失效怎么办？**

重新 `lp-auto login` 登录一次即可。也可以 `lp-auto verify` 先探测一下当前是否还生效。

**Q4：导出的文件是空的？**

说明点击卡片后弹的不是标准模态框，或元素定位失效。先用 `lp-auto identify` 或修改 `constants.py` 里的 `MODAL_CONTENT_SELECTORS` / `CARD_SELECTOR`。

**Q5：怎么设置搜索词？**

默认读 `~/.lp_auto/search_config.json`，或 `lp-auto search --config ./my.json`。

---

## 许可证

[MIT](./LICENSE)

---

## 免责声明

> 在使用本项目（下称「本工具」）之前，请务必仔细阅读并充分理解本声明的全部内容。一旦下载、安装、运行或以任何方式使用本工具，即视为你已同意并接受本声明的全部条款。

### 一、使用目的

本工具仅供**学习、研究及技术交流**使用，旨在演示浏览器自动化、页面数据提取等编程技术。本工具**不提供、不鼓励、不协助**任何针对 liepin 平台（liepin.com）或其他任何网站进行的批量抓取、数据采集、账号滥用或规避反爬机制等行为。

### 二、合规与授权

1. 使用本工具不得违反《中华人民共和国网络安全法》《中华人民共和国数据安全法》《中华人民共和国个人信息保护法》以及目标网站所在地的其他适用法律法规。
2. 你应当自行查阅并严格遵守 liepin 平台的服务条款、用户协议、隐私政策及 robots 协议等平台规定，并**自行取得**使用相关数据的合法授权与许可。
3. 本工具不用于：批量抓取简历/职位数据、采集或存储他人个人信息、绕过验证码或登录保护、干扰平台正常服务，或其他任何可能损害第三方合法权益的用途。
4. 若你计划从事任何数据采集活动，须确保已获得数据主体及平台的明确授权，并对所采集数据的合法性、正当性与必要性负责。

### 三、风险自负

1. **账号风险**：不正当使用可能导致你的 liepin 账号被限制、封禁，或在法律层面被追究责任。此类风险及后果**完全由使用者自行承担**。
2. **法律风险**：因使用本工具产生的任何民事、行政或刑事责任，均由使用者自行承担。
3. **数据风险**：本工具生成或导出的数据可能存在不准确、不完整、过期等情况，作者不对数据的真实性、准确性与可用性作出任何保证。

### 四、责任免除

本项目按「现状（AS IS）」提供，作者**不作任何明示或暗示的保证**，包括但不限于对适销性、特定用途适用性、不侵权及持续可用性的保证。在适用法律允许的最大范围内，因安装、使用或无法使用本工具而造成的任何直接、间接、偶然、附带、特殊或后果性损失（包括但不限于数据丢失、账号封禁、利润损失、业务中断、行政处罚、诉讼及赔偿责任），作者及贡献者**均不承担任何责任**，即使已被告知发生此类损失的可能性。

### 五、其他

使用者应独立判断本工具的适用性，并对其使用行为负全部责任。若本声明的任何条款被认定无效或不可执行，不影响其余条款的效力。本声明的解释权及适用以中华人民共和国法律为准。
