amz_review_analyse/词频_jieba.py
OnesvmWhoops 2441c9119c Initial commit: VOC LLM 结构化分析流水线
包含七步编排入口、结构化/向量化/聚类/词频/报告模块与 prompts 配置;忽略原始 CSV 与本地密钥。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 15:40:13 +08:00

182 lines
4.9 KiB
Python
Raw 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.

"""
词频统计(jieba 分词版):流程与 词频.py 相同,第 2 步改用 jieba 分词。
1. 随机样本 → LLM 归纳专有名词与停用词(与 spaCy 版共用 voc_terms.json)
2. jieba 全量 content 分词 + 词频 → output/word_freq.csv
用法::
python3 词频_jieba.py
python3 词频_jieba.py --skip-llm
"""
from __future__ import annotations
import argparse
import json
import logging
import re
import sys
from collections import Counter
from pathlib import Path
from typing import List, Sequence, Set, Tuple
from 词频 import (
OUTPUT_DIR,
SAMPLE_SEED,
SAMPLE_SIZE,
STRUCTURED_DB,
TERMS_JSON,
WORD_FREQ_CSV,
_apply_product_terms,
_finalize_term_lists,
_is_pure_number,
_latest_job,
_load_contents,
_load_spacy,
_load_terms_json,
_merge_word_forms,
_overlaps_span,
_resolve_api_key,
_resolve_source_path,
_sample_reviews,
_save_word_freq,
_step1_extract_terms,
)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
stream=sys.stderr,
)
logger = logging.getLogger("voc_wordfreq_jieba")
# 英文词或中文词均保留
_TOKEN_OK = re.compile(r"[a-z\u4e00-\u9fff]", re.I)
def _prepare_jieba(product_terms: Sequence[str]) -> None:
import jieba
for term in product_terms:
t = (term or "").strip()
if t:
jieba.add_word(t)
def _tokenize_jieba(
text: str,
protected_spans: Sequence[Tuple[int, int]],
stop_set: Set[str],
) -> List[str]:
import jieba
tokens: List[str] = []
for word, start, end in jieba.tokenize(text):
if _overlaps_span(start, end, protected_spans):
continue
w = word.strip().lower()
if not w or _is_pure_number(w) or w in stop_set:
continue
if not _TOKEN_OK.search(w):
continue
tokens.append(w)
return tokens
def _build_word_freq(
rows: Sequence[Tuple[int, str]],
product_terms: List[str],
stop_set: Set[str],
) -> Counter[str]:
_prepare_jieba(product_terms)
counter: Counter[str] = Counter()
for _, text in rows:
if not text.strip():
continue
lower = text.lower()
protected = _apply_product_terms(lower, product_terms, counter, stop_set)
for tok in _tokenize_jieba(text, protected, stop_set):
counter[tok] += 1
counter = Counter({k: v for k, v in counter.items() if not _is_pure_number(k)})
nlp = _load_spacy()
return _merge_word_forms(counter, nlp)
def run(*, skip_llm: bool = False) -> dict:
if not STRUCTURED_DB.is_file():
raise FileNotFoundError(f"缺少 {STRUCTURED_DB}")
import sqlite3
conn = sqlite3.connect(STRUCTURED_DB)
try:
job_id, industry, product_name, source_file = _latest_job(conn)
finally:
conn.close()
csv_path = _resolve_source_path(source_file)
rows = _load_contents(csv_path)
logger.info(
"job_id=%s product=%r 评论 %s 条,来源 %s(jieba 分词)",
job_id,
product_name,
len(rows),
csv_path.name,
)
if skip_llm:
meta = _load_terms_json()
product_terms, stop_set = _finalize_term_lists(
list(meta.get("product_terms") or []),
list(meta.get("stopwords_custom") or []),
product_name,
)
logger.info("第 1 步跳过,复用 %s", TERMS_JSON)
else:
api_key = _resolve_api_key()
if not api_key:
raise RuntimeError("缺少 DASHSCOPE_API_KEY 或 .dashscope_key")
samples = _sample_reviews(rows, n=SAMPLE_SIZE, seed=SAMPLE_SEED)
meta = _step1_extract_terms(
job_id=job_id,
industry=industry,
product_name=product_name,
samples=samples,
api_key=api_key,
)
product_terms, stop_set = _finalize_term_lists(
meta["product_terms"],
meta.get("stopwords_custom") or [],
product_name,
)
logger.info("第 2 步:jieba 全量分词与词频统计")
counter = _build_word_freq(rows, product_terms, stop_set)
_save_word_freq(counter, WORD_FREQ_CSV)
logger.info("已写入 %s", WORD_FREQ_CSV)
return {
"job_id": job_id,
"product_name": product_name,
"tokenizer": "jieba",
"terms_json": str(TERMS_JSON),
"word_freq_csv": str(WORD_FREQ_CSV),
"unique_words": len(counter),
"total_tokens": sum(counter.values()),
}
def main() -> None:
parser = argparse.ArgumentParser(description="VOC 词频统计(jieba 分词)")
parser.add_argument(
"--skip-llm",
action="store_true",
help="跳过第 1 步,使用 output/voc_terms.json",
)
args = parser.parse_args()
result = run(skip_llm=args.skip_llm)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()