如何基于Python獲取SonarQube的檢查報(bào)告信息詳解
1. 代碼效果


2. 代碼處理過程
2.1. 流程圖

2.2. 流程圖的Mermaid
flowchart TD
A([Start])
A --> B[讀取配置
Sonar 地址 / 項(xiàng)目 Key / Token / 輸出路徑]
B --> C[創(chuàng)建輸出目錄]
C --> D[創(chuàng)建 Token 會(huì)話
用于 Sonar Web API]
D --> E{是否啟用 Web 登錄}
E -->|是| F[執(zhí)行 Web 登錄
獲取會(huì)話 Cookie]
E -->|否| G[跳過 Web 登錄]
F --> H[Web 會(huì)話可用]
G --> H[僅使用 Token 會(huì)話]
H --> I[獲取質(zhì)量門禁狀態(tài)]
H --> J[獲取項(xiàng)目指標(biāo)]
H --> K[分頁獲取未解決問題列表]
I --> L[初始化緩存
規(guī)則緩存 / 源碼緩存]
J --> L
K --> L
L --> M[生成 Markdown 報(bào)告]
M --> N[寫入 Markdown 文件]
%% ========== Markdown 內(nèi)部 ==========
subgraph S1[生成 Markdown 報(bào)告的內(nèi)部流程]
S1a[遍歷每一條 Issue]
S1a --> S1b[獲取規(guī)則詳情
規(guī)則結(jié)果緩存]
S1a --> S1c[獲取代碼片段]
end
M --> S1
%% ========== 代碼片段策略 ==========
subgraph S2[代碼片段獲取策略]
T1{Web 會(huì)話是否可用}
T1 -->|是| T2[調(diào)用 issue_snippets 接口]
T2 --> T3{是否成功}
T3 -->|是| T4[解析 UI 同款代碼片段]
T3 -->|否| T5[降級(jí)使用源碼接口]
T1 -->|否| T5
T5 --> T6[調(diào)用 sources/show 接口]
T6 --> T7[截取違規(guī)行前后 N 行]
end
S1c --> S2
N --> P[為 HTML 預(yù)計(jì)算所有代碼片段]
P --> Q[生成 HTML 報(bào)告]
Q --> R[寫入 HTML 文件]
R --> S[輸出原始 JSON 數(shù)據(jù)]
S --> Z([End])
3. 代碼內(nèi)容
3.1. 注意事項(xiàng)
- 注意修改
- SONAR_HOST = “http://192.168.152.134:9000”
- SONAR_USER = “admin”
- SONAR_PASS = “admin123456”
- PROJECT_KEY = “java_tool_geotoolsinput_b98089cb-632a-41c8-adc0-0bba35ed9f54”
- BRANCH = None # 如需分支: “main”
- SONAR_TOKEN = “sqp_437d1cb4b65080bad63f49a4ae244c82b4ba1390”
3.2. 代碼內(nèi)容
# -*- coding: utf-8 -*-
import math
import json
import html
import re
from html import unescape
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple
import requests
# ======================================================
# 配置(?? 技術(shù)可研:暫時(shí)寫死)
# ======================================================
SONAR_HOST = "http://192.168.152.134:9000"
# ? Web 登錄(用于拉 UI 同款 issue_snippets)
SONAR_USER = "admin"
SONAR_PASS = "admin123456"
PROJECT_KEY = "java_tool_geotoolsinput_b98089cb-632a-41c8-adc0-0bba35ed9f54"
BRANCH = None # 如需分支: "main"
# ? Token 通道(穩(wěn)定,可拉指標(biāo)/問題/規(guī)則等)
SONAR_TOKEN = "sqp_437d1cb4b65080bad63f49a4ae244c82b4ba1390"
OUT_DIR = Path(".")
MD_PATH = OUT_DIR / "sonar-report.zh-CN.md"
HTML_PATH = OUT_DIR / "sonar-report.zh-CN.html"
JSON_PATH = OUT_DIR / "sonar-report.raw.json"
# 代碼片段上下文行數(shù):違規(guī)行前后 N 行(你要的 ±5)
SNIPPET_CONTEXT = 5
# ======================================================
# 中文映射
# ======================================================
SEVERITY_CN = {
"BLOCKER": "阻斷",
"CRITICAL": "嚴(yán)重",
"MAJOR": "主要",
"MINOR": "次要",
"INFO": "提示",
}
TYPE_CN = {
"BUG": "缺陷",
"VULNERABILITY": "漏洞",
"CODE_SMELL": "代碼異味",
"SECURITY_HOTSPOT": "安全熱點(diǎn)",
}
QUALITY_CN = {
"RELIABILITY": "可靠性",
"SECURITY": "安全性",
"MAINTAINABILITY": "可維護(hù)性",
"reliability": "可靠性",
"security": "安全性",
"maintainability": "可維護(hù)性",
}
IMPACT_LEVEL_CN = {
"LOW": "低",
"MEDIUM": "中",
"HIGH": "高",
"low": "低",
"medium": "中",
"high": "高",
}
# ======================================================
# Snippet HTML 處理(issue_snippets 返回的 code 帶 <span> + HTML entity)
# ======================================================
SPAN_TAG_RE = re.compile(r"</?span[^>]*>")
def snippet_html_to_plain(text: str) -> str:
if not text:
return ""
text = SPAN_TAG_RE.sub("", text)
text = unescape(text)
return text
# ======================================================
# HTTP 基礎(chǔ)
# ======================================================
def api_get(session: requests.Session, path: str, params: Optional[dict] = None) -> dict:
url = f"{SONAR_HOST}{path}"
r = session.get(url, params=params, timeout=60)
if r.status_code in (401, 403):
raise RuntimeError(f"[ERROR] GET 鑒權(quán)失?。▄r.status_code}):{url}\n{r.text[:500]}")
r.raise_for_status()
return r.json()
def get_cookie(session: requests.Session, name: str) -> Optional[str]:
for c in session.cookies:
if c.name == name:
return c.value
return None
# ======================================================
# Token 通道 API(穩(wěn)定)
# ======================================================
def fetch_quality_gate(session: requests.Session) -> dict:
params = {"projectKey": PROJECT_KEY}
if BRANCH:
params["branch"] = BRANCH
return api_get(session, "/api/qualitygates/project_status", params)
def fetch_measures(session: requests.Session) -> dict:
metrics = ",".join(
[
"bugs",
"vulnerabilities",
"code_smells",
"security_hotspots",
"coverage",
"duplicated_lines_density",
"ncloc",
"reliability_rating",
"security_rating",
"sqale_rating",
]
)
params = {"component": PROJECT_KEY, "metricKeys": metrics}
if BRANCH:
params["branch"] = BRANCH
return api_get(session, "/api/measures/component", params)
def fetch_all_issues(session: requests.Session) -> List[dict]:
issues: List[dict] = []
params = {
"componentKeys": PROJECT_KEY,
"resolved": "false",
"ps": 500,
"p": 1,
}
if BRANCH:
params["branch"] = BRANCH
first = api_get(session, "/api/issues/search", params)
total = int(first.get("total", 0))
issues.extend(first.get("issues", []))
pages = max(1, math.ceil(total / 500))
for p in range(2, pages + 1):
params["p"] = p
data = api_get(session, "/api/issues/search", params)
issues.extend(data.get("issues", []))
return issues
def fetch_rule_detail(session: requests.Session, rule_key: str, cache: Dict[str, dict]) -> dict:
if rule_key in cache:
return cache[rule_key]
data = api_get(session, "/api/rules/show", {"key": rule_key})
rule = data.get("rule", {}) or {}
cache[rule_key] = rule
return rule
# sources/show:降級(jí)方案(拿純?cè)创a行)
def fetch_sources_lines(session: requests.Session, component_key: str, cache: Dict[str, List[dict]]) -> List[dict]:
if not component_key:
return []
if component_key in cache:
return cache[component_key]
try:
data = api_get(session, "/api/sources/show", {"component": component_key})
lines = data.get("sources", []) or []
cache[component_key] = lines
return lines
except Exception:
cache[component_key] = []
return []
def fetch_code_snippet_by_sources_show(
session: requests.Session,
component_key: str,
line: Optional[int],
context: int,
sources_cache: Dict[str, List[dict]],
) -> str:
if not component_key or not line:
return ""
sources = fetch_sources_lines(session, component_key, sources_cache)
if not sources:
return ""
start = max(1, int(line) - context)
end = int(line) + context
snippet_lines = []
for s in sources:
ln = s.get("line")
code = s.get("code", "")
if ln is None:
continue
ln = int(ln)
if start <= ln <= end:
prefix = ">>" if ln == int(line) else " "
snippet_lines.append(f"{prefix}{ln:4d}: [code]")
return "\n".join(snippet_lines)
# ======================================================
# Web 登錄 + issue_snippets(UI 同款代碼塊)
# ======================================================
def sonar_login_web(web_session: requests.Session) -> None:
url = f"{SONAR_HOST}/api/authentication/login"
headers = {
"Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded",
}
data = {"login": SONAR_USER, "password": SONAR_PASS}
r = web_session.post(url, headers=headers, data=data, timeout=60)
if r.status_code not in (200, 204):
raise RuntimeError(f"登錄失敗: HTTP {r.status_code}, body={r.text[:300]}")
jwt = get_cookie(web_session, "JWT-SESSION")
if not jwt:
raise RuntimeError("登錄后未拿到 JWT-SESSION Cookie(可能登錄失敗或被策略攔截)")
# XSRF 有時(shí)會(huì)一起下發(fā);沒有的話,后續(xù)也許仍能用,或者你可先 GET 一次頁面觸發(fā)
xsrf = get_cookie(web_session, "XSRF-TOKEN")
if not xsrf:
# 觸發(fā)一下頁面,某些環(huán)境會(huì)在這里補(bǔ)發(fā) XSRF
try:
web_session.get(f"{SONAR_HOST}/sessions/new", timeout=30)
except Exception:
pass
def fetch_issue_snippets_web(web_session: requests.Session, issue_key: str) -> Dict[str, Any]:
url = f"{SONAR_HOST}/api/sources/issue_snippets"
params = {"issueKey": issue_key}
xsrf = get_cookie(web_session, "XSRF-TOKEN") or ""
headers = {"Accept": "application/json"}
if xsrf:
headers["X-Xsrf-Token"] = xsrf
r = web_session.get(url, params=params, headers=headers, timeout=60)
if r.status_code == 403:
raise RuntimeError("403 Forbidden:issue_snippets 需要 Cookie 會(huì)話 + XSRF,或權(quán)限不足。")
r.raise_for_status()
return r.json()
def issue_snippets_payload_to_text_block(payload: Dict[str, Any]) -> Tuple[str, str]:
"""
把 issue_snippets 單文件 payload 轉(zhuǎn)成純文本代碼塊(含行號(hào))
返回:(path, code_text)
"""
comp = payload.get("component", {}) or {}
path = comp.get("path") or comp.get("name") or (comp.get("key") or "")
lines = []
for row in payload.get("sources", []) or []:
ln = row.get("line")
code_html = row.get("code", "")
code_plain = snippet_html_to_plain(code_html)
if isinstance(ln, int):
lines.append(f"{ln:4d}: {code_plain}")
else:
lines.append(code_plain)
return path, "\n".join(lines)
# ======================================================
# Snippet 統(tǒng)一入口:優(yōu)先 issue_snippets,失敗降級(jí) sources/show
# ======================================================
def guess_code_lang(component_key: str) -> str:
ck = (component_key or "").lower()
if ck.endswith(".java"):
return "java"
if ck.endswith(".xml"):
return "xml"
if ck.endswith(".yml") or ck.endswith(".yaml"):
return "yaml"
if ck.endswith(".sql"):
return "sql"
return ""
def get_issue_code_snippet(
issue: dict,
web_session: Optional[requests.Session],
token_session: requests.Session,
sources_cache: Dict[str, List[dict]],
) -> Tuple[str, str]:
"""
返回 (lang, snippet_text)
- 優(yōu)先:issue_snippets(web_session)
- 降級(jí):sources/show(token_session,±SNIPPET_CONTEXT)
"""
component = issue.get("component", "") or ""
line_no = issue.get("line")
issue_key = issue.get("key", "") or ""
# 1) 優(yōu)先 issue_snippets(UI 同款)
if web_session is not None and issue_key:
try:
snips = fetch_issue_snippets_web(web_session, issue_key)
# 返回結(jié)構(gòu):{ "<componentKey>": {component:{...}, sources:[...]} }
# 通常只有一個(gè) key;取第一個(gè)即可
for _k, payload in snips.items():
path, code_text = issue_snippets_payload_to_text_block(payload)
# issue_snippets 自帶“范圍行”,通常已經(jīng)類似截圖內(nèi)容
lang = guess_code_lang(path)
return lang, code_text
except Exception:
# 靜默降級(jí)
pass
# 2) 降級(jí) sources/show(前后 N 行)
if component and line_no:
code = fetch_code_snippet_by_sources_show(
session=token_session,
component_key=component,
line=int(line_no),
context=SNIPPET_CONTEXT,
sources_cache=sources_cache,
)
lang = guess_code_lang(component)
return lang, code
return "", ""
# ======================================================
# 格式化工具
# ======================================================
def cn_quality_gate(status: str) -> str:
if status == "OK":
return "通過"
if status == "ERROR":
return "未通過"
return status or "未知"
def safe_md(text: str) -> str:
if text is None:
return ""
return str(text).replace("\n", " ").replace("|", "\\|").strip()
def safe_html(text: str) -> str:
return html.escape("" if text is None else str(text))
def issue_ui_link(issue_key: str) -> str:
return f"{SONAR_HOST}/project/issues?id={PROJECT_KEY}&open={issue_key}"
def rule_ui_link(rule_key: str) -> str:
return f"{SONAR_HOST}/coding_rules?open={rule_key}"
def format_impacts(rule: dict) -> str:
impacts = rule.get("impacts")
if not impacts:
return "—"
parts = []
if isinstance(impacts, dict):
for k, v in impacts.items():
q = QUALITY_CN.get(k, str(k) or "")
lv = IMPACT_LEVEL_CN.get(v, str(v) or "")
if q:
parts.append(f"{q}·{lv}")
return "、".join(parts) if parts else "—"
if isinstance(impacts, list):
for item in impacts:
if not isinstance(item, dict):
continue
qk = item.get("softwareQuality") or item.get("quality") or item.get("key")
lv = item.get("severity") or item.get("level") or item.get("impact")
q = QUALITY_CN.get(qk, str(qk) if qk else "")
lv_cn = IMPACT_LEVEL_CN.get(lv, str(lv) if lv else "")
if q:
parts.append(f"{q}·{lv_cn}")
return "、".join(parts) if parts else "—"
return "—"
def format_remediation(rule: dict) -> str:
base = rule.get("remFnBaseEffort")
rtype = rule.get("remFnType")
gap = rule.get("remFnGapMultiplier")
coeff = rule.get("debtRemFnCoeff")
if base:
return f"{base}"
if isinstance(coeff, str) and coeff.strip():
return coeff.strip()
if rtype or gap:
return f"{rtype or ''}{(' / ' + gap) if gap else ''}".strip(" /")
return "—"
def strip_html_to_text(s: str) -> str:
if not s:
return ""
s = re.sub(r"<br\s*/?>", " ", s, flags=re.IGNORECASE)
s = re.sub(r"</p\s*>", " ", s, flags=re.IGNORECASE)
s = re.sub(r"<[^>]+>", "", s)
return re.sub(r"\s+", " ", s).strip()
# ======================================================
# Markdown 報(bào)告
# ======================================================
def build_markdown_report(
qg: dict,
measures: dict,
issues: List[dict],
rule_cache: Dict[str, dict],
token_session: requests.Session,
web_session: Optional[requests.Session],
sources_cache: Dict[str, List[dict]],
) -> str:
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
qg_status = qg.get("projectStatus", {}).get("status", "UNKNOWN")
metric_map = {}
for m in measures.get("component", {}).get("measures", []) or []:
metric_map[m.get("metric")] = m.get("value")
def mv(k: str) -> str:
return str(metric_map.get(k, ""))
lines = []
lines.append("# SonarQube 檢查報(bào)告(中文)")
lines.append("")
lines.append(f"- 項(xiàng)目 Key:`{PROJECT_KEY}`")
if BRANCH:
lines.append(f"- 分支:`{BRANCH}`")
lines.append(f"- 生成時(shí)間:`{now}`")
lines.append(f"- SonarQube:`{SONAR_HOST}`")
lines.append(f"- 質(zhì)量門禁:**{cn_quality_gate(qg_status)}**")
lines.append("")
lines.append("## 指標(biāo)概覽")
lines.append("")
lines.append("| 指標(biāo) | 數(shù)值 |")
lines.append("|---|---:|")
lines.append(f"| Bugs(缺陷) | {mv('bugs')} |")
lines.append(f"| Vulnerabilities(漏洞) | {mv('vulnerabilities')} |")
lines.append(f"| Code Smells(代碼異味) | {mv('code_smells')} |")
lines.append(f"| Security Hotspots(安全熱點(diǎn)) | {mv('security_hotspots')} |")
lines.append(f"| Coverage(覆蓋率%) | {mv('coverage')} |")
lines.append(f"| Duplications(重復(fù)率%) | {mv('duplicated_lines_density')} |")
lines.append(f"| NCLOC(有效代碼行) | {mv('ncloc')} |")
lines.append("")
conds = qg.get("projectStatus", {}).get("conditions", []) or []
if conds:
lines.append("## 質(zhì)量門禁條件(Quality Gate Conditions)")
lines.append("")
lines.append("| 指標(biāo) | 條件 | 閾值 | 實(shí)際值 | 狀態(tài) |")
lines.append("|---|---|---:|---:|---|")
for c in conds:
metric = c.get("metricKey", "")
op = c.get("operator", "")
thr = c.get("errorThreshold", "")
actual = c.get("actualValue", "")
st = c.get("status", "")
lines.append(f"| {metric} | {op} | {thr} | {actual} | {st} |")
lines.append("")
lines.append("## 問題清單(未解決)")
lines.append(f"問題總數(shù):**{len(issues)}**")
lines.append("")
lines.append("| 嚴(yán)重性 | 類型 | 規(guī)則(可點(diǎn)) | 影響(軟件質(zhì)量) | 修復(fù)成本 | 文件 | 行號(hào) | 標(biāo)題/說明 |")
lines.append("|---|---|---|---|---|---|---:|---|")
for it in issues:
rule_key = it.get("rule", "")
rule = fetch_rule_detail(token_session, rule_key, rule_cache) if rule_key else {}
severity_cn = SEVERITY_CN.get(it.get("severity"), it.get("severity") or "")
type_cn = TYPE_CN.get(it.get("type"), it.get("type") or "")
rule_name = rule.get("name", "") or ""
impacts_cn = format_impacts(rule)
remediation = format_remediation(rule)
component = it.get("component", "") or ""
line_no = it.get("line") or ""
message = it.get("message", "") or ""
rule_link = rule_ui_link(rule_key) if rule_key else ""
rule_cell = f"[`{safe_md(rule_key)}`]({rule_link})"
if rule_name:
rule_cell += f"<br/>{safe_md(rule_name)}"
lines.append(
"| {sev} | {typ} | {rule} | {imp} | {rem} | `{file}` | {line} | {msg} |".format(
sev=safe_md(severity_cn),
typ=safe_md(type_cn),
rule=rule_cell,
imp=safe_md(impacts_cn),
rem=safe_md(remediation),
file=safe_md(component),
line=safe_md(line_no),
msg=safe_md(message),
)
)
lines.append("")
lines.append(f"## 問題詳情(含代碼片段:優(yōu)先 UI 同款,否則降級(jí)為前后 {SNIPPET_CONTEXT} 行)")
lines.append("")
for idx, it in enumerate(issues, start=1):
rule_key = it.get("rule", "")
rule = fetch_rule_detail(token_session, rule_key, rule_cache) if rule_key else {}
severity_cn = SEVERITY_CN.get(it.get("severity"), it.get("severity") or "")
type_cn = TYPE_CN.get(it.get("type"), it.get("type") or "")
rule_name = rule.get("name", "") or ""
impacts_cn = format_impacts(rule)
remediation = format_remediation(rule)
component = it.get("component", "") or ""
line_no = it.get("line")
message = it.get("message", "") or ""
issue_key = it.get("key", "") or ""
lines.append(f"### {idx}. {safe_md(message)}")
lines.append(f"- 嚴(yán)重性:{safe_md(severity_cn)}")
lines.append(f"- 類型:{safe_md(type_cn)}")
lines.append(f"- 規(guī)則:`{safe_md(rule_key)}` {safe_md(rule_name)}")
lines.append(f"- 影響:{safe_md(impacts_cn)}")
lines.append(f"- 修復(fù)成本:{safe_md(remediation)}")
lines.append(f"- 位置:`{safe_md(component)}` 行 {line_no if line_no else ''}")
if issue_key:
lines.append(f"- Sonar 鏈接:{issue_ui_link(issue_key)}")
lines.append("")
lang, snippet = get_issue_code_snippet(it, web_session, token_session, sources_cache)
if snippet:
# issue_snippets 通常范圍更大;sources/show 是 ±N 行并帶 >> 標(biāo)記
lines.append(f"```{lang}".rstrip())
lines.append(snippet)
lines.append("```")
else:
lines.append("> (無法獲取源碼片段:可能權(quán)限不足,或分析未保存源碼。)")
lines.append("")
lines.append("## 規(guī)則說明(抽取本次用到的規(guī)則)")
lines.append("")
used_rules = sorted({it.get("rule") for it in issues if it.get("rule")})
for rk in used_rules:
rule = fetch_rule_detail(token_session, rk, rule_cache)
name = rule.get("name", "") or ""
impacts_cn = format_impacts(rule)
remediation = format_remediation(rule)
desc = strip_html_to_text(rule.get("htmlDesc", "") or "")
desc_short = (desc[:220] + "…") if len(desc) > 220 else desc
lines.append(f"### `{rk}` {name}")
lines.append(f"- 影響:{impacts_cn}")
lines.append(f"- 修復(fù)成本:{remediation}")
if desc_short:
lines.append(f"- 說明:{safe_md(desc_short)}")
lines.append(f"- 規(guī)則鏈接:{rule_ui_link(rk)}")
lines.append("")
return "\n".join(lines)
# ======================================================
# HTML 報(bào)告(表格 + 內(nèi)嵌代碼片段)
# ======================================================
def build_html_report(md_text: str, issues: List[dict], rule_cache: Dict[str, dict], issue_snippets: Dict[str, str]) -> str:
rows = []
for it in issues:
rule_key = it.get("rule", "") or ""
rule = rule_cache.get(rule_key, {}) if rule_key else {}
sev = SEVERITY_CN.get(it.get("severity"), it.get("severity") or "")
typ = TYPE_CN.get(it.get("type"), it.get("type") or "")
rule_name = rule.get("name", "") or ""
impacts_cn = format_impacts(rule)
remediation = format_remediation(rule)
component = it.get("component", "") or ""
line_no = it.get("line") or ""
message = it.get("message", "") or ""
issue_key = it.get("key", "") or ""
issue_link = issue_ui_link(issue_key) if issue_key else ""
rule_link = rule_ui_link(rule_key) if rule_key else ""
rule_name_html = f"<div class='sub'>{safe_html(rule_name)}</div>" if rule_name else ""
open_html = f"<a href='{safe_html(issue_link)}' target='_blank'>打開</a>" if issue_link else "—"
code_block = issue_snippets.get(issue_key, "")
code_html = f"<pre class='code'>{safe_html(code_block)}</pre>" if code_block else "<div class='small'>(無源碼片段)</div>"
row = (
"<tr>"
f"<td>{safe_html(sev)}</td>"
f"<td>{safe_html(typ)}</td>"
f"<td><a href='{safe_html(rule_link)}' target='_blank'>{safe_html(rule_key)}</a>{rule_name_html}</td>"
f"<td>{safe_html(impacts_cn)}</td>"
f"<td>{safe_html(remediation)}</td>"
f"<td><code>{safe_html(component)}</code></td>"
f"<td class='num'>{safe_html(line_no)}</td>"
f"<td>{safe_html(message)}{code_html}</td>"
f"<td>{open_html}</td>"
"</tr>"
)
rows.append(row)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
branch_html = f" 分支:<code>{safe_html(BRANCH)}</code>" if BRANCH else ""
return f"""<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>SonarQube 檢查報(bào)告(中文)</title>
<style>
body {{ font-family: system-ui, -apple-system, "Segoe UI", Arial, sans-serif; margin: 24px; color: #111; }}
h1 {{ margin: 0 0 8px 0; }}
.meta {{ color:#555; margin: 0 0 16px 0; line-height: 1.6; }}
table {{ border-collapse: collapse; width: 100%; }}
th, td {{ border: 1px solid #e5e7eb; padding: 10px; vertical-align: top; }}
th {{ background: #f8fafc; text-align: left; }}
td.num {{ text-align: right; white-space: nowrap; }}
.sub {{ color:#555; margin-top: 4px; font-size: 12px; line-height: 1.4; }}
.small {{ font-size: 12px; color:#666; }}
.hr {{ height:1px; background:#eee; margin: 18px 0; }}
details pre {{ white-space: pre-wrap; word-wrap: break-word; background:#f6f8fa; padding: 12px; border-radius: 10px; }}
code {{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-size: 12px; }}
pre.code {{
white-space: pre-wrap;
word-wrap: break-word;
background:#0b1020;
color:#e6edf3;
padding: 10px;
border-radius: 10px;
margin-top: 8px;
font-size: 12px;
line-height: 1.45;
}}
</style>
</head>
<body>
<h1>SonarQube 檢查報(bào)告(中文)</h1>
<div class="meta">
<div>生成時(shí)間:<code>{safe_html(now)}</code></div>
<div>項(xiàng)目 Key:<code>{safe_html(PROJECT_KEY)}</code>{branch_html}</div>
<div>SonarQube:<code>{safe_html(SONAR_HOST)}</code></div>
<div class="small">說明:本報(bào)告自動(dòng)內(nèi)嵌每條問題的源碼片段(優(yōu)先 UI issue_snippets,否則降級(jí)為 sources/show 前后 {SNIPPET_CONTEXT} 行)。</div>
</div>
<div class="hr"></div>
<h2>問題清單(未解決)</h2>
<div class="small">總計(jì):{len(issues)} 條</div>
<br/>
<table>
<thead>
<tr>
<th>嚴(yán)重性</th>
<th>類型</th>
<th>規(guī)則</th>
<th>影響(軟件質(zhì)量)</th>
<th>修復(fù)成本</th>
<th>文件</th>
<th>行號(hào)</th>
<th>標(biāo)題/說明(含代碼片段)</th>
<th>打開</th>
</tr>
</thead>
<tbody>
{''.join(rows) if rows else '<tr><td colspan="9">無未解決問題</td></tr>'}
</tbody>
</table>
<div class="hr"></div>
<details>
<summary>展開:Markdown 原文(便于復(fù)制到 Obsidian)</summary>
<pre>{safe_html(md_text)}</pre>
</details>
</body>
</html>
"""
# ======================================================
# main
# ======================================================
def main():
OUT_DIR.mkdir(parents=True, exist_ok=True)
# 1) Token Session(穩(wěn)定)
token_session = requests.Session()
token_session.auth = (SONAR_TOKEN, "")
# 2) Web Session(用于 issue_snippets,可失敗,失敗則只用 token 降級(jí))
web_session: Optional[requests.Session] = None
try:
ws = requests.Session()
sonar_login_web(ws)
web_session = ws
print("[INFO] Web 登錄成功:將啟用 issue_snippets 代碼片段。")
except Exception as e:
print(f"[WARN] Web 登錄失敗,將降級(jí)僅用 Token 通道獲取代碼片段(sources/show):{e}")
# 3) 拉取數(shù)據(jù)
qg = fetch_quality_gate(token_session)
measures = fetch_measures(token_session)
issues = fetch_all_issues(token_session)
rule_cache: Dict[str, dict] = {}
sources_cache: Dict[str, List[dict]] = {}
# 4) 生成 Markdown(內(nèi)部會(huì)為每條 issue 拉 snippet)
md = build_markdown_report(
qg=qg,
measures=measures,
issues=issues,
rule_cache=rule_cache,
token_session=token_session,
web_session=web_session,
sources_cache=sources_cache,
)
MD_PATH.write_text(md, encoding="utf-8")
# 5) 為 HTML 預(yù)計(jì)算每條 issue 的 snippet(按 issue_key 存)
issue_snippets: Dict[str, str] = {}
for it in issues:
issue_key = it.get("key", "") or ""
if not issue_key:
continue
lang, snippet = get_issue_code_snippet(it, web_session, token_session, sources_cache)
if snippet:
issue_snippets[issue_key] = snippet
html_doc = build_html_report(md, issues, rule_cache, issue_snippets)
HTML_PATH.write_text(html_doc, encoding="utf-8")
# 6) Raw JSON
JSON_PATH.write_text(
json.dumps(
{
"projectKey": PROJECT_KEY,
"branch": BRANCH,
"sonarHost": SONAR_HOST,
"qualityGate": qg,
"measures": measures,
"issues": issues,
"rules": rule_cache,
"snippetsContext": SNIPPET_CONTEXT,
"snippetsSource": "issue_snippets(web) preferred, fallback sources/show(token)",
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
print("? 報(bào)告生成完成:")
print(f" - {MD_PATH.name}")
print(f" - {HTML_PATH.name}")
print(f" - {JSON_PATH.name}")
if __name__ == "__main__":
main()
總結(jié)
到此這篇關(guān)于如何基于Python獲取SonarQube的檢查報(bào)告信息的文章就介紹到這了,更多相關(guān)Python獲取SonarQube檢查報(bào)告信息內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python GUI庫圖形界面開發(fā)之PyQt5布局控件QHBoxLayout詳細(xì)使用方法與實(shí)例
這篇文章主要介紹了python GUI庫圖形界面開發(fā)之PyQt5布局控件QHBoxLayout詳細(xì)使用方法與實(shí)例,需要的朋友可以參考下2020-03-03
基于Python實(shí)現(xiàn)圖像文字識(shí)別OCR工具
在工作、生活中常常會(huì)用到,比如票據(jù)、漫畫、掃描件、照片的文本提取。本文主要介紹了基于PyQt + PaddleOCR實(shí)現(xiàn)的一個(gè)桌面端的OCR工具,用于快速實(shí)現(xiàn)圖片中文本區(qū)域自動(dòng)檢測(cè)+文本自動(dòng)識(shí)別,需要的朋友可以參考一下2021-12-12
python?spotlight庫簡(jiǎn)化交互式方法探索數(shù)據(jù)分析
這篇文章主要為大家介紹了python?spotlight庫簡(jiǎn)化的交互式方法探索數(shù)據(jù),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
對(duì)PyQt5的輸入對(duì)話框使用(QInputDialog)詳解
今天小編就為大家分享一篇對(duì)PyQt5的輸入對(duì)話框使用(QInputDialog)詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-06-06
Python 實(shí)現(xiàn)取多維數(shù)組第n維的前幾位
今天小編就為大家分享一篇Python 實(shí)現(xiàn)取多維數(shù)組第n維的前幾位,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-11-11
pytorch在fintune時(shí)將sequential中的層輸出方法,以vgg為例
今天小編就為大家分享一篇pytorch在fintune時(shí)將sequential中的層輸出方法,以vgg為例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-08-08
Python 相對(duì)路徑報(bào)錯(cuò):"No such file or 
如果你取相對(duì)路徑不是在主文件里,可能就會(huì)有相對(duì)路徑問題:"No such file or directory",由于python 的相對(duì)路徑,相對(duì)的都是主文件所以會(huì)出現(xiàn)Python 相對(duì)路徑報(bào)錯(cuò),今天小編給大家?guī)砹送昝澜鉀Q方案,感興趣的朋友一起看看吧2023-02-02

