化腳本實(shí)踐)
簡(jiǎn)介面向開發(fā)者與科研人員的GitHub資源批量獲取工具可針對(duì)關(guān)鍵詞搜索并一鍵下載指定起始頁到結(jié)束頁的倉庫自動(dòng)過濾涉政等無關(guān)內(nèi)容大幅提升批量收集效率。工具調(diào)用官方API運(yùn)行安全穩(wěn)定適合需要系統(tǒng)性整理開源代碼、數(shù)據(jù)集或主題資源的中高頻GitHub用戶。壓縮包內(nèi)共193個(gè)文件以程序運(yùn)行所需的dll組件為主含3個(gè)exe主程序、少量json配置文件與ini參數(shù)設(shè)置整體約30.84MB結(jié)構(gòu)偏向軟件分發(fā)形態(tài)。目前已有199人學(xué)習(xí)下載。借助該工具用戶無需逐頁手動(dòng)打開倉庫即可按關(guān)鍵詞與頁碼范圍自動(dòng)抓取資源并獲取官方API解析、過濾邏輯與批量調(diào)度等實(shí)現(xiàn)思路便于二次開發(fā)或嵌入自己的工作流。1. 批量下載 GitHub 倉庫先從“根據(jù)搜索條件生成下載清單”開始如果你要維護(hù)開源組件清單或者做代碼審計(jì)會(huì)發(fā)現(xiàn)一個(gè)高頻重復(fù)的操作把符合某些特征的 GitHub 倉庫拉到本地。特征可能是“語言是 Python、星標(biāo)超過 100、最近還有提交”也可能是“名字里帶某個(gè)特定關(guān)鍵詞”。手工做要復(fù)制每一個(gè)倉庫地址再逐條執(zhí)行下載倉庫一旦超過十個(gè)就會(huì)非常耗費(fèi)時(shí)間。GitHub 倉庫批量下載工具的本質(zhì)是合并這兩步接收一個(gè)查詢表達(dá)式用 GitHub Search API 拿到候選倉庫列表再按預(yù)設(shè)方式把代碼落盤。工具本身不復(fù)雜但用熟后能節(jié)省大量時(shí)間也讓“搜索條件 下載產(chǎn)物”變得可復(fù)現(xiàn)適合需要定期下載開源依賴、做組件分析或離線歸檔的研發(fā)和運(yùn)維人員。下面給出可直接復(fù)刻成 Python 命令行腳本的完整實(shí)現(xiàn)思路。2. 數(shù)據(jù)源選型用 GitHub Search API 替代網(wǎng)頁抓取2.1 為什么 Search API 是這個(gè)工具的默認(rèn)通道有人會(huì)把“根據(jù)搜索下載”做成直接抓 github.com 搜索頁的方案。這種方案不是不能用但它有一個(gè)長(zhǎng)期的維護(hù)成本搜索結(jié)果頁的 DOM 結(jié)構(gòu)、加載方式、是否要求登錄都可能隨前端改版變化解析代碼需要定期返工而且抓取結(jié)果缺少default_branch、archived這類結(jié)構(gòu)化字段后續(xù)還要為每個(gè)倉庫單獨(dú)再請(qǐng)求一次詳情接口。GitHub 提供的 Search API 恰好把這些問題處理干凈GET https://api.github.com/search/repositories返回標(biāo)準(zhǔn) JSON字段包含full_name、owner、default_branch、stargazers_count、archived、pushed_at等。正是這些字段讓批量下載工具可以在“下載”這個(gè)動(dòng)作之前做篩選比如跳過 archived 倉庫、只下載指定 owner 的項(xiàng)目。所以把 Search API 當(dāng)作默認(rèn)數(shù)據(jù)源是合理的網(wǎng)頁抓取只作為特殊場(chǎng)景的補(bǔ)充。不帶 token 時(shí)這個(gè)接口的限速為 10 次/分鐘帶 token 后為 30 次/分鐘。批量下載規(guī)模推到幾百個(gè)倉庫時(shí)搜索階段不會(huì)產(chǎn)生太大壓力真正的限速壓力出現(xiàn)在下載階段后面會(huì)專門說配置參數(shù)。2.2 q 參數(shù)入門把中文搜索意圖翻譯成可解析表達(dá)式q 參數(shù)是 Search API 的入口大多數(shù)下載需求都可以用空格拼接的片段來表達(dá)。常用條件如下表搜索片段用途in:name,description關(guān)鍵詞同時(shí)匹配倉庫名和描述language:python只返回主語言為 Python 的倉庫stars:100星標(biāo)數(shù)大于 100pushed:2024-06-01最近提交晚于指定日期過濾死倉庫archived:false排除歸檔倉庫size:1000倉庫體積大于 1 MB避免空殼項(xiàng)目例如“查找名字里帶 docker、語言是 Go、最近有提交”的完整 q 為docker in:name language:go pushed:2023-01-01 archived:false。這個(gè) q 不能直接拼到 URL 里因?yàn)楹涂崭駮?huì)被服務(wù)器解析錯(cuò)。為了快速驗(yàn)證先用 curl 加 jq 看一次返回具體命令curl -s -H Accept: application/vnd.githubjson \ https://api.github.com/search/repositories?qdockerin:namelanguage:gopushed:%3E2023-01-01per_page3 \ | jq {total: .total_count, first: [.items[] | {full_name, default_branch}]}這里把空格換成把編碼成%3E。per_page3用來壓縮輸出正常使用會(huì)設(shè)為 100。jq 只提取total_count和兩個(gè)關(guān)鍵字段就能看出查詢是否命中目標(biāo)范圍。如果 total 是 0優(yōu)先檢查pushed:后面的日期格式這個(gè)參數(shù)寫錯(cuò)最常見。2.3 分頁要從總數(shù)推導(dǎo)而不是固定循環(huán) 10 次Search API 的per_page上限是 100默認(rèn)是 30。批量下載為了減少請(qǐng)求次數(shù)通常直接傳per_page100。翻頁邏輯的參數(shù)來自響應(yīng)里的total_counttotal_count data[total_count] page_limit min(10, (total_count per_page - 1) // per_page) page_limit min(page_limit, 10) # 搜索結(jié)果最多取前 1000 條(total_count per_page - 1) // per_page是向上取整的標(biāo)準(zhǔn)寫法避免總數(shù)為 101 時(shí)只翻一頁漏掉一個(gè)倉庫。搜索接口限制最多返回前 1000 條結(jié)果所以這里用min(..., 10)封頂。當(dāng)total_count明顯超過 1000單獨(dú)做一次全量下載意義不大正確的做法是收緊 q 參數(shù)把時(shí)間范圍拆成幾段分別查詢最后合并結(jié)果。這里有一個(gè)容易踩的坑如果查詢里帶了sortstars然后用page翻頁結(jié)果順序會(huì)保持穩(wěn)定但per_page改變后總頁數(shù)也要同步重算否則多出來的頁會(huì)拿到空items。2.4 把搜索階段和下載階段解耦我習(xí)慣把 Search API 的原文響應(yīng)原樣緩存在本地例如search_cache/page_1.json。工具搜索階段只把每個(gè)請(qǐng)求的 JSON 寫入磁盤再?gòu)拇疟P讀取items生成下載任務(wù)。好處有兩個(gè)其一批量下載執(zhí)行時(shí)間通常遠(yuǎn)長(zhǎng)于搜索階段一旦中途斷網(wǎng)或機(jī)器掉電重啟后可以直接復(fù)用緩存不消耗請(qǐng)求次數(shù)其二搜索和下載是兩類完全不同的錯(cuò)誤分開之后可以先確認(rèn)搜索條件有沒有問題再判斷下載邏輯。緩存目錄按查詢參數(shù)命名換查詢條件后自動(dòng)區(qū)分文件即可。3. 實(shí)現(xiàn)核心下載器zip 與 git clone 雙模式切換3.1 為什么第一版優(yōu)先走 codeload 的 zip 下載“下載”要落盤為完整倉庫常見兩條路拉 zip 包或者用 git clone。批量下載工具的第一版建議盡量用 zip。理由有三條第一zip 下載對(duì)本地 Git 環(huán)境沒有依賴命令里只需要 HTTP 請(qǐng)求第二下載過程不會(huì)創(chuàng)建.git目錄不會(huì)因?yàn)楸镜匾汛嬖谕募A而報(bào) already exists第三GitHub 為公開倉庫提供了https://codeload.github.com/{owner}/{repo}/zip/refs/heads/{branch}這個(gè)下載地址分支名可以從 Search API 返回的default_branch字段直接獲得。zip 的缺點(diǎn)也明顯拿不到歷史提交也無法增量拉取新提交。所以面對(duì)“離線歸檔”“靜態(tài)掃描”這類需求zip 完全夠用但如果想在下載結(jié)果上繼續(xù)寫代碼就應(yīng)該用 git clone而且最好加--depth 1做淺克隆。3.2 主體代碼從查詢表達(dá)式到本地目錄下面的 Python 腳本是這個(gè)工具的最小可行版本。代碼把搜索、下載、解壓三個(gè)步驟分開方便替換成自己的任務(wù)隊(duì)列import argparse import json import time import zipfile from pathlib import Path import requests SEARCH_API https://api.github.com/search/repositories CODELOAD https://codeload.github.com/{full_name}/zip/refs/heads/{branch} def search_query(query, token, per_page100, max_pages10): 搜索并返回候選倉庫列表限制每頁條數(shù)和翻頁數(shù)。 headers {Accept: application/vnd.githubjson} if token: headers[Authorization] fBearer {token} cache Path(search_cache) cache.mkdir(exist_okTrue) items: list[dict] [] for page in range(1, max_pages 1): cache_file cache / fpage_{page}.json if cache_file.exists(): data json.loads(cache_file.read_text(utf-8)) else: resp requests.get( SEARCH_API, params{q: query, per_page: per_page, page: page}, headersheaders, timeout30, ) resp.raise_for_status() data resp.json() cache_file.write_text(json.dumps(data, ensure_asciiFalse), utf-8) time.sleep(0.3) items.extend(data.get(items, [])) total data.get(total_count, 0) if len(items) min(total, per_page * max_pages): break return items def download_zip(repo, out_rootdownloads): 按返回的 default_branch 從 codeload 下載 zip 并解壓。 headers {User-Agent: batch-downloader} full_name repo[full_name] branch repo.get(default_branch) or main zip_url CODELOAD.format(full_namefull_name, branchbranch) resp requests.get(zip_url, headersheaders, timeout60) resp.raise_for_status() out_dir Path(out_root) out_dir.mkdir(parentsTrue, exist_okTrue) zip_path out_dir / f{full_name.replace(/, _)}.zip zip_path.write_bytes(resp.content) target out_dir / full_name.replace(/, _) target.mkdir(parentsTrue, exist_okTrue) with zipfile.ZipFile(zip_path) as zf: zf.extractall(target) return str(target)參數(shù)說明都寫在注釋里這里再補(bǔ)充三點(diǎn)。第一搜索緩存判斷條件只有文件是否存在所以換一次搜索詞就要清理search_cache否則會(huì)用上一輪的倉庫列表去下載。第二download_zip的分支名取自default_branch如果個(gè)別倉庫返回字段缺失就回退到 main這個(gè)防御性寫法可以避免因?yàn)槟J(rèn)分支是 master 而下載到 404。第三zip 解壓后會(huì)自動(dòng)帶一層owner-repo-branch的頂層目錄如果直接把這個(gè)目錄作為最終產(chǎn)物代碼里需要再做一次 rename這個(gè)在 4.2 節(jié)展開。3.3 批量與重試的常用參數(shù)表把所有可調(diào)項(xiàng)統(tǒng)一成命令行參數(shù)便于在 CI 或定時(shí)任務(wù)里改配置。常用參數(shù)如下參數(shù)默認(rèn)值作用--query必填傳給 Search API 的完整查詢表達(dá)式--token空GitHub personal access token提升搜索限速--modezip下載方式可選 zip 或 clone--per-page100每頁返回倉庫數(shù)上限 100--max-pages10最多翻頁數(shù)1 表示只取前 100 條--retry2單個(gè)倉庫下載失敗后的重試次數(shù)--sleep0.3搜索請(qǐng)求之間的暫停秒數(shù)參數(shù)之間的聯(lián)動(dòng)關(guān)系需要留意--max-pages 1 --per-page 30等價(jià)于只獲取前 30 個(gè)倉庫適合先用少量結(jié)果驗(yàn)證流程--retry 2只對(duì) zip 下載有效git clone 重試時(shí)要把目標(biāo)目錄先刪除否則第二次 clone 會(huì)報(bào) already exists and is not an empty directory。3.4 失敗特征與超時(shí)處理批量場(chǎng)景里最典型的問題是單個(gè)倉庫下載失敗導(dǎo)致整個(gè)進(jìn)程中斷。所以要在調(diào)用download_zip的外層包一個(gè)失敗處理for repo in results: try: download_zip(repo) except (requests.exceptions.RequestException, zipfile.BadZipFile) as exc: print(fskip {repo[full_name]}: {exc})把 RequestException 和 BadZipFile 一起捕獲提示這是網(wǎng)絡(luò)層和文件層兩類可重試錯(cuò)誤。超時(shí)設(shè)置方面timeout60是 zip 下載的上限比普通 API 請(qǐng)求長(zhǎng)一倍因?yàn)?zip 包可能達(dá)到幾十 MB如果目標(biāo)倉庫普遍很大建議改到 300 秒否則會(huì)頻繁觸發(fā)超時(shí)誤報(bào)。4. 把工具放進(jìn)真實(shí)工作流范圍精調(diào)、目錄規(guī)整、遷移到內(nèi)網(wǎng) Git4.1 先用范圍壓縮再讓“根據(jù)搜索下載”不超出磁盤盲目用qdocker會(huì)得到上萬條結(jié)果下載工具會(huì)跑幾個(gè)小時(shí)。在寫批量下載前用幾個(gè)條件組合壓縮范圍指定language:把組件限定在熟悉的技術(shù)棧里。指定stars:過濾掉個(gè)人練習(xí)項(xiàng)目。指定pushed:確保倉庫仍在維護(hù)。指定archived:false避免下載只讀歸檔項(xiàng)目。指定size:排除只有 README 的空殼倉庫。這四個(gè)條件組合后的示例 q 為etcd in:name,description language:go stars:50 pushed:2023-01-01 archived:false size:1000。這里的size單位是 KBsize:1000表示大于 1 MB。很多剛接觸 Search API 的開發(fā)者會(huì)把pushed:寫成updated:API 并不支持這個(gè)字段查詢會(huì)直接返回空結(jié)果這個(gè)差異在調(diào)試時(shí)值得優(yōu)先排查。4.2 解壓后的目錄整理為 owner_repo下載工具把 zip 解壓后目錄名自動(dòng)帶上一長(zhǎng)串比如owner-repo-branch在批量歸檔時(shí)并不方便。常見做法是在下載后立刻把倉庫目錄重命名為owner_repo同時(shí)把 zip 包集中放到archives/子目錄避免與源碼混在一起。這可以合并進(jìn)download_zip的返回處理target out_dir / full_name.replace(/, _) tmp_dir next(target.iterdir()) # zip 解壓后唯一的一層頂層目錄 tmp_dir.rename(target)注意這行假設(shè)解壓產(chǎn)物只有一層頂層目錄。如果一個(gè) zip 里打包了多個(gè)根目錄直接用 next() 會(huì)漏掉其余內(nèi)容所以我在腳本里會(huì)用list(target.iterdir())取長(zhǎng)度超過 1 就打印警告并把 zip 保留備份而不是直接 rename。4.3 與內(nèi)網(wǎng) Git 平臺(tái)批量銜接批量下載到的源碼可能需要在內(nèi)部的 Git 平臺(tái)歸檔例如 GitLab 或 Gitee。這里只提常用操作不屬于工具本職。對(duì)每個(gè)已解壓的目錄執(zhí)行cd /data/repos/owner_repo git init git add . git commit -m import from github snapshot git remote add origin gitgitlab.example.com:archive/owner_repo.git git push -u origin main這種做法的邊界要說清楚git init之后生成的 git 歷史是全新的與原倉庫的提交記錄沒有任何關(guān)系。如果業(yè)務(wù)要求保留原提交歷史就不能走 zip 下載這條路而要提前切換到git clone模式拿到完整.git之后再改 remote。這也是在 3.1 節(jié)堅(jiān)持保留雙模式的原因。4.4 增量下載用本地目錄做去重批量下載工具在每天定時(shí)執(zhí)行時(shí)最好支持“只下載新增倉庫”。去重邏輯很簡(jiǎn)單done_dirs {p.name for p in Path(out_root).glob(*/) if p.is_dir()} results [r for r in results if r[full_name].replace(/, _) not in done_dirs]把已經(jīng)存在的owner_repo目錄名收集起來再?gòu)乃阉鹘Y(jié)果里過濾掉。優(yōu)點(diǎn)是零依賴不用維護(hù)狀態(tài)文件缺點(diǎn)是如果某個(gè)倉庫在本地被手動(dòng)改名它會(huì)被當(dāng)成新倉庫重新下載。下載量大時(shí)可以觀察腳本打印的跳過率決定是否需要加一層內(nèi)容哈希校驗(yàn)。5. 下載完成后的完整性校驗(yàn)與斷點(diǎn)續(xù)傳技巧5.1 用 CRC 校驗(yàn)和文件數(shù)驗(yàn)證下載結(jié)果下載結(jié)束后的狀態(tài)需要驗(yàn)證不能只看文件是否落盤。Python 的 ZipFile 提供了一次性校驗(yàn)全部文件的方法def verify_archive(path): with zipfile.ZipFile(path) as zf: bad_file zf.testzip() return (bad_file is None, len(zf.namelist()))testzip()會(huì)檢查全部 zip 條目的 CRC返回第一個(gè)損壞文件的名稱沒有損壞則返回 None。第二個(gè)返回值是包內(nèi)文件數(shù)用于判斷“內(nèi)容是否過少”。若校驗(yàn)失敗只需重新下載規(guī)劃中的該倉庫不需要全部重跑。批量場(chǎng)景里可以把校驗(yàn)步驟放在每次下載后然后通過一個(gè)小技巧記錄結(jié)果在倉庫目錄內(nèi)寫入隱藏的.download_state文件包含 zip 的 CRC 和文件數(shù)。下一次運(yùn)行時(shí)先比較該文件相同的跳過解壓不同的重新拉取。這種方法把校驗(yàn)成本降到了最低。5.2 斷點(diǎn)續(xù)傳用標(biāo)記文件避免重復(fù)下載真正要落地定時(shí)任務(wù)還需要處理“下載了一半進(jìn)程被殺掉”的情況。常規(guī)做法是引入.partial標(biāo)記文件下載前在目標(biāo)目錄里創(chuàng)建.partial下載完成并校驗(yàn)通過后刪除下次運(yùn)行時(shí)如果看到.partial仍存在說明上一次沒有完成就把這個(gè)半成品目錄移動(dòng)到broken/子目錄再重新下載mark target / .partial if mark.exists(): (target.parent / broken).mkdir(exist_okTrue) target.rename(target.parent / broken / target.name) mark.touch() download_zip(repo) mark.unlink()這樣所有倉庫的下載過程都有明確的冪等狀態(tài)要么是完整目錄要么是 broken 目錄不會(huì)存在一個(gè)不確定的半成品。配合 5.1 的驗(yàn)證函數(shù)每次結(jié)束后都能得到一份可信任的下載清單。若查詢條件被修改記得為新的 q 參數(shù)單獨(dú)建緩存目錄避免上一輪的搜索結(jié)果污染本輪任務(wù)。本文還有配套的精品資源點(diǎn)擊獲取