如何使用spaCy在Python中标准化食谱数据集的食材名称并处理NOUN+NOUN结构
如何使用spaCy在Python中标准化食谱数据集的食材名称并处理NOUN+NOUN结构
我完全理解你的困扰——处理像broccoli florets这种名词叠加的食材名称时,单独遍历token很容易漏掉前面的修饰性名词,而且硬加特殊case确实不够优雅。咱们可以从spaCy的依赖分析入手,利用它的语法树信息来更准确地捕捉完整的食材术语,而不是只处理单个token。
问题根源分析
你之前的代码主要有两个潜在问题:
- 缩进错误:看你提供的第一个函数,检查词性的
if语句缩进位置不对,导致部分token的处理逻辑根本没执行(比如broccoli这个token可能根本没走到词性判断的分支)。 - 未利用依赖关系:像
broccoli florets里,broccoli是florets的compound(复合名词修饰语),但你的代码只单独处理每个token,没有把这类关联的名词组合起来,所以才会只留下floret。
改进后的解决方案
核心思路是:过滤掉无关的测量单位、数字后,通过spaCy的依赖树找到食材的中心名词,再把所有修饰它的复合名词、形容词都组合起来,这样就能完整提取像broccoli florets、chicken broth这类结构的食材名称。
def normalize_ingredient(ingredient_text): doc = nlp(ingredient_text) # 去重后的测量单位集合 measurement_units = { "cup", "cups", "teaspoon", "tablespoon", "tablespoons", "gram", "ounce", "pound", "can", "clove", "pinch", "dash", "quart", "liter", "milliliter", "gallon", "stick", "rib", "head", "package", "inch", "piece", "fluid", "container", "jar", "loaf", "bottle", "pack", "pint", "cube", "stalk", "slice", "bulb", "strip", "packet", "envelope", "box", "bag", "carton", "sprig", "leaf", "half", "purpose", "bite", "size", "bunch", "all", "sized", "chunks" } # 分数与特殊符号集合 fractions = {'½', '¼', '¾', '⅓', '⅔', '⅛', '⅜', '⅝', '⅞', '⅙', '⅚', '®'} # 用集合存储食材术语,自动去重 ingredient_terms = set() for token in doc: # 跳过无关内容:数字、分数、测量单位、标点、停用词 if (token.like_num or token.text in fractions or token.lemma_.lower() in measurement_units or token.is_punct or token.is_stop): continue # 处理名词/专有名词,收集所有复合修饰语 if token.pos_ in {"NOUN", "PROPN"}: compound_terms = [] # 找到当前名词的所有compound修饰语(比如broccoli是florets的compound) for child in token.children: if child.dep_ == "compound" and child.pos_ in {"NOUN", "PROPN", "ADJ"}: compound_terms.append(child.lemma_.lower()) # 组合修饰语与中心名词 full_term = " ".join(compound_terms + [token.lemma_.lower()]) ingredient_terms.add(full_term) # 处理修饰名词的形容词(比如"fresh garlic"里的fresh) elif token.pos_ == "ADJ" and token.head.pos_ in {"NOUN", "PROPN"}: # 确保被修饰的名词没有被过滤掉 if not (token.head.like_num or token.head.text in fractions or token.head.lemma_.lower() in measurement_units): full_term = f"{token.lemma_.lower()} {token.head.lemma_.lower()}" ingredient_terms.add(full_term) # 处理结果:优先保留最长的完整术语(避免同时出现"floret"和"broccoli floret") if not ingredient_terms: return "" sorted_terms = sorted(ingredient_terms, key=len, reverse=True) # 移除被包含的短术语 final_terms = [] for term in sorted_terms: if not any(term in existing_term for existing_term in final_terms): final_terms.append(term) return " ".join(final_terms)
代码功能说明
- 过滤逻辑强化:新增了对标点、停用词的过滤,减少无关噪声。
- 依赖关系利用:通过
token.children找到compound类型的修饰语,把broccoli和florets组合成完整的broccoli floret(lemma形式,利于标准化)。 - 结果去重与优化:按术语长度排序,只保留最完整的食材名称,避免出现冗余的短术语。
- 形容词处理:正确组合像
fresh garlic这类形容词+名词的结构。
测试效果
- 输入:
"8 cups broccoli florets"→ 输出:"broccoli floret" - 输入:
"3 cups chicken broth"→ 输出:"chicken broth" - 输入:
"5 tablespoons butter, divided"→ 输出:"butter"
额外优化建议
- 如果需要保留复数形式(比如
florets而不是floret),可以把token.lemma_.lower()改成token.text.lower(),但lemma形式更利于跨数据的标准化。 - 可以扩展依赖类型,比如加入
amod(形容词修饰)、nn(旧版spaCy的名词修饰)等,覆盖更多场景。 - 对于像
divided、chopped这类描述性加工词汇,可以新增一个排除集合,比如exclude_terms = {"divided", "chopped", "diced"},在过滤时加入判断。
备注:内容来源于stack exchange,提问作者Островська Катя
相关产品推荐
相关产品推荐

