refactor: 竞品报告迁入 pipeline,爬虫目录仅保留采集与兼容入口

competitor_report 子包与 jd_competitor_report 主模块移至 backend/pipeline;runner 与测试改为 from pipeline import jd_competitor_report;crawler_copy 下 jd_competitor_report 为转发 shim;jd_keyword_pipeline 文档更新。

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-17 13:47:34 +08:00
parent b6a52c04d2
commit 0d8e1962d7
27 changed files with 1410 additions and 1443 deletions

View File

@ -1 +0,0 @@
"""竞品报告脚本共享表头常量、CSV 辅助、报告调参解析。"""

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,7 @@ PC 搜索导出 CSV、评价扁平 CSV、详情汇总 CSV``detail_ware_export
默认启用 **应用场景筛选**``brief_content.txt`` 4.1 中式面点/主食 + 4.2 烘焙仅命中关键词的 SKU 进入详情与评论队列词表见 ``scenario_filter.py````SCENARIO_FILTER_ENABLED=False`` 可关闭``SCENARIO_FILTER_PC_SEARCH_CSV="filtered"`` 可使导出 CSV 与筛选后列表一致 默认启用 **应用场景筛选**``brief_content.txt`` 4.1 中式面点/主食 + 4.2 烘焙仅命中关键词的 SKU 进入详情与评论队列词表见 ``scenario_filter.py````SCENARIO_FILTER_ENABLED=False`` 可关闭``SCENARIO_FILTER_PC_SEARCH_CSV="filtered"`` 可使导出 CSV 与筛选后列表一致
SKU 完整接口 JSON 仍在 ``detail/ware_{sku}_response.json`` SKU 完整接口 JSON 仍在 ``detail/ware_{sku}_response.json``
端到端竞品速览 Markdown配置 ``jd_competitor_report.py`` 顶部 ``KEYWORD`` 后执行 ``python jd_competitor_report.py``内部调用本模块 ``main(keyword=...)`` 端到端竞品速览 Markdown``backend`` 配置 ``pipeline.jd_competitor_report`` 顶部 ``KEYWORD`` 后执行 ``python -m pipeline.jd_competitor_report``或执行本目录兼容入口 ``python jd_competitor_report.py``内部调用本模块 ``main(keyword=...)``
""" """
from __future__ import annotations from __future__ import annotations
@ -317,7 +317,7 @@ def main(keyword: str | None = None) -> Path:
""" """
跑完整条流水线``keyword`` 非空时覆盖文件内 ``KEYWORD``返回本次运行目录 跑完整条流水线``keyword`` 非空时覆盖文件内 ``KEYWORD``返回本次运行目录
``jd_competitor_report`` 等脚本 ``import`` 调用命令行仍执行 ``main()`` 无参 ``pipeline.jd_competitor_report`` 等脚本 ``import`` 调用命令行仍执行 ``main()`` 无参
""" """
try: try:
if hasattr(sys.stdout, "reconfigure"): if hasattr(sys.stdout, "reconfigure"):

View File

@ -0,0 +1 @@
"""京东竞品分析报告CSV 解析、统计与 Markdown/简报(归属 ``pipeline``,与爬虫采集分离)。"""

View File

@ -1,4 +1,4 @@
"""CSV 表头键、运行默认调参与关注词/场景配置(与 ``jd_competitor_report`` 顶层一致)。""" """CSV 表头键、运行默认调参与关注词/场景配置(与 ``pipeline.jd_competitor_report`` 顶层一致)。"""
from __future__ import annotations from __future__ import annotations
from pipeline.csv_schema import ( from pipeline.csv_schema import (

View File

@ -43,7 +43,7 @@ JCR_ROOT = BACKEND_ROOT / "crawler_copy" / "jd_pc_search"
if str(JCR_ROOT) not in sys.path: if str(JCR_ROOT) not in sys.path:
sys.path.insert(0, str(JCR_ROOT)) sys.path.insert(0, str(JCR_ROOT))
import jd_competitor_report as jcr # noqa: E402 from pipeline import jd_competitor_report as jcr # noqa: E402
import jd_keyword_pipeline as kpl # noqa: E402 import jd_keyword_pipeline as kpl # noqa: E402
from pipeline.csv_schema import MERGED_FIELD_TO_CSV_HEADER # noqa: E402 from pipeline.csv_schema import MERGED_FIELD_TO_CSV_HEADER # noqa: E402

View File

@ -26,7 +26,7 @@ JCR_ROOT = BACKEND_ROOT / "crawler_copy" / "jd_pc_search"
if str(JCR_ROOT) not in sys.path: if str(JCR_ROOT) not in sys.path:
sys.path.insert(0, str(JCR_ROOT)) sys.path.insert(0, str(JCR_ROOT))
import jd_competitor_report as jcr # noqa: E402 from pipeline import jd_competitor_report as jcr # noqa: E402
import jd_keyword_pipeline as kpl # noqa: E402 import jd_keyword_pipeline as kpl # noqa: E402

View File

@ -37,7 +37,7 @@ JCR_ROOT = BACKEND_ROOT / "crawler_copy" / "jd_pc_search"
if str(JCR_ROOT) not in sys.path: if str(JCR_ROOT) not in sys.path:
sys.path.insert(0, str(JCR_ROOT)) sys.path.insert(0, str(JCR_ROOT))
import jd_competitor_report as jcr # noqa: E402 from pipeline import jd_competitor_report as jcr # noqa: E402
import jd_keyword_pipeline as kpl # noqa: E402 import jd_keyword_pipeline as kpl # noqa: E402
from pipeline.csv_schema import MERGED_FIELD_TO_CSV_HEADER # noqa: E402 from pipeline.csv_schema import MERGED_FIELD_TO_CSV_HEADER # noqa: E402

View File

@ -1,5 +1,5 @@
""" """
使用 ``crawler_copy/jd_pc_search`` 中的副本脚本执行流水线并生成竞品 Markdown 使用 ``crawler_copy/jd_pc_search`` 中的采集脚本执行流水线竞品 Markdown ``pipeline.jd_competitor_report`` 生成
依赖环境变量 ``LOW_GI_PROJECT_ROOT`` Django settings ``market_assistant/.env`` 注入 依赖环境变量 ``LOW_GI_PROJECT_ROOT`` Django settings ``market_assistant/.env`` 注入
""" """
from __future__ import annotations from __future__ import annotations
@ -133,13 +133,14 @@ def try_write_competitor_report_if_merged_exists(
def _jd_crawler_modules(): def _jd_crawler_modules():
from pipeline import jd_competitor_report as jcr # noqa: WPS433
root = Path(settings.CRAWLER_JD_ROOT) root = Path(settings.CRAWLER_JD_ROOT)
if not root.is_dir(): if not root.is_dir():
raise FileNotFoundError(f"爬虫副本目录不存在: {root}") raise FileNotFoundError(f"爬虫副本目录不存在: {root}")
root_s = str(root.resolve()) root_s = str(root.resolve())
if root_s not in sys.path: if root_s not in sys.path:
sys.path.insert(0, root_s) sys.path.insert(0, root_s)
import jd_competitor_report as jcr # noqa: WPS433
import jd_keyword_pipeline as kpl # noqa: WPS433 import jd_keyword_pipeline as kpl # noqa: WPS433
return jcr, kpl return jcr, kpl
@ -163,7 +164,7 @@ def use_chunked_group_summaries_llm(report_config: dict[str, Any] | None) -> boo
def get_default_report_config() -> dict[str, Any]: def get_default_report_config() -> dict[str, Any]:
"""与 ``jd_competitor_report`` 模块常量一致的默认报告调参(供前端回填)。""" """与 ``pipeline.jd_competitor_report`` 模块常量一致的默认报告调参(供前端回填)。"""
jcr, _ = _jd_crawler_modules() jcr, _ = _jd_crawler_modules()
return { return {
"llm_comment_sentiment": True, "llm_comment_sentiment": True,

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
""" """
``competitor_report.matrix_group`` / 历史 ``jd_competitor_report`` 中路径解析逻辑同源 ``pipeline.competitor_report.matrix_group`` / 历史爬虫侧脚本中路径解析逻辑同源
从商详 ``detail_category_path`` 解析 §5 竞品矩阵用的类目展示名如饼干 从商详 ``detail_category_path`` 解析 §5 竞品矩阵用的类目展示名如饼干
""" """
from __future__ import annotations from __future__ import annotations

View File

@ -545,7 +545,7 @@ def generate_report_charts(
) )
def scenario_group_asset_slug(group: str, index: int) -> str: def scenario_group_asset_slug(group: str, index: int) -> str:
"""与 ``jd_competitor_report._scenario_group_asset_slug`` 保持一致。""" """与 ``pipeline.jd_competitor_report`` / ``report_md_helpers._scenario_group_asset_slug`` 保持一致。"""
raw = (group or "").strip() raw = (group or "").strip()
core = re.sub(r"[^\w\u4e00-\u9fff-]", "", raw)[:20] core = re.sub(r"[^\w\u4e00-\u9fff-]", "", raw)[:20]
if not core: if not core:

View File

@ -2,11 +2,8 @@
from __future__ import annotations from __future__ import annotations
import json import json
import sys
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
from django.conf import settings
from django.test import SimpleTestCase from django.test import SimpleTestCase
from pipeline.llm.generate import ( from pipeline.llm.generate import (
@ -16,10 +13,7 @@ from pipeline.llm.generate import (
def _load_jcr(): def _load_jcr():
root = Path(settings.CRAWLER_JD_ROOT).resolve() from pipeline import jd_competitor_report as jcr # noqa: WPS433
if str(root) not in sys.path:
sys.path.insert(0, str(root))
import jd_competitor_report as jcr # noqa: WPS433
return jcr return jcr

View File

@ -1,24 +1,18 @@
"""结构化竞品摘要:空样本烟测(不依赖真实 run_dir CSV""" """结构化竞品摘要:空样本烟测(不依赖真实 run_dir CSV"""
from __future__ import annotations from __future__ import annotations
import sys
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from django.conf import settings
from django.test import SimpleTestCase from django.test import SimpleTestCase
from pipeline import jd_competitor_report as jcr
from pipeline.csv_schema import infer_total_sales_from_sales_floor from pipeline.csv_schema import infer_total_sales_from_sales_floor
from pipeline.reporting.charts import _cn_volume_int from pipeline.reporting.charts import _cn_volume_int
class BuildCompetitorBriefTests(SimpleTestCase): class BuildCompetitorBriefTests(SimpleTestCase):
def test_empty_merged_json_safe(self) -> None: def test_empty_merged_json_safe(self) -> None:
root = Path(settings.CRAWLER_JD_ROOT).resolve()
if str(root) not in sys.path:
sys.path.insert(0, str(root))
import jd_competitor_report as jcr # noqa: WPS433
with tempfile.TemporaryDirectory() as td: with tempfile.TemporaryDirectory() as td:
run_dir = Path(td) run_dir = Path(td)
(run_dir / "pc_search_raw").mkdir(parents=True) (run_dir / "pc_search_raw").mkdir(parents=True)
@ -43,11 +37,6 @@ class BuildCompetitorBriefTests(SimpleTestCase):
json.dumps(out) json.dumps(out)
def test_comment_sentiment_llm_payload_has_semantic_pool(self) -> None: def test_comment_sentiment_llm_payload_has_semantic_pool(self) -> None:
root = Path(settings.CRAWLER_JD_ROOT).resolve()
if str(root) not in sys.path:
sys.path.insert(0, str(root))
import jd_competitor_report as jcr # noqa: WPS433
texts = ["口感软硬适中很好吃", "太差了不建议"] texts = ["口感软硬适中很好吃", "太差了不建议"]
attr = [f"【细类ASKU1品名x店铺y】{t}" for t in texts] attr = [f"【细类ASKU1品名x店铺y】{t}" for t in texts]
pl = jcr.build_comment_sentiment_llm_payload( pl = jcr.build_comment_sentiment_llm_payload(
@ -61,11 +50,6 @@ class BuildCompetitorBriefTests(SimpleTestCase):
self.assertGreaterEqual(len(pl["sample_reviews_semantic_pool"]), 1) self.assertGreaterEqual(len(pl["sample_reviews_semantic_pool"]), 1)
def test_comment_sentiment_score_then_lexeme(self) -> None: def test_comment_sentiment_score_then_lexeme(self) -> None:
root = Path(settings.CRAWLER_JD_ROOT).resolve()
if str(root) not in sys.path:
sys.path.insert(0, str(root))
import jd_competitor_report as jcr # noqa: WPS433
texts = ["很好吃", "太差了", "一般般"] texts = ["很好吃", "太差了", "一般般"]
scores = [5, 1, 3] scores = [5, 1, 3]
lex = jcr._comment_sentiment_lexicon(texts, scores) lex = jcr._comment_sentiment_lexicon(texts, scores)
@ -77,22 +61,12 @@ class BuildCompetitorBriefTests(SimpleTestCase):
self.assertEqual(pl.get("sentiment_bucket_method"), "score_then_lexeme") self.assertEqual(pl.get("sentiment_bucket_method"), "score_then_lexeme")
def test_comment_sentiment_all_scores_missing_falls_back_keyword(self) -> None: def test_comment_sentiment_all_scores_missing_falls_back_keyword(self) -> None:
root = Path(settings.CRAWLER_JD_ROOT).resolve()
if str(root) not in sys.path:
sys.path.insert(0, str(root))
import jd_competitor_report as jcr # noqa: WPS433
texts = ["好吃推荐", "差评"] texts = ["好吃推荐", "差评"]
scores = [None, None] scores = [None, None]
lex = jcr._comment_sentiment_lexicon(texts, scores) lex = jcr._comment_sentiment_lexicon(texts, scores)
self.assertEqual(lex.get("method"), "keyword_lexicon") self.assertEqual(lex.get("method"), "keyword_lexicon")
def test_custom_focus_words_in_report_config(self) -> None: def test_custom_focus_words_in_report_config(self) -> None:
root = Path(settings.CRAWLER_JD_ROOT).resolve()
if str(root) not in sys.path:
sys.path.insert(0, str(root))
import jd_competitor_report as jcr # noqa: WPS433
with tempfile.TemporaryDirectory() as td: with tempfile.TemporaryDirectory() as td:
run_dir = Path(td) run_dir = Path(td)
(run_dir / "pc_search_raw").mkdir(parents=True) (run_dir / "pc_search_raw").mkdir(parents=True)
@ -115,11 +89,6 @@ class BuildCompetitorBriefTests(SimpleTestCase):
self.assertIn("自定义词阿尔法", words) self.assertIn("自定义词阿尔法", words)
def test_matrix_groups_require_detail_category_path(self) -> None: def test_matrix_groups_require_detail_category_path(self) -> None:
root = Path(settings.CRAWLER_JD_ROOT).resolve()
if str(root) not in sys.path:
sys.path.insert(0, str(root))
import jd_competitor_report as jcr # noqa: WPS433
sku_h = "SKU(skuId)" sku_h = "SKU(skuId)"
merged = [ merged = [
{ {
@ -149,11 +118,6 @@ class BuildCompetitorBriefTests(SimpleTestCase):
def test_comment_lines_with_product_context_prefix(self) -> None: def test_comment_lines_with_product_context_prefix(self) -> None:
"""评价抽样须带细类/SKU/品名前缀,便于归因。""" """评价抽样须带细类/SKU/品名前缀,便于归因。"""
root = Path(settings.CRAWLER_JD_ROOT).resolve()
if str(root) not in sys.path:
sys.path.insert(0, str(root))
import jd_competitor_report as jcr # noqa: WPS433
sku_h = "SKU(skuId)" sku_h = "SKU(skuId)"
title_h = "标题(wareName)" title_h = "标题(wareName)"
merged = [ merged = [
@ -179,11 +143,6 @@ class BuildCompetitorBriefTests(SimpleTestCase):
self.assertIn("整体口感还差点意思", lines[0]) self.assertIn("整体口感还差点意思", lines[0])
def test_scenario_groups_llm_payload_matches_section_8_4_counts(self) -> None: def test_scenario_groups_llm_payload_matches_section_8_4_counts(self) -> None:
root = Path(settings.CRAWLER_JD_ROOT).resolve()
if str(root) not in sys.path:
sys.path.insert(0, str(root))
import jd_competitor_report as jcr # noqa: WPS433
sku_h = "SKU(skuId)" sku_h = "SKU(skuId)"
merged = [ merged = [
{ {
@ -226,11 +185,6 @@ class BuildCompetitorBriefTests(SimpleTestCase):
def test_mix_top_remainder_sums_to_all_rows(self) -> None: def test_mix_top_remainder_sums_to_all_rows(self) -> None:
"""mix_top 各 count 之和须等于 strip 后可统计行数(与扇图同源)。""" """mix_top 各 count 之和须等于 strip 后可统计行数(与扇图同源)。"""
root = Path(settings.CRAWLER_JD_ROOT).resolve()
if str(root) not in sys.path:
sys.path.insert(0, str(root))
import jd_competitor_report as jcr # noqa: WPS433
names = [f"{i}" for i in range(30)] names = [f"{i}" for i in range(30)]
mix = jcr._counter_mix_top_rows_with_remainder( mix = jcr._counter_mix_top_rows_with_remainder(
names, top_n=24, remainder_label="(其余店铺)" names, top_n=24, remainder_label="(其余店铺)"