港交所披露易下载
使用 Playwright 浏览器自动化,从港交所披露易(DION)下载权益披露PDF。
前置条件
pip install playwright beautifulsoup4 pypdf
python -m playwright install chromium
Windows 沙箱环境建议用 managed Python venv:
C:\Users\cxq\.workbuddy\binaries\python\envs\default\Scripts\python.exe
执行脚本
核心脚本位于 scripts/download_disclosure.py。
配置参数
| 参数 | 说明 | 默认值 |
|------|------|--------|
| --stock | 港股代码 | "09992" (泡泡玛特) |
| --start / --end | 披露日期范围 | "2025-01-01" / "2026-06-25" |
| --keyword | 股东名称关键词,逗号分隔,任一命中即纳入 | "Duan Yong Ping,H&H" |
| --type | 披露编号类型过滤,逗号分隔(如 IS,CS);默认全部 | 空 |
| --dir | PDF保存目录 | ~/Desktop/泡泡玛特 |
| --headless / --visible | 无头模式 / 显示浏览器 | 启用无头 |
运行
# 推荐:命令行方式(段永平+H&H 关于泡泡玛特的全部披露)
python scripts/download_disclosure.py \
--stock 09992 \
--keyword "Duan Yong Ping,H&H" \
--start 2025-01-01 \
--end 2026-06-25 \
--dir ~/Desktop/泡泡玛特
# 只下载个人大股东通知(IS)
python scripts/download_disclosure.py --stock 09992 --keyword "Duan Yong Ping,H&H" --type IS
# 显示浏览器窗口调试
python scripts/download_disclosure.py --visible
工作流程(v2)
[1] 启动 Chromium (Playwright)
[2] 访问港交所披露易搜索页 (NSSrchCorp.aspx),填表提交搜索
[3] 搜索结果页列出4个报告类别入口:
- 大股東完整名單(仅指定日期在册大股东最新一次披露)
- 大股東綜合名單
- 由大股東所提交的披露權益通知一覽表(★ 全部历史披露,本脚本用这个)
- 董事完整名單
[4] 点击进入"由大股東所提交的披露權益通知一覽表"
[5] 解析列表页,按 KEYWORD(任一命中) + TYPE 过滤,自动翻页
[6] 逐条:context.new_page() + goto(NSFormXPrint.aspx) → page.pdf() 生成干净PDF
关键实现细节(v2 核心修复)
1. 报告类别入口选择(最关键)
港交所披露易搜索结果页有4个报告类别入口。**必须选"由大股東所提交的披露權益通知一覽表"**才能拿到日期范围内的全部历史披露通知。
- ❌ "大股東完整名單":只显示指定日期当天的在册大股东最新一次披露,会漏掉历史建仓过程中的多次披露。v1 误用此入口,导致段永平8份披露只拿到2份。
- ✅ "由大股東所提交的披露權益通知一覽表":列出日期范围内大股东提交的每一份披露通知。
2. PDF 生成方式:context.new_page() + goto
v1 用 source_page.evaluate('window.open(url, "_blank")') + expect_page 打开详情/Print页。但在 headless Chromium 下,无真实用户手势的 window.open 会被静默拦截,新标签页始终不出现,expect_page 超时。
v2 改用:
ppage = context.new_page()
ppage.goto(print_url, wait_until="domcontentloaded")
time.sleep(4) # 等JS渲染表格
ppage.pdf(path=..., format="A4", print_background=True, margin={...})
ppage.close()
Print 页面是纯展示页,URL 自带 fn/sid/corpn 参数,goto 不依赖 ASP.NET session,稳定可靠。
3. Print URL 通用构造
披露详情页有 NSForm1.aspx(个人 IS)、NSForm2.aspx(法团 CS)等多种。Print 页面 URL 构造用通用正则:
re.sub(r'NSForm(\d+)\.aspx', r'NSForm\1Print.aspx', detail_url)
- NSForm1.aspx → NSForm1Print.aspx(个人大股东通知)
- NSForm2.aspx → NSForm2Print.aspx(法团大股东通知)
v1 只硬编码替换 NSForm1,导致 CS 法团披露的 Print URL 替换失效,回退到详情页 page.pdf(),生成带导航栏遮挡的版本。
4. 多关键词匹配
段永平通过 H&H International Investment, LLC(法团)和个人双名义披露,同一权益事件会同时生成 IS + CS 两份通知。--keyword 支持逗号分隔,任一命中即纳入:
--keyword "Duan Yong Ping,H&H"
5. PDF 完整性校验
生成后用 pypdf 检查首页文本是否含导航关键词("上市公司文件"/"股權披露"/"披露權益")。若含则判定为带遮挡的详情页版本,触发重试或兜底。
6. Windows 控制台编码处理
PowerShell stdout 默认 GBK,无法输出 ✓✗★ 等 Unicode 符号会触发 UnicodeEncodeError 导致脚本崩溃。脚本顶部强制:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
同时关键日志写入 scripts/_run.log(UTF-8),便于乱码时排查。
记录类型说明
披露编号前缀:
IS= Individual Substantial Shareholder(个人大股东通知,NSForm1)CS= Corporate Substantial Shareholder(法团大股东通知,NSForm2)DA= Director Acquisition(董事权益通知)
脚本默认全部下载,可用 --type IS,CS 过滤。
常见问题
| 问题 | 原因 | 解决 |
|------|------|------|
| 只下载到少量记录 | 误入"大股東完整名單"(仅最新) | 改入"由大股東所提交的披露權益通知一覽表"(v2已修复) |
| PDF 含导航栏遮挡 | Print URL 替换只处理 NSForm1,CS 法团用 NSForm2 失效 | 用通用正则 NSForm(\d+)\.aspx 替换(v2已修复) |
| PDF 生成失败/新标签不打开 | headless 下 window.open 被弹窗拦截 | 改用 context.new_page() + goto(v2已修复) |
| UnicodeEncodeError 崩溃 | PowerShell stdout 是 GBK,无法输出 ✓ 等符号 | 脚本顶部 sys.stdout.reconfigure(utf-8)(v2已修复) |
| 文件生成成功但脚本判失败 | MIN_PDF_SIZE(字节) 与 size_kb(KB) 单位混淆 | 用 fsize(字节) 比较,勿用 size_kb(v2已修复) |
| 搜索按钮点击超时 | click() 默认等待导航 | click(timeout=10000) + sleep(8),不依赖导航等待 |
| Print页面加载超时 | 用了 networkidle | 改用 domcontentloaded + sleep(4) |
| 详情页 .pdf 链接全是指南 | 港交所不提供独立PDF下载,内容在HTML表格 | 用 Print 页面 page.pdf() 生成 |
踩坑记录(避坑指南)
坑1:报告类别入口选错(v1→v2 最重大修复)
- 现象:段永平关于泡泡玛特应有8份披露,v1只下载到2份
- 根因:v1 点击"大股東完整名單",该入口只返回指定日期当天在册大股东的最新一次披露(5条:王宁/GWF/UBS/段永平IS/H&H的CS),把历史建仓过程的多次披露折叠了
- 正确做法:点击"由大股東所提交的披露權益通知一覽表",该入口列出日期范围内的每一份披露通知
- 验证:v2 在该入口下找到8份(3次事件×2~4份通知),与用户预期一致
坑2:headless 下 window.open 被拦截
- 现象:
source_page.evaluate('window.open(url, "_blank")')执行不报错,但新标签页始终不出现,expect_page 超时 - 根因:headless Chromium 对无用户手势的 window.open 静默拦截。v1 之所以成功,是因为点击"大股东完整名单"链接是手势动作,后续在那个上下文里 window.open 偶然可行;v2 报告列表页是"当前页跳转"返回,后续 evaluate 的 window.open 无手势上下文
- 解决:改用
context.new_page() + page.goto(print_url),完全不依赖 window.open
坑3:Print URL 只替换 NSForm1
- 现象:CS 法团披露(NSForm2.aspx)的 Print URL 替换失效,回退到详情页 page.pdf(),生成带导航栏遮挡版本
- 根因:v1 硬编码
url.replace("NSForm1.aspx", "NSForm1Print.aspx"),对 NSForm2.aspx 无效 - 解决:
re.sub(r'NSForm(\d+)\.aspx', r'NSForm\1Print.aspx', url)通用替换
坑4:Windows PowerShell GBK 编码崩溃
- 现象:脚本输出
✓时触发UnicodeEncodeError: 'gbk' codec can't encode character '\u2713' - 根因:PowerShell stdout 默认 GBK 编码,无法输出 Unicode 符号;且异常处理分支里打印符号会导致二次崩溃,掩盖真实错误
- 解决:脚本顶部
sys.stdout.reconfigure(encoding="utf-8");关键日志同时写scripts/_run.log(UTF-8)
坑5:PDF 大小单位混淆
- 现象:154KB 的干净 PDF 被误判为失败,错误触发兜底(详情页,有遮挡)覆盖了干净文件
- 根因:
MIN_PDF_SIZE=5000(字节)与size_kb(KB)直接比较,相当于要求 5000KB=5MB - 解决:用
fsize(字节)与MIN_PDF_SIZE(字节)比较:return fsize > MIN_PDF_SIZE, ...
坑6:详情页 .pdf 链接全是导航指南
- 现象:NSForm1.aspx 详情页中
href*=".pdf"匹配到的都是PredefinedSearchGuide_c.pdf等操作指南,非披露文件 - 根因:港交所披露内容直接渲染在 HTML 表格,不提供独立 PDF 下载
- 结论:绝对不要通过
.pdf后缀匹配披露文件,必须用 Print 页面 page.pdf() 生成
Scan to join WeChat group