archive: 紫鸟/HubStudio 文档快照(3889 篇 Markdown)
来源:紫鸟帮助中心/博客/主站资讯 + HubStudio API 文档/帮助中心 + 三个官方 skill 仓库。 只入库 Markdown 与元数据;原始 HTML 与 tarball 留在本机。
This commit is contained in:
commit
322b7737eb
3976 files changed
+695714
No files matched your search
+206
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
"""package.py — 把抓取结果整理成可检索的归档:README.md + INDEX.md + tar.gz + git 提交。
|
||||
|
||||
产物
|
||||
- README.md 归档总览:来源、统计、目录说明、版权与缺口
|
||||
- INDEX.md 每组一篇索引(标题 + 本地路径 + 来源 URL)
|
||||
- ARCHIVE.tar.gz 本地全量快照(含 raw/ 原始响应)
|
||||
- git 仓库 只提交 md/ + repos/ + manifest + 索引(raw/ 不入库,避免几百 MB 的 HTML)
|
||||
|
||||
用法: python3 package.py [--push <gitea-remote-url>]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import datetime
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
GROUPS = {
|
||||
'zn-help': '紫鸟帮助中心(help/docs + help-v5/docs,共 468 页)',
|
||||
'zn-blog': '紫鸟跨境知识库(WordPress 博客,255 篇)',
|
||||
'zn-news': '紫鸟主站资讯与博客(/news + /blog,含 SEO 文章约 3000 篇)',
|
||||
'hs-api': 'HubStudio API 文档(api-docs,Markdown 直出,160 页)',
|
||||
'hs-support': 'HubStudio 帮助中心(support 站,首页可见的文章)',
|
||||
}
|
||||
SOURCES = [
|
||||
('紫鸟 sitemap', 'https://www.ziniao.com/sitemap.xml'),
|
||||
('紫鸟博客 sitemap', 'https://blog.ziniao.com/sitemaps.xml'),
|
||||
('紫鸟帮助中心', 'https://www.ziniao.com/help/'),
|
||||
('紫鸟开放平台', 'https://open.ziniao.com/'),
|
||||
('紫鸟官方 Skill 仓库', 'https://github.com/ziniao-open/skills'),
|
||||
('紫鸟官方 WebDriver/Skill Hub', 'https://github.com/ziniao-open/skill-hub'),
|
||||
('HubStudio API 文档 sitemap', 'https://api-docs.hubstudio.cn/sitemap.xml'),
|
||||
('HubStudio 帮助中心', 'https://support-orig.hubstudio.cn/'),
|
||||
('HubStudio 官方 Skill 仓库', 'https://github.com/hubstudio-Max/hubstudio-skill'),
|
||||
]
|
||||
|
||||
|
||||
def group_stats() -> dict:
|
||||
stats = {}
|
||||
manifest = ROOT / 'manifest.tsv'
|
||||
rows = []
|
||||
if manifest.exists():
|
||||
for ln in manifest.read_text(encoding='utf-8').splitlines():
|
||||
parts = ln.split('\t')
|
||||
if len(parts) >= 4:
|
||||
rows.append(parts)
|
||||
for g in GROUPS:
|
||||
mds = list((ROOT / 'md' / g).rglob('*.md'))
|
||||
raws = list((ROOT / 'raw' / g).rglob('*')) if (ROOT / 'raw' / g).exists() else []
|
||||
raws = [f for f in raws if f.is_file()]
|
||||
ok = sum(1 for r in rows if r[0] == g and r[2] == '200')
|
||||
fail = sum(1 for r in rows if r[0] == g and r[2] != '200')
|
||||
stats[g] = {
|
||||
'md': len(mds), 'raw': len(raws), 'list': len(_urls(g)),
|
||||
'ok': ok, 'fail': fail,
|
||||
'md_bytes': sum(f.stat().st_size for f in mds),
|
||||
'raw_bytes': sum(f.stat().st_size for f in raws),
|
||||
}
|
||||
return stats
|
||||
|
||||
|
||||
def _urls(group: str) -> list[str]:
|
||||
f = ROOT / f'urls-{group}.txt'
|
||||
return [x for x in f.read_text(encoding='utf-8').splitlines() if x.strip()] if f.exists() else []
|
||||
|
||||
|
||||
def title_of(md: Path) -> str:
|
||||
try:
|
||||
for ln in md.read_text(encoding='utf-8', errors='replace').splitlines()[:40]:
|
||||
if ln.startswith('#'):
|
||||
return ln.lstrip('# ').strip()
|
||||
for ln in md.read_text(encoding='utf-8', errors='replace').splitlines()[:6]:
|
||||
if 'source:' in ln:
|
||||
return md.stem
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return md.stem
|
||||
|
||||
|
||||
def write_readme(stats: dict) -> None:
|
||||
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
|
||||
total_md = sum(s['md'] for s in stats.values())
|
||||
total_raw = sum(s['raw'] for s in stats.values())
|
||||
total_mb = sum(s['md_bytes'] + s['raw_bytes'] for s in stats.values()) / 2**20
|
||||
lines = [
|
||||
'# 紫鸟 / HubStudio 文档归档', '',
|
||||
f'抓取时间:**{now}** 抓取工具:`crawl.py`(本目录,源码随包提交)', '',
|
||||
'## 目录结构', '',
|
||||
'| 路径 | 说明 |', '|---|---|',
|
||||
'| `md/<group>/…` | 提取后的 Markdown(可直接阅读/检索,gitea 里有) |',
|
||||
'| `raw/<group>/…` | 原始响应(HTML 或 .md),**仅本地保留**,不入 git |',
|
||||
'| `repos/` | 三个官方 Skill 仓库的文件级快照(见下) |',
|
||||
'| `manifest.tsv` | 每页记录:group / url / http / bytes / sha256前16 / title |',
|
||||
'| `INDEX.md` | 分组索引:标题 + 本地路径 + 来源 URL |',
|
||||
'| `ARCHIVE.tar.gz` | 本地全量快照(含 raw/) |', '',
|
||||
'## 统计', '',
|
||||
'| group | 说明 | 计划 | 成功 | 失败 | md | raw | 体积 |',
|
||||
'|---|---|---:|---:|---:|---:|---:|---:|',
|
||||
]
|
||||
for g, desc in GROUPS.items():
|
||||
s = stats[g]
|
||||
lines.append(f"| `{g}` | {desc} | {s['list']} | {s['ok']} | {s['fail']} | {s['md']} | {s['raw']} | "
|
||||
f"{s['md_bytes']/2**20:.1f}M + {s['raw_bytes']/2**20:.1f}M |")
|
||||
lines += ['', f'合计:**{total_md} 篇 Markdown**、{total_raw} 份原始响应、约 **{total_mb:.0f} MB**。', '',
|
||||
'## 来源', '']
|
||||
lines += [f'- {n}:<{u}>' for n, u in SOURCES]
|
||||
lines += ['',
|
||||
'## 官方 Skill 仓库(repos/)', '',
|
||||
'| 仓库 | 内容 |', '|---|---|',
|
||||
'| `repos/skills` | 紫鸟官方 skill 包(员工/角色/部门/账号/设备/店铺控制等),CC BY 4.0 |',
|
||||
'| `repos/skill-hub` | 紫鸟 WebDriver 文档与示例(applyAuth/getBrowserList/startBrowser…) |',
|
||||
'| `repos/hubstudio-skill` | HubStudio 官方 skill(含 ADB 连接、OpenClaw 教程、commands 清单) |', '',
|
||||
'## 已知缺口(重要)', '',
|
||||
'- **HubStudio 帮助中心(support 站)是 Baklib 托管的 SPA**:文章页无服务端内链、站点无 sitemap,',
|
||||
' 只能取到首页可见的文章(约 20 篇)。要抓全需登录态或渲染 JS,本归档不含。',
|
||||
'- 紫鸟主站 `/blog/*` 与 WordPress 博客是两套内容,均按原样归档,可能有重叠主题。',
|
||||
'- 页面内的图片/附件未下载(只存正文文本);需要时按 `raw/` 里的原始 URL 再取。',
|
||||
'- 部分页面为 404/登录后可见,manifest 里 http 非 200 的行即失败项。', '',
|
||||
'## 版权与使用', '',
|
||||
'- 文档内容版权归各自厂商(紫鸟:福建紫讯信息科技;HubStudio:其运营方)所有,**本归档仅供内部查阅参考**,',
|
||||
' 不改变原授权;对外分发前请先确认授权。',
|
||||
'- `repos/skills`、`repos/skill-hub`、`repos/hubstudio-skill` 为官方公开仓库快照,'
|
||||
'紫鸟 skill 包声明为 **CC BY 4.0**,使用时请保留署名。', '',
|
||||
'## 复现', '', '```bash', 'python3 crawl.py --list # 看各来源规模',
|
||||
'python3 crawl.py # 全量抓取(幂等,可续抓)',
|
||||
'python3 mirror_repos.py # 快照官方仓库', 'python3 package.py --push <remote>', '```', '']
|
||||
(ROOT / 'README.md').write_text('\n'.join(lines), encoding='utf-8')
|
||||
|
||||
|
||||
def write_index() -> None:
|
||||
out = ['# 索引', '']
|
||||
for g, desc in GROUPS.items():
|
||||
mds = sorted((ROOT / 'md' / g).rglob('*.md'))
|
||||
out += [f'## {g} — {desc}', '', f'共 {len(mds)} 篇。', '',
|
||||
'| 标题 | 本地路径 |', '|---|---|']
|
||||
for md in mds:
|
||||
rel = md.relative_to(ROOT)
|
||||
t = title_of(md).replace('|', '/')[:90]
|
||||
out.append(f'| {t or md.stem} | `{rel}` |')
|
||||
out.append('')
|
||||
(ROOT / 'INDEX.md').write_text('\n'.join(out), encoding='utf-8')
|
||||
|
||||
|
||||
def make_tar() -> None:
|
||||
tar = ROOT / 'ARCHIVE.tar.gz'
|
||||
with tarfile.open(tar, 'w:gz') as tf:
|
||||
for sub in ('md', 'raw', 'repos'):
|
||||
p = ROOT / sub
|
||||
if p.exists():
|
||||
tf.add(p, arcname=sub)
|
||||
for f in ('manifest.tsv', 'INDEX.md', 'README.md', 'crawl.py', 'mirror_repos.py', 'package.py'):
|
||||
if (ROOT / f).exists():
|
||||
tf.add(ROOT / f, arcname=f)
|
||||
print(f' ARCHIVE.tar.gz: {tar.stat().st_size / 2**20:.1f} MB')
|
||||
|
||||
|
||||
def git_commit_push(remote: str | None, stats: dict) -> None:
|
||||
"""gitea 侧只提交 md/ + repos/ + 元数据(raw/ 不入库)。"""
|
||||
(ROOT / '.gitignore').write_text('raw/\nARCHIVE.tar.gz\ncrawl.log\n*.bak\n__pycache__/\n')
|
||||
cmds = [['git', 'init', '-q'], ['git', 'add', '-A'],
|
||||
['git', '-c', 'user.name=admin9webs', '-c', 'user.email=admin9webs@9webs.site',
|
||||
'commit', '-q', '-m',
|
||||
f'archive: 紫鸟/HubStudio 文档快照({sum(s["md"] for s in stats.values())} 篇 Markdown)\n\n'
|
||||
'来源:紫鸟帮助中心/博客/主站资讯 + HubStudio API 文档/帮助中心 + 三个官方 skill 仓库。\n'
|
||||
'只入库 Markdown 与元数据;原始 HTML 与 tarball 留在本机。']]
|
||||
for c in cmds:
|
||||
r = subprocess.run(c, cwd=ROOT, capture_output=True, text=True)
|
||||
if r.returncode != 0 and 'nothing to commit' not in (r.stdout + r.stderr):
|
||||
print(' git:', ' '.join(c[1:3]), '->', (r.stdout + r.stderr).strip()[:200])
|
||||
if remote:
|
||||
subprocess.run(['git', 'remote', 'remove', 'origin'], cwd=ROOT, capture_output=True)
|
||||
subprocess.run(['git', 'remote', 'add', 'origin', remote], cwd=ROOT, check=False)
|
||||
r = subprocess.run(['git', 'push', '-u', 'origin', 'HEAD:main'], cwd=ROOT,
|
||||
capture_output=True, text=True)
|
||||
out = re.sub(r'://[^@\s]+@', '://***@', (r.stdout + r.stderr).strip())
|
||||
print(' push:', out[-200:] if r.returncode else 'OK')
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--push', default=None, help='gitea remote(含凭据),推送归档')
|
||||
ap.add_argument('--no-tar', action='store_true')
|
||||
args = ap.parse_args()
|
||||
|
||||
stats = group_stats()
|
||||
write_readme(stats)
|
||||
write_index()
|
||||
print(' README.md / INDEX.md 已生成')
|
||||
for g, s in stats.items():
|
||||
print(f" {g:11s} md={s['md']:4d} ok={s['ok']:4d} fail={s['fail']:3d} "
|
||||
f"({s['md_bytes']/2**20:.1f}M md)")
|
||||
if not args.no_tar:
|
||||
make_tar()
|
||||
git_commit_push(args.push, stats)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user