最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

如何基于Python獲取SonarQube的檢查報(bào)告信息詳解

 更新時(shí)間:2026年06月05日 09:34:33   作者:九又四分之三站臺(tái)Emm  
SonarQube 是一種流行的開源平臺(tái),用于持續(xù)檢查代碼質(zhì)量問題,它支持多種編程語言,包括 Python,這篇文章主要介紹了如何基于Python獲取SonarQube的檢查報(bào)告信息的相關(guān)資料,需要的朋友可以參考下

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多線程下的變量問題

    淺析Python多線程下的變量問題

    這篇文章主要介紹了Python多線程下的變量問題,由于GIL的存在,Python的多線程編程問題一直是開發(fā)者中的熱點(diǎn)話題,需要的朋友可以參考下
    2015-04-04
  • python GUI庫圖形界面開發(fā)之PyQt5布局控件QHBoxLayout詳細(xì)使用方法與實(shí)例

    python GUI庫圖形界面開發(fā)之PyQt5布局控件QHBoxLayout詳細(xì)使用方法與實(shí)例

    這篇文章主要介紹了python GUI庫圖形界面開發(fā)之PyQt5布局控件QHBoxLayout詳細(xì)使用方法與實(shí)例,需要的朋友可以參考下
    2020-03-03
  • 基于Python實(shí)現(xiàn)圖像文字識(shí)別OCR工具

    基于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ù)分析

    這篇文章主要為大家介紹了python?spotlight庫簡(jiǎn)化的交互式方法探索數(shù)據(jù),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2024-01-01
  • Python之Pygame的Draw繪圖

    Python之Pygame的Draw繪圖

    Pygame 中提供了一個(gè)draw模塊用來繪制一些簡(jiǎn)單的圖形狀,比如矩形、多邊形、圓形、直線、弧線等。本文主要介紹Pygame中的Draw繪圖,感興趣的同學(xué)可以參考閱讀
    2023-04-04
  • python如何用pyecharts制作詞云圖

    python如何用pyecharts制作詞云圖

    大家好,本篇文章主要講的是python如何用pyecharts制作詞云圖,感興趣的同學(xué)趕快來看一看吧,對(duì)你有幫助的話記得收藏一下
    2022-01-01
  • 對(duì)PyQt5的輸入對(duì)話框使用(QInputDialog)詳解

    對(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維的前幾位

    今天小編就為大家分享一篇Python 實(shí)現(xiàn)取多維數(shù)組第n維的前幾位,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2019-11-11
  • pytorch在fintune時(shí)將sequential中的層輸出方法,以vgg為例

    pytorch在fintune時(shí)將sequential中的層輸出方法,以vgg為例

    今天小編就為大家分享一篇pytorch在fintune時(shí)將sequential中的層輸出方法,以vgg為例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2019-08-08
  • Python 相對(duì)路徑報(bào)錯(cuò):"No such file or directory"'原因及解決方法

    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

最新評(píng)論

凌海市| 东兰县| 新巴尔虎左旗| 清丰县| 巴马| 普洱| 霍林郭勒市| 宁国市| 徐闻县| 淮北市| 温州市| 金溪县| 固镇县| 武义县| 阿尔山市| 横峰县| 读书| 雅安市| 泰宁县| 景谷| 江口县| 漳平市| 阿图什市| 博野县| 正阳县| 门源| 双流县| 河曲县| 三台县| 安泽县| 商河县| 玛沁县| 普陀区| 长宁县| 南开区| 常宁市| 酒泉市| 偏关县| 和龙市| 冕宁县| 广灵县|