hub-gif ca6e71eb4b refactor(pipeline): 整理 csv、jd 细类标签与竞品报告子包
- 新增 pipeline.csv 与 pipeline.jd.matrix_group_label,根目录模块保留兼容转发

- 竞品报告主实现迁至 competitor_report.jd_report,兼容入口补全下划线辅助函数再导出

- 增加 pipeline/__init__.py 避免 Django 将 pipeline 识别为多路径命名空间包

Made-with: Cursor
2026-04-17 14:14:48 +08:00

269 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
与 ``jd_pc_search`` 导出 CSV 列对齐的字段名映射(入库 / API / 导出共用)。
内部键与爬虫侧 ``JD_ITEM_CSV_FIELDS`` / ``WARE_PARSED_CSV_FIELDNAMES`` 一致,便于对照源码。
"""
from __future__ import annotations
import re
def strip_buyer_ranking_line_prefix(value: str) -> str:
"""去掉榜单列上的 ``榜单/曝光:`` 历史前缀,展示与入库一致。"""
s = (value or "").strip()
prefix = "榜单/曝光:"
if s.startswith(prefix):
return s[len(prefix) :].strip()
if s.startswith("榜单/曝光"):
return s[len("榜单/曝光") :].lstrip("").strip()
return s
# --- 搜索导出 pc_search_export.csv纯中文表头与 jd_h5_search_requests.JD_EXPORT_COLUMN_HEADERS 一致)---
JD_SEARCH_INTERNAL_KEYS: tuple[str, ...] = (
"item_id",
"sku_id",
"title",
"price",
"coupon_price",
"original_price",
"selling_point",
"comment_sales_floor",
"total_sales",
"hot_list_rank",
"comment_count",
"shop_name",
"shop_url",
"shop_info_url",
"location",
"detail_url",
"image",
"seckill_info",
"attributes",
"leaf_category",
"platform",
"keyword",
"page",
)
JD_SEARCH_CSV_HEADERS: dict[str, str] = {
"item_id": "主商品ID",
"sku_id": "SKU",
"title": "标题",
"price": "标价",
"coupon_price": "券后到手价",
"original_price": "原价",
"selling_point": "卖点",
"comment_sales_floor": "销量楼层",
"total_sales": "销量展示",
"hot_list_rank": "榜单类文案",
"comment_count": "评价量",
"shop_name": "店铺名",
"shop_url": "店铺链接",
"shop_info_url": "店铺信息链接",
"location": "地域",
"detail_url": "商品链接",
"image": "主图",
"seckill_info": "秒杀",
"attributes": "规格属性",
"leaf_category": "类目",
"platform": "平台",
"keyword": "搜索词",
"page": "页码",
}
# CSV 表头 -> 模型属性名
SEARCH_CSV_HEADER_TO_FIELD: dict[str, str] = {
h: k for k, h in JD_SEARCH_CSV_HEADERS.items()
}
# lean 商详ORM 内部键(英文 snake_caseCSV 表头为中文(见 DETAIL_CSV_COLUMNS
MERGED_LEAN_DETAIL_INTERNAL_KEYS: tuple[str, ...] = (
"detail_brand",
"detail_price_final",
"detail_shop_name",
"detail_category_path",
"detail_product_attributes",
"detail_body_ingredients",
"buyer_ranking_line",
"buyer_promo_text",
)
LEAN_DETAIL_EXPORT_FIELDNAMES: tuple[str, ...] = MERGED_LEAN_DETAIL_INTERNAL_KEYS
LEAN_DETAIL_CSV_HEADERS: tuple[str, ...] = (
"品牌",
"到手价",
"店铺名称",
"类目路径",
"商品参数",
"配料表",
"榜单排名",
"促销摘要",
)
DETAIL_CSV_HEADER_TO_FIELD: dict[str, str] = dict(
zip(LEAN_DETAIL_CSV_HEADERS, MERGED_LEAN_DETAIL_INTERNAL_KEYS)
)
# --- 商详 detail_ware_export.csvleanSKU + 上列full 模式爬虫仍可能多列,入库只认 DETAIL_CSV_COLUMNS---
JD_DETAIL_MERGE_KEYS: tuple[str, ...] = MERGED_LEAN_DETAIL_INTERNAL_KEYS
DETAIL_CSV_COLUMNS: tuple[str, ...] = ("SKU", *LEAN_DETAIL_CSV_HEADERS)
DETAIL_CSV_TO_FIELD: dict[str, str] = {
"SKU": "sku_id",
**DETAIL_CSV_HEADER_TO_FIELD,
}
# --- 评价 comments_flat.csv表头中文爬虫行字典仍用英文 API 键,写出时映射)---
COMMENT_CSV_COLUMNS: tuple[str, ...] = (
"SKU",
"评价ID",
"用户昵称",
"评价内容",
"评价时间",
"购买次数",
"晒图链接",
"评分",
)
COMMENT_CSV_TO_FIELD: dict[str, str] = {
"SKU": "sku_id",
"评价ID": "comment_id",
"用户昵称": "user_nick_name",
"评价内容": "tag_comment_content",
"评价时间": "comment_date",
"购买次数": "buy_count_text",
"晒图链接": "large_pic_urls",
"评分": "comment_score",
}
# 爬虫评价行 dict 键(与京东接口字段一致)→ CSV 中文表头
COMMENT_ROW_DICT_KEYS: tuple[str, ...] = (
"sku",
"commentId",
"userNickName",
"tagCommentContent",
"commentDate",
"buyCountText",
"largePicURLs",
"commentScore",
)
# --- 合并宽表 keyword_pipeline_merged.csvlean = 搜索块 + 商详块 + 评论块;改列请改对应块,勿在尾部堆列)---
MERGED_SEARCH_INTERNAL_KEYS: tuple[str, ...] = (
"pipeline_keyword",
"sku_id",
"ware_id",
"title",
"price",
"coupon_price",
"original_price",
"selling_point",
"hot_list_rank",
"comment_fuzzy",
"comment_sales_floor",
"total_sales",
"shop_name",
"detail_url",
"image",
"attributes",
"leaf_category",
"keyword",
"page",
)
def _merged_search_csv_header_for_internal(k: str) -> str:
"""整合表搜索块:内部键 → 中文 CSV 表头(与 PC 搜索导出列名对齐,合并表专有列单独处理)。"""
if k == "ware_id":
return JD_SEARCH_CSV_HEADERS["item_id"]
if k == "comment_fuzzy":
return "评价量"
return JD_SEARCH_CSV_HEADERS[k]
MERGED_SEARCH_CSV_COLUMNS: tuple[str, ...] = (
"流水线关键词",
*(
_merged_search_csv_header_for_internal(k)
for k in MERGED_SEARCH_INTERNAL_KEYS[1:]
),
)
# 商详块CSV 为中文表头;内部键见 MERGED_LEAN_DETAIL_INTERNAL_KEYS
MERGED_LEAN_DETAIL_KEYS: tuple[str, ...] = LEAN_DETAIL_CSV_HEADERS
MERGED_COMMENT_CSV_COLUMNS: tuple[str, ...] = (
"评论条数",
"评价摘要",
)
MERGED_COMMENT_INTERNAL_KEYS: tuple[str, ...] = (
"pipeline_comment_count",
"comment_preview",
)
MERGED_CSV_COLUMNS: tuple[str, ...] = (
*MERGED_SEARCH_CSV_COLUMNS,
*MERGED_LEAN_DETAIL_KEYS,
*MERGED_COMMENT_CSV_COLUMNS,
)
MERGED_INTERNAL_KEYS: tuple[str, ...] = (
*MERGED_SEARCH_INTERNAL_KEYS,
*MERGED_LEAN_DETAIL_INTERNAL_KEYS,
*MERGED_COMMENT_INTERNAL_KEYS,
)
assert len(MERGED_CSV_COLUMNS) == len(MERGED_INTERNAL_KEYS)
MERGED_CSV_TO_FIELD: dict[str, str] = dict(zip(MERGED_CSV_COLUMNS, MERGED_INTERNAL_KEYS))
MERGED_FIELD_TO_CSV_HEADER: dict[str, str] = {
internal: csv_h for csv_h, internal in MERGED_CSV_TO_FIELD.items()
}
def remap_merged_row_english_detail_keys_to_csv_headers(merged: dict[str, str]) -> None:
"""整合表写入前:将 ``ware_flat`` / 抽取逻辑产生的英文商详与购买者内部键改为中文 CSV 列名(原地修改)。"""
for ik, zh in zip(MERGED_LEAN_DETAIL_INTERNAL_KEYS, LEAN_DETAIL_CSV_HEADERS):
if ik in merged:
merged[zh] = str(merged.get(ik) or "")
del merged[ik]
def infer_total_sales_from_sales_floor(cell: str) -> str:
"""
从「销量楼层(commentSalesFloor)」列文案截取可作 ``销量展示(totalSales)`` 的片段(与列表接口未单独落 totalSales 列时的兜底一致)。
"""
t = (cell or "").strip()
if not t:
return ""
m = re.search(r"已售\s*[\d,.+]*\s*[万亿]?\s*\+?", t)
if m:
return m.group(0).strip()
m2 = re.search(r"已售\s*[\d,.+\s万千亿]+", t)
return m2.group(0).strip() if m2 else ""
def merged_csv_effective_total_sales(row: dict[str, str]) -> str:
"""合并表一行:优先已有 ``销量展示(totalSales)`` 列,否则从销量楼层推断。"""
h_ts = MERGED_FIELD_TO_CSV_HEADER["total_sales"]
h_fl = MERGED_FIELD_TO_CSV_HEADER["comment_sales_floor"]
direct = str(row.get(h_ts) or "").strip()
if direct:
return direct
return infer_total_sales_from_sales_floor(str(row.get(h_fl) or ""))
def search_csv_effective_total_sales(row: dict[str, str]) -> str:
"""PC 搜索导出表一行:与 ``merged_csv_effective_total_sales`` 解析规则一致(中文表头)。"""
h_ts = JD_SEARCH_CSV_HEADERS["total_sales"]
h_fl = JD_SEARCH_CSV_HEADERS["comment_sales_floor"]
direct = str(row.get(h_ts) or "").strip()
if direct:
return direct
return infer_total_sales_from_sales_floor(str(row.get(h_fl) or ""))