包含七步编排入口、结构化/向量化/聚类/词频/报告模块与 prompts 配置;忽略原始 CSV 与本地密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
182 lines
4.9 KiB
Python
182 lines
4.9 KiB
Python
"""
|
||
词频统计(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()
|