html-reader-llm 0.0.1__tar.gz → 0.0.2__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.
- html_reader_llm-0.0.2/.env.example +12 -0
- html_reader_llm-0.0.2/.gitignore +230 -0
- html_reader_llm-0.0.2/.python-version +1 -0
- html_reader_llm-0.0.2/AGENTS.md +102 -0
- {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/PKG-INFO +1 -1
- html_reader_llm-0.0.2/openspec/changes/chrome-path-detection/.openspec.yaml +2 -0
- html_reader_llm-0.0.2/openspec/changes/chrome-path-detection/design.md +60 -0
- html_reader_llm-0.0.2/openspec/changes/chrome-path-detection/proposal.md +25 -0
- html_reader_llm-0.0.2/openspec/changes/chrome-path-detection/specs/chrome-path-detection/spec.md +93 -0
- html_reader_llm-0.0.2/openspec/changes/chrome-path-detection/tasks.md +25 -0
- html_reader_llm-0.0.2/openspec/changes/dotenv-config/.openspec.yaml +2 -0
- html_reader_llm-0.0.2/openspec/changes/llm-analysis/.openspec.yaml +2 -0
- html_reader_llm-0.0.2/openspec/changes/llm-analysis/design.md +108 -0
- html_reader_llm-0.0.2/openspec/changes/llm-analysis/proposal.md +30 -0
- html_reader_llm-0.0.2/openspec/changes/llm-analysis/specs/llm-analysis/spec.md +119 -0
- html_reader_llm-0.0.2/openspec/changes/llm-analysis/tasks.md +41 -0
- html_reader_llm-0.0.2/openspec/changes/render-detection/.openspec.yaml +2 -0
- html_reader_llm-0.0.2/openspec/changes/render-detection/design.md +99 -0
- html_reader_llm-0.0.2/openspec/changes/render-detection/proposal.md +31 -0
- html_reader_llm-0.0.2/openspec/changes/render-detection/specs/render-detection/spec.md +106 -0
- html_reader_llm-0.0.2/openspec/changes/render-detection/tasks.md +34 -0
- html_reader_llm-0.0.2/openspec/changes/simplify-pipeline/.openspec.yaml +2 -0
- html_reader_llm-0.0.2/openspec/changes/simplify-pipeline/design.md +88 -0
- html_reader_llm-0.0.2/openspec/changes/simplify-pipeline/proposal.md +31 -0
- html_reader_llm-0.0.2/openspec/changes/simplify-pipeline/specs/html-simplification/spec.md +191 -0
- html_reader_llm-0.0.2/openspec/changes/simplify-pipeline/tasks.md +50 -0
- html_reader_llm-0.0.2/openspec/config.yaml +32 -0
- {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/pyproject.toml +1 -1
- html_reader_llm-0.0.2/tests/test_browser.py +205 -0
- html_reader_llm-0.0.2/tests/test_cli.py +137 -0
- html_reader_llm-0.0.2/tests/test_llm.py +231 -0
- html_reader_llm-0.0.2/tests/test_render_detect.py +304 -0
- html_reader_llm-0.0.2/tests/test_simplify.py +751 -0
- html_reader_llm-0.0.2/uv.lock +581 -0
- {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/LICENSE +0 -0
- {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/README.md +0 -0
- {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/__init__.py +0 -0
- {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/browser.py +0 -0
- {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/cli.py +0 -0
- {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/llm.py +0 -0
- {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/render_detect.py +0 -0
- {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/selector_validator.py +0 -0
- {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/settings.py +0 -0
- {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/simplify.py +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# htmlreader environment variables
|
|
2
|
+
|
|
3
|
+
# LLM API configuration
|
|
4
|
+
HTMLREADER_LLM_BASE_URL=
|
|
5
|
+
HTMLREADER_LLM_API_KEY=
|
|
6
|
+
HTMLREADER_LLM_MODEL=
|
|
7
|
+
|
|
8
|
+
# Optional overrides
|
|
9
|
+
# HTMLREADER_LLM_MAX_TOKENS=512000
|
|
10
|
+
# HTMLREADER_HTTP_TIMEOUT=15
|
|
11
|
+
# HTMLREADER_CHROME_TIMEOUT=30
|
|
12
|
+
# HTMLREADER_CHROME_PATH=
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
|
|
204
|
+
# Ruff stuff:
|
|
205
|
+
.ruff_cache/
|
|
206
|
+
|
|
207
|
+
# PyPI configuration file
|
|
208
|
+
.pypirc
|
|
209
|
+
|
|
210
|
+
# Marimo
|
|
211
|
+
marimo/_static/
|
|
212
|
+
marimo/_lsp/
|
|
213
|
+
__marimo__/
|
|
214
|
+
|
|
215
|
+
# Streamlit
|
|
216
|
+
.streamlit/secrets.toml
|
|
217
|
+
|
|
218
|
+
# claude
|
|
219
|
+
.claude/specs/
|
|
220
|
+
.claude/
|
|
221
|
+
|
|
222
|
+
# zcode
|
|
223
|
+
.zcode/
|
|
224
|
+
|
|
225
|
+
# env files
|
|
226
|
+
.env
|
|
227
|
+
|
|
228
|
+
# temporary files
|
|
229
|
+
.cache/
|
|
230
|
+
.temp/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# 项目上下文 (AI Agent 指令)
|
|
2
|
+
|
|
3
|
+
## 基本信息
|
|
4
|
+
|
|
5
|
+
- **项目**:htmlreader — 智能 HTML 解析与提取库
|
|
6
|
+
- **阶段**:Phase 1(核心功能开发)
|
|
7
|
+
- **技术栈**:Python 3.12+ / uv / httpx2 / selectolax
|
|
8
|
+
- **语言**:中文优先
|
|
9
|
+
|
|
10
|
+
## 常用命令
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
uv venv && uv sync # 安装依赖
|
|
14
|
+
uv run htmlreader # CLI
|
|
15
|
+
uv build # 构建
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 架构
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
输入 URL/HTML
|
|
22
|
+
│
|
|
23
|
+
├─ 自定义简化流程(后续参考草稿+开源代码)─→ minify-html ─→ LLM 分析结构
|
|
24
|
+
│
|
|
25
|
+
├─ 普通请求 vs 浏览器渲染 ─→ 差异度计算 + 文本统计
|
|
26
|
+
│
|
|
27
|
+
├─ httpx2 请求 ─→ LLM(页面分类 / CSS 选择器规则生成)
|
|
28
|
+
│ └─ tranfilatura 辅助元数据提取
|
|
29
|
+
│
|
|
30
|
+
└─ 自定义 CSS 提取语法 ─→ 低代码提取工作流
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 设计决策(探索阶段)
|
|
34
|
+
|
|
35
|
+
### HTML 简化
|
|
36
|
+
- minify-html 是喂给 LLM 之前的最后一步压缩
|
|
37
|
+
- 中间有自定义简化步骤,后续提供草稿代码 + 大量开源代码学习后确定
|
|
38
|
+
|
|
39
|
+
### 浏览器渲染判断
|
|
40
|
+
- 不用框架,用 Chrome 原生 `--dump-dom` 管道获取渲染后 HTML
|
|
41
|
+
- 第一版让用户填入 Chrome 路径(或 auto 自动查找)
|
|
42
|
+
- 关键参数:
|
|
43
|
+
- `--virtual-time-budget=` 预估等待 JS 异步加载
|
|
44
|
+
- `--disable-gpu`
|
|
45
|
+
- `--run-all-compositor-stages-before-draw`
|
|
46
|
+
- `--user-agent="指定的 UA"`
|
|
47
|
+
- `--headless`
|
|
48
|
+
- `--timeout=毫秒数`
|
|
49
|
+
- `--blink-settings=imagesEnabled=false` 禁用图片减少流量
|
|
50
|
+
- `--user-data-dir=` 缓存用户数据,减少重复加载
|
|
51
|
+
- 纯管道模式,不走 CDP 协议(后续可扩展)
|
|
52
|
+
- 渲染后 HTML 与 httpx2 结果对比,统计文本差异度
|
|
53
|
+
|
|
54
|
+
### CSS 提取语法(草稿)
|
|
55
|
+
- 三个核心字段:
|
|
56
|
+
- `selector`:CSS 选择器
|
|
57
|
+
- `value_mode`:取值方式
|
|
58
|
+
- `$html`:取元素内部完整 HTML
|
|
59
|
+
- `$text`:取纯文本(去标签)
|
|
60
|
+
- `@attribute`:取指定属性值(如 `@href`、`@src`)
|
|
61
|
+
- 交互形式(YAML / 链式 API / LLM 生成)后续再定
|
|
62
|
+
|
|
63
|
+
## 核心需求
|
|
64
|
+
|
|
65
|
+
### 1. HTML 简化(供 LLM 分析)
|
|
66
|
+
- 将原始 HTML 转换为适合 LLM 理解的简化格式
|
|
67
|
+
- 保留结构语义,去除噪声(样式、脚本、无用属性)
|
|
68
|
+
|
|
69
|
+
### 2. 渲染方式判断
|
|
70
|
+
- 对同一 URL,分别用普通 HTTP 请求和浏览器渲染获取结果
|
|
71
|
+
- 计算差异度(DOM 结构、文本量)
|
|
72
|
+
- 统计文本长度,判断是否需要浏览器渲染
|
|
73
|
+
- 输出决策:`use_browser: bool`
|
|
74
|
+
|
|
75
|
+
### 3. LLM 驱动的智能分析
|
|
76
|
+
- 使用 httpx2 发送 HTTP 请求与 LLM 通信
|
|
77
|
+
- **页面分类**:识别 `list`(列表页)/ `detail`(详情页)
|
|
78
|
+
- **CSS 选择器规则生成**:
|
|
79
|
+
- list 页:提取列表规则
|
|
80
|
+
- detail 页:提取正文 + 元数据规则
|
|
81
|
+
- **tranfilatura 协作**:提取元数据作为 LLM 上下文,提高选择器准确度
|
|
82
|
+
|
|
83
|
+
### 4. CSS 提取语法工作流
|
|
84
|
+
- 自定义一套文本化的 CSS 提取规则语法
|
|
85
|
+
- 低代码方式描述网页内容提取逻辑
|
|
86
|
+
- 支持组合、嵌套、条件选择
|
|
87
|
+
- 规则可序列化、可存储、可复用
|
|
88
|
+
|
|
89
|
+
## 依赖说明
|
|
90
|
+
|
|
91
|
+
| 依赖 | 用途 |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `httpx2` | 异步 HTTP 客户端,用于下载页面及 LLM API 通信 |
|
|
94
|
+
| `selectolax` | 高性能 HTML 解析(基于 Modest/Lexbor) |
|
|
95
|
+
| `minify-html` | HTML 极致压缩,缩短 LLM 提示词长度 |
|
|
96
|
+
|
|
97
|
+
## 关键概念
|
|
98
|
+
|
|
99
|
+
- **页面分类**:`list`(列表/索引页)vs `detail`(内容/详情页)
|
|
100
|
+
- **差异度**:普通请求与浏览器渲染结果的 DOM/文本相似度
|
|
101
|
+
- **提取规则**:文本化 CSS 选择器组合,描述从 HTML 中提取目标数据的逻辑
|
|
102
|
+
- **简化格式**:去除样式噪声后的 HTML,保留结构语义供 LLM 理解
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
## Context
|
|
2
|
+
|
|
3
|
+
The browser rendering feature needs a Chrome/Edge path. Users may not know where their browser is installed, so automatic detection is essential. The provided draft code in the conversation demonstrates the core approach: platform-specific path lists + verification.
|
|
4
|
+
|
|
5
|
+
## Goals / Non-Goals
|
|
6
|
+
|
|
7
|
+
**Goals:**
|
|
8
|
+
- Standalone `browser.py` module, zero external dependencies (stdlib only)
|
|
9
|
+
- Three usage modes: list all paths, auto-select first, validate user path
|
|
10
|
+
- ENV override via `HTMLREADER_CHROME_PATH`
|
|
11
|
+
- Cross-platform: Windows, Linux, macOS
|
|
12
|
+
|
|
13
|
+
**Non-Goals:**
|
|
14
|
+
- Browser version comparison or feature detection
|
|
15
|
+
- CDP protocol connection (deferred to browser rendering stage)
|
|
16
|
+
- Browser installation/download
|
|
17
|
+
- Firefox or other browsers (Chrome/Edge only)
|
|
18
|
+
|
|
19
|
+
## Decisions
|
|
20
|
+
|
|
21
|
+
### 1. Detection strategy: file existence + --version verification
|
|
22
|
+
|
|
23
|
+
**Decision:** On Windows, check known paths via `os.path.isfile()`. On Linux/macOS, check PATH-based commands via `[cmd, "--version"]` subprocess call (timeout 2s). Match output prefix to confirm it's actually Chrome/Edge (not another binary with the same name).
|
|
24
|
+
|
|
25
|
+
**Rationale:** File existence is fast and reliable for fixed Windows paths. `--version` verification prevents false positives on Linux where a script might be named `google-chrome` but not actually be Chrome.
|
|
26
|
+
|
|
27
|
+
**Alternative considered:** Only check file existence on all platforms. Rejected — Linux/macOS binaries on PATH need verification to avoid false matches.
|
|
28
|
+
|
|
29
|
+
### 2. Path list deduplication + mtime sorting
|
|
30
|
+
|
|
31
|
+
**Decision:** Collect all found paths into a set (dedup), resolve to absolute, sort by `os.path.getmtime()` descending (newest first).
|
|
32
|
+
|
|
33
|
+
**Rationale:** Users with multiple Chrome versions (stable/beta/dev) want the newest. Set dedup handles cases where the same binary is found via multiple detection methods.
|
|
34
|
+
|
|
35
|
+
### 3. Module structure
|
|
36
|
+
|
|
37
|
+
**Decision:** Single file `src/htmlreader/browser.py` with these public functions:
|
|
38
|
+
- `detect_browser_paths() -> list[str]` — list all found paths
|
|
39
|
+
- `get_browser_path() -> str | None` — auto-select first, or None
|
|
40
|
+
- `validate_browser_path(path: str) -> str` — validate user path, raise if invalid
|
|
41
|
+
|
|
42
|
+
**Rationale:** Three clear functions for three usage modes. The `get_browser_path()` function checks ENV first, then falls back to auto-detect.
|
|
43
|
+
|
|
44
|
+
### 4. Error handling
|
|
45
|
+
|
|
46
|
+
**Decision:** Custom `BrowserNotFoundError` exception (not `ChromeNotFoundError` — we also detect Edge).
|
|
47
|
+
|
|
48
|
+
**Rationale:** The name should reflect what we're actually looking for (browsers, not just Chrome).
|
|
49
|
+
|
|
50
|
+
### 5. ENV integration
|
|
51
|
+
|
|
52
|
+
**Decision:** `get_browser_path()` checks `HTMLREADER_CHROME_PATH` first. If set, validate it and return. If not set, fall back to auto-detect.
|
|
53
|
+
|
|
54
|
+
**Rationale:** ENV override lets users pin a specific browser without code changes. Consistent with the `HTMLREADER_*` convention from settings.py.
|
|
55
|
+
|
|
56
|
+
## Risks / Trade-offs
|
|
57
|
+
|
|
58
|
+
- **[False negatives on unusual install paths]** → Mitigated by supporting both fixed paths and PATH-based detection
|
|
59
|
+
- **[Subprocess timeout on slow systems]** → 2s timeout is generous; if it times out, that binary is skipped
|
|
60
|
+
- **[mtime doesn't always correlate with "best" browser]** → Acceptable heuristic for first version; users can override via ENV or explicit path
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## Why
|
|
2
|
+
|
|
3
|
+
The browser rendering feature requires a Chrome/Edge browser path. Users can provide an explicit path, but most will prefer automatic detection. The system needs to discover installed Chrome/Edge executables on the current platform, validate user-provided paths, and expose the detection as a standalone module that other features can import.
|
|
4
|
+
|
|
5
|
+
## What Changes
|
|
6
|
+
|
|
7
|
+
- **New module**: `src/htmlreader/browser.py` — Chrome/Edge path detection
|
|
8
|
+
- **Auto-detect**: Platform-aware search (Windows/Linux/macOS) for Chrome and Edge executables
|
|
9
|
+
- **Three modes**: list all found paths, auto-select first match, validate user-provided path
|
|
10
|
+
- **ENV support**: `HTMLREADER_CHROME_PATH` overrides auto-detection
|
|
11
|
+
- **Test file**: `tests/test_browser.py` — detection + validation tests
|
|
12
|
+
|
|
13
|
+
## Capabilities
|
|
14
|
+
|
|
15
|
+
### New Capabilities
|
|
16
|
+
- `chrome-path-detection`: Platform-aware Chrome/Edge path discovery, validation, and selection
|
|
17
|
+
|
|
18
|
+
### Modified Capabilities
|
|
19
|
+
<!-- None -->
|
|
20
|
+
|
|
21
|
+
## Impact
|
|
22
|
+
|
|
23
|
+
- New files: `src/htmlreader/browser.py`, `tests/test_browser.py`
|
|
24
|
+
- Modified: `src/htmlreader/__init__.py` (public API export)
|
|
25
|
+
- No new dependencies (stdlib only: `os`, `platform`, `subprocess`, `shutil`)
|
html_reader_llm-0.0.2/openspec/changes/chrome-path-detection/specs/chrome-path-detection/spec.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
## Purpose
|
|
2
|
+
|
|
3
|
+
Provides platform-aware Chrome/Edge browser path discovery, validation, and selection for the browser rendering feature.
|
|
4
|
+
|
|
5
|
+
## ADDED Requirements
|
|
6
|
+
|
|
7
|
+
### Requirement: Platform-aware browser detection
|
|
8
|
+
The system SHALL detect installed Chrome and Edge executables on the current platform (Windows, Linux, macOS). On Windows, it SHALL check known installation paths and use `os.path.isfile()` to verify existence. On Linux, it SHALL check PATH-based commands via `--version` output. On macOS, it SHALL check known application bundle paths.
|
|
9
|
+
|
|
10
|
+
#### Scenario: Windows Chrome found
|
|
11
|
+
- **WHEN** Chrome is installed at a known Windows path
|
|
12
|
+
- **THEN** that path appears in the detected paths list
|
|
13
|
+
|
|
14
|
+
#### Scenario: Linux Chrome found
|
|
15
|
+
- **WHEN** `google-chrome-stable` is available on PATH
|
|
16
|
+
- **THEN** its absolute path appears in the detected paths list
|
|
17
|
+
|
|
18
|
+
#### Scenario: macOS Chrome found
|
|
19
|
+
- **WHEN** Chrome is installed at `/Applications/Google Chrome.app/...`
|
|
20
|
+
- **THEN** that path appears in the detected paths list
|
|
21
|
+
|
|
22
|
+
#### Scenario: No browser found
|
|
23
|
+
- **WHEN** no Chrome or Edge installation is detected
|
|
24
|
+
- **THEN** an empty list is returned
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
### Requirement: Path list with ordering
|
|
29
|
+
The system SHALL return all detected browser paths sorted by modification time (newest first). Each path SHALL be an absolute, resolved path.
|
|
30
|
+
|
|
31
|
+
#### Scenario: Multiple browsers found
|
|
32
|
+
- **WHEN** both Chrome and Edge are installed
|
|
33
|
+
- **THEN** both paths are returned, sorted by mtime descending
|
|
34
|
+
|
|
35
|
+
#### Scenario: Paths are absolute
|
|
36
|
+
- **WHEN** a browser is found via a relative PATH command on Linux
|
|
37
|
+
- **THEN** the returned path is resolved to absolute
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### Requirement: Auto-select first path
|
|
42
|
+
The system SHALL provide a function that returns the first detected browser path (newest by mtime), or `None` if no browser is found.
|
|
43
|
+
|
|
44
|
+
#### Scenario: Auto-select with browsers present
|
|
45
|
+
- **WHEN** multiple browsers are installed
|
|
46
|
+
- **THEN** the function returns the path with the most recent modification time
|
|
47
|
+
|
|
48
|
+
#### Scenario: Auto-select with no browsers
|
|
49
|
+
- **WHEN** no browser is found
|
|
50
|
+
- **THEN** the function returns `None`
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Requirement: User path validation
|
|
55
|
+
The system SHALL validate a user-provided browser path. It SHALL check that the file exists and is a valid executable. If invalid, it SHALL raise a clear error.
|
|
56
|
+
|
|
57
|
+
#### Scenario: Valid user path
|
|
58
|
+
- **WHEN** user provides a path that exists on disk
|
|
59
|
+
- **THEN** the path is returned as-is (resolved to absolute)
|
|
60
|
+
|
|
61
|
+
#### Scenario: Invalid user path
|
|
62
|
+
- **WHEN** user provides a path that does not exist
|
|
63
|
+
- **THEN** a `BrowserNotFoundError` is raised with a descriptive message
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### Requirement: ENV override
|
|
68
|
+
The system SHALL respect `HTMLREADER_CHROME_PATH` environment variable. When set, it overrides auto-detection and is used directly (after validation).
|
|
69
|
+
|
|
70
|
+
#### Scenario: ENV path set and valid
|
|
71
|
+
- **WHEN** `HTMLREADER_CHROME_PATH=/usr/bin/google-chrome` is set and the file exists
|
|
72
|
+
- **THEN** that path is returned without scanning
|
|
73
|
+
|
|
74
|
+
#### Scenario: ENV path set but invalid
|
|
75
|
+
- **WHEN** `HTMLREADER_CHROME_PATH=/nonexistent` is set
|
|
76
|
+
- **THEN** a `BrowserNotFoundError` is raised
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### Requirement: Edge browser support
|
|
81
|
+
The system SHALL detect Microsoft Edge in addition to Google Chrome, on all supported platforms.
|
|
82
|
+
|
|
83
|
+
#### Scenario: Edge on Windows
|
|
84
|
+
- **WHEN** Edge is installed at `C:/Program Files/Microsoft/Edge/Application/msedge.exe`
|
|
85
|
+
- **THEN** that path is detected
|
|
86
|
+
|
|
87
|
+
#### Scenario: Edge on Linux
|
|
88
|
+
- **WHEN** `microsoft-edge-stable` is available on PATH
|
|
89
|
+
- **THEN** its path is detected
|
|
90
|
+
|
|
91
|
+
#### Scenario: Edge on macOS
|
|
92
|
+
- **WHEN** Edge is installed at `/Applications/Microsoft Edge.app/...`
|
|
93
|
+
- **THEN** that path is detected
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## 1. Module setup
|
|
2
|
+
|
|
3
|
+
- [ ] 1.1 Create `src/htmlreader/browser.py` with platform path constants (WIN32_PATHS, LINUX_PATHS, DARWIN_PATHS)
|
|
4
|
+
- [ ] 1.2 Define `BrowserNotFoundError` exception
|
|
5
|
+
|
|
6
|
+
## 2. Detection functions
|
|
7
|
+
|
|
8
|
+
- [ ] 2.1 Implement `_iter_browser_paths()` — platform-aware iterator yielding found paths
|
|
9
|
+
- [ ] 2.2 Implement `detect_browser_paths() -> list[str]` — dedup, resolve to absolute, sort by mtime descending
|
|
10
|
+
- [ ] 2.3 Implement `get_browser_path() -> str | None` — check ENV first, then auto-select first detected
|
|
11
|
+
- [ ] 2.4 Implement `validate_browser_path(path: str) -> str` — validate user path, raise BrowserNotFoundError if invalid
|
|
12
|
+
|
|
13
|
+
## 3. Public API
|
|
14
|
+
|
|
15
|
+
- [ ] 3.1 Export `BrowserNotFoundError`, `detect_browser_paths`, `get_browser_path`, `validate_browser_path` from `src/htmlreader/__init__.py`
|
|
16
|
+
|
|
17
|
+
## 4. Tests
|
|
18
|
+
|
|
19
|
+
- [ ] 4.1 Test detect_browser_paths returns list on current platform
|
|
20
|
+
- [ ] 4.2 Test get_browser_path returns path or None (no crash)
|
|
21
|
+
- [ ] 4.3 Test validate_browser_path with valid path
|
|
22
|
+
- [ ] 4.4 Test validate_browser_path with invalid path raises BrowserNotFoundError
|
|
23
|
+
- [ ] 4.5 Test ENV override: HTMLREADER_CHROME_PATH set to valid path
|
|
24
|
+
- [ ] 4.6 Test ENV override: HTMLREADER_CHROME_PATH set to invalid path raises error
|
|
25
|
+
- [ ] 4.7 Test platform constants are non-empty for each OS
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
## Context
|
|
2
|
+
|
|
3
|
+
The project has HTML simplification (`simplify.py`), render detection (`render_detect.py`), and browser path detection (`browser.py`). The next layer is LLM intelligence: send simplified HTML to an LLM, get back page classification and CSS selector rules.
|
|
4
|
+
|
|
5
|
+
## Goals / Non-Goals
|
|
6
|
+
|
|
7
|
+
**Goals:**
|
|
8
|
+
- OpenAI-compatible API via httpx2
|
|
9
|
+
- trafilatura metadata as LLM prompt context
|
|
10
|
+
- Page classification (list/detail)
|
|
11
|
+
- CSS selector rule generation for both page types
|
|
12
|
+
- Prompt budget management (<50% of context window)
|
|
13
|
+
- CLI `analyze` command
|
|
14
|
+
|
|
15
|
+
**Non-Goals:**
|
|
16
|
+
- Streaming responses
|
|
17
|
+
- Multi-turn conversation
|
|
18
|
+
- Rule execution (just generation)
|
|
19
|
+
- Rule storage/persistence
|
|
20
|
+
- Multiple LLM provider fallback
|
|
21
|
+
|
|
22
|
+
## Decisions
|
|
23
|
+
|
|
24
|
+
### 1. HTTP client for LLM: httpx2
|
|
25
|
+
|
|
26
|
+
**Decision:** Use httpx2 for LLM API communication.
|
|
27
|
+
|
|
28
|
+
**Rationale:** Already a project dependency. Supports async (future), proper timeout, and JSON handling.
|
|
29
|
+
|
|
30
|
+
### 2. API format: OpenAI chat completions
|
|
31
|
+
|
|
32
|
+
**Decision:** Use `/v1/chat/completions` with `response_format: { type: "json_object" }`.
|
|
33
|
+
|
|
34
|
+
**Rationale:** Widely compatible with most LLM providers. JSON mode ensures structured output.
|
|
35
|
+
|
|
36
|
+
### 3. Prompt structure
|
|
37
|
+
|
|
38
|
+
**Decision:** Three-part prompt:
|
|
39
|
+
1. System prompt: role definition + output format instructions
|
|
40
|
+
2. trafilatura metadata: extracted fields as context
|
|
41
|
+
3. Simplified HTML: the page content
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
system = """You are an HTML analysis expert. Analyze the given HTML and return JSON with:
|
|
45
|
+
- page_type: "list" or "detail"
|
|
46
|
+
- metadata: {title, author, date, ...} from trafilatura
|
|
47
|
+
- rules: CSS selector extraction rules
|
|
48
|
+
- list: [{name, is_main, selectors: {title, link, pubtime}}]
|
|
49
|
+
- detail: {title, author, date, description, image, categories, tags, content}
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
user = f"""Metadata (from trafilatura):
|
|
53
|
+
{metadata_json}
|
|
54
|
+
|
|
55
|
+
HTML:
|
|
56
|
+
{simplified_html}
|
|
57
|
+
"""
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 4. Prompt budget: 50% of max_tokens
|
|
61
|
+
|
|
62
|
+
**Decision:** Estimate token count as `len(text) / 3` (rough CJK/English average). If HTML exceeds 50% of max_tokens, truncate from the middle, keeping head and tail.
|
|
63
|
+
|
|
64
|
+
**Rationale:** Middle truncation preserves page structure (header/nav at top, footer at bottom) while cutting the bulk content.
|
|
65
|
+
|
|
66
|
+
### 5. trafilatura integration: extract before simplify
|
|
67
|
+
|
|
68
|
+
**Decision:** Run trafilatura on the **raw** HTML (before simplification), because trafilatura's parsers expect full HTML with metadata tags. The simplified HTML goes to LLM.
|
|
69
|
+
|
|
70
|
+
**Order:** raw HTML → trafilatura (metadata) + simplify (HTML) → LLM prompt
|
|
71
|
+
|
|
72
|
+
### 6. Result structure: TypedDict
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
class ListRule(TypedDict):
|
|
76
|
+
name: str # "main", "sidebar", etc.
|
|
77
|
+
is_main: bool
|
|
78
|
+
selectors: dict[str, str] # title, link, pubtime CSS selectors
|
|
79
|
+
|
|
80
|
+
class DetailRules(TypedDict):
|
|
81
|
+
# Each field is Optional CSS selector string
|
|
82
|
+
title: str | None
|
|
83
|
+
author: str | None
|
|
84
|
+
date: str | None
|
|
85
|
+
description: str | None
|
|
86
|
+
image: str | None
|
|
87
|
+
categories: str | None
|
|
88
|
+
tags: str | None
|
|
89
|
+
content: str | None
|
|
90
|
+
|
|
91
|
+
class AnalysisResult(TypedDict):
|
|
92
|
+
page_type: str # "list" or "detail"
|
|
93
|
+
metadata: dict # trafilatura metadata
|
|
94
|
+
list_rules: list[ListRule] | None
|
|
95
|
+
detail_rules: DetailRules | None
|
|
96
|
+
raw_llm_response: str
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 7. Error handling
|
|
100
|
+
|
|
101
|
+
**Decision:** If LLM returns invalid JSON, log warning and return result with `raw_llm_response` filled but rules as None. If LLM request fails, raise with clear error message.
|
|
102
|
+
|
|
103
|
+
## Risks / Trade-offs
|
|
104
|
+
|
|
105
|
+
- **[Token estimation inaccuracy]** → `len/3` is rough; may over-truncate or under-truncate. Mitigated by logging actual token usage from API response.
|
|
106
|
+
- **[trafilatura metadata quality]** → Some sites have poor metadata. LLM can still work without it, just less accurate.
|
|
107
|
+
- **[LLM hallucination in selectors]** → Generated selectors may not exist in the HTML. Downstream validation needed (not in this change).
|
|
108
|
+
- **[Cost]** → Each analysis is one LLM call. Budget management prevents runaway costs.
|