301 lines
14 KiB
Python
301 lines
14 KiB
Python
|
|
"""
|
|||
|
|
最终版二创文章相似度检测器
|
|||
|
|
精确调整的算法
|
|||
|
|
"""
|
|||
|
|
import re
|
|||
|
|
import jieba
|
|||
|
|
from typing import Dict, List
|
|||
|
|
import difflib
|
|||
|
|
|
|||
|
|
|
|||
|
|
class FinalPlagiarismDetector:
|
|||
|
|
"""最终版相似度检测器"""
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
"""初始化检测器"""
|
|||
|
|
self.stopwords = {
|
|||
|
|
'的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', '个',
|
|||
|
|
'上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好',
|
|||
|
|
'自己', '这', '那', '里', '后', '以', '所', '如果', '但是', '因为', '所以'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def analyze_similarity(self, original_text: str, modified_text: str) -> Dict:
|
|||
|
|
"""
|
|||
|
|
分析两篇文章的相似度
|
|||
|
|
"""
|
|||
|
|
# 预处理
|
|||
|
|
clean_original = self._preprocess(original_text)
|
|||
|
|
clean_modified = self._preprocess(modified_text)
|
|||
|
|
|
|||
|
|
# 基础文本相似度
|
|||
|
|
text_similarity = self._calculate_text_similarity(clean_original, clean_modified)
|
|||
|
|
|
|||
|
|
# 关键词相似度
|
|||
|
|
keyword_similarity = self._calculate_keyword_similarity(clean_original, clean_modified)
|
|||
|
|
|
|||
|
|
# 人物相似度
|
|||
|
|
character_similarity = self._calculate_character_similarity(clean_original, clean_modified)
|
|||
|
|
|
|||
|
|
# 情节相似度
|
|||
|
|
plot_similarity = self._calculate_plot_similarity(clean_original, clean_modified)
|
|||
|
|
|
|||
|
|
# 综合相似度(更严格的计算)
|
|||
|
|
dimensions = {
|
|||
|
|
'text': text_similarity,
|
|||
|
|
'keyword': keyword_similarity,
|
|||
|
|
'character': character_similarity,
|
|||
|
|
'plot': plot_similarity
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 权重调整
|
|||
|
|
weights = {'text': 0.4, 'keyword': 0.3, 'character': 0.2, 'plot': 0.1}
|
|||
|
|
overall = sum(dimensions[dim] * weights[dim] for dim in dimensions)
|
|||
|
|
|
|||
|
|
# 应用严格阈值
|
|||
|
|
overall = self._apply_strict_threshold(overall, dimensions)
|
|||
|
|
|
|||
|
|
# 剽窃等级
|
|||
|
|
level = self._determine_plagiarism_level(overall)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
'overall_similarity': overall,
|
|||
|
|
'plagiarism_level': level,
|
|||
|
|
'dimensions': dimensions,
|
|||
|
|
'detailed_analysis': self._generate_analysis(dimensions)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def _preprocess(self, text: str) -> str:
|
|||
|
|
"""预处理文本"""
|
|||
|
|
text = re.sub(r'\s+', ' ', text)
|
|||
|
|
return text.strip()
|
|||
|
|
|
|||
|
|
def _calculate_text_similarity(self, text1: str, text2: str) -> float:
|
|||
|
|
"""计算文本基础相似度"""
|
|||
|
|
if not text1 or not text2:
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
# 使用SequenceMatcher计算整体相似度
|
|||
|
|
matcher = difflib.SequenceMatcher(None, text1, text2)
|
|||
|
|
base_similarity = matcher.ratio()
|
|||
|
|
|
|||
|
|
# 提取公共子串
|
|||
|
|
common_substrings = self._find_common_substrings(text1, text2)
|
|||
|
|
substring_bonus = len(common_substrings) * 0.02 # 每个公共子串加2%
|
|||
|
|
|
|||
|
|
return min(1.0, base_similarity + substring_bonus)
|
|||
|
|
|
|||
|
|
def _find_common_substrings(self, text1: str, text2: str) -> List[str]:
|
|||
|
|
"""查找公共子串"""
|
|||
|
|
substrings = []
|
|||
|
|
# 查找长度大于5的公共子串
|
|||
|
|
for i in range(len(text1) - 5):
|
|||
|
|
for j in range(i + 5, len(text1)):
|
|||
|
|
substring = text1[i:j]
|
|||
|
|
if substring in text2 and len(substring) > 5:
|
|||
|
|
substrings.append(substring)
|
|||
|
|
return substrings
|
|||
|
|
|
|||
|
|
def _calculate_keyword_similarity(self, text1: str, text2: str) -> float:
|
|||
|
|
"""计算关键词相似度"""
|
|||
|
|
keywords1 = self._extract_keywords(text1)
|
|||
|
|
keywords2 = self._extract_keywords(text2)
|
|||
|
|
|
|||
|
|
if not keywords1 and not keywords2:
|
|||
|
|
return 1.0
|
|||
|
|
if not keywords1 or not keywords2:
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
# 计算交集和并集
|
|||
|
|
intersection = set(keywords1) & set(keywords2)
|
|||
|
|
union = set(keywords1) | set(keywords2)
|
|||
|
|
|
|||
|
|
# Jaccard系数
|
|||
|
|
jaccard = len(intersection) / len(union) if union else 0.0
|
|||
|
|
|
|||
|
|
# 加权计算(考虑出现频率)
|
|||
|
|
weight1 = len(intersection) / len(keywords1) if keywords1 else 0
|
|||
|
|
weight2 = len(intersection) / len(keywords2) if keywords2 else 0
|
|||
|
|
|
|||
|
|
return (jaccard * 0.6 + (weight1 + weight2) * 0.2)
|
|||
|
|
|
|||
|
|
def _extract_keywords(self, text: str) -> List[str]:
|
|||
|
|
"""提取关键词"""
|
|||
|
|
words = jieba.cut(text)
|
|||
|
|
keywords = []
|
|||
|
|
for w in words:
|
|||
|
|
if len(w) > 1 and w not in self.stopwords and not w.isdigit():
|
|||
|
|
keywords.append(w)
|
|||
|
|
return keywords
|
|||
|
|
|
|||
|
|
def _calculate_character_similarity(self, text1: str, text2: str) -> float:
|
|||
|
|
"""计算人物相似度"""
|
|||
|
|
chars1 = self._find_names(text1)
|
|||
|
|
chars2 = self._find_names(text2)
|
|||
|
|
|
|||
|
|
if not chars1 and not chars2:
|
|||
|
|
return 1.0
|
|||
|
|
if not chars1 or not chars2:
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
# 计算人物匹配度
|
|||
|
|
common_chars = set(chars1) & set(chars2)
|
|||
|
|
total_chars = set(chars1) | set(chars2)
|
|||
|
|
|
|||
|
|
char_similarity = len(common_chars) / len(total_chars) if total_chars else 0.0
|
|||
|
|
|
|||
|
|
# 如果人物完全不同,相似度为0
|
|||
|
|
if not common_chars:
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
return char_similarity
|
|||
|
|
|
|||
|
|
def _find_names(self, text: str) -> List[str]:
|
|||
|
|
"""提取人名"""
|
|||
|
|
# 简化的中文姓名识别
|
|||
|
|
pattern = r'[王李张刘陈杨赵黄周吴徐孙胡朱高林何郭马罗梁宋郑谢韩唐冯于董萧程曹袁邓许傅沈曾彭吕苏卢蒋蔡贾丁魏薛叶阎余潘杜戴夏锺汪田任姜范方石姚谭廖邹熊金陆郝孔白崔康毛邱秦江史顾侯邵孟龙万段漕钱汤尹黎易常武乔贺赖龚文][一-龥]{1,3}'
|
|||
|
|
names = re.findall(pattern, text)
|
|||
|
|
return list(set(names))
|
|||
|
|
|
|||
|
|
def _calculate_plot_similarity(self, text1: str, text2: str) -> float:
|
|||
|
|
"""计算情节相似度"""
|
|||
|
|
events1 = self._extract_events(text1)
|
|||
|
|
events2 = self._extract_events(text2)
|
|||
|
|
|
|||
|
|
if not events1 and not events2:
|
|||
|
|
return 1.0
|
|||
|
|
if not events1 or not events2:
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
# 计算事件匹配度
|
|||
|
|
common_events = 0
|
|||
|
|
for event1 in events1:
|
|||
|
|
for event2 in events2:
|
|||
|
|
# 如果事件描述相似
|
|||
|
|
if self._events_similar(event1, event2):
|
|||
|
|
common_events += 1
|
|||
|
|
|
|||
|
|
max_events = max(len(events1), len(events2))
|
|||
|
|
return common_events / max_events if max_events > 0 else 0.0
|
|||
|
|
|
|||
|
|
def _extract_events(self, text: str) -> List[str]:
|
|||
|
|
"""提取关键事件"""
|
|||
|
|
# 提取包含关键动作的句子
|
|||
|
|
action_patterns = [
|
|||
|
|
r'([^。!?.]*发现[^。!?.]*[。!?.])',
|
|||
|
|
r'([^。!?.]*决定[^。!?.]*[。!?.])',
|
|||
|
|
r'([^。!?.]*帮助[^。!?.]*[。!?.])',
|
|||
|
|
r'([^。!?.]*发生[^。!?.]*[。!?.])'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
events = []
|
|||
|
|
for pattern in action_patterns:
|
|||
|
|
matches = re.findall(pattern, text)
|
|||
|
|
events.extend(matches)
|
|||
|
|
|
|||
|
|
return events
|
|||
|
|
|
|||
|
|
def _events_similar(self, event1: str, event2: str) -> bool:
|
|||
|
|
"""判断两个事件是否相似"""
|
|||
|
|
# 简单的相似度判断
|
|||
|
|
words1 = set(jieba.cut(event1))
|
|||
|
|
words2 = set(jieba.cut(event2))
|
|||
|
|
|
|||
|
|
common_words = words1 & words2
|
|||
|
|
total_words = words1 | words2
|
|||
|
|
|
|||
|
|
if not total_words:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
similarity = len(common_words) / len(total_words)
|
|||
|
|
return similarity > 0.6 # 相似度超过60%认为相似
|
|||
|
|
|
|||
|
|
def _apply_strict_threshold(self, similarity: float, dimensions: Dict) -> float:
|
|||
|
|
"""应用严格阈值"""
|
|||
|
|
# 如果文本本身相似度很低,大幅降低总分
|
|||
|
|
if dimensions['text'] < 0.3:
|
|||
|
|
similarity *= 0.3
|
|||
|
|
elif dimensions['text'] < 0.5:
|
|||
|
|
similarity *= 0.6
|
|||
|
|
|
|||
|
|
# 如果关键词相似度很低,进一步降低
|
|||
|
|
if dimensions['keyword'] < 0.2:
|
|||
|
|
similarity *= 0.4
|
|||
|
|
|
|||
|
|
# 如果人物完全不同且情节也不同,大幅降低
|
|||
|
|
if dimensions['character'] == 0.0 and dimensions['plot'] < 0.2:
|
|||
|
|
similarity *= 0.2
|
|||
|
|
|
|||
|
|
return similarity
|
|||
|
|
|
|||
|
|
def _determine_plagiarism_level(self, similarity: float) -> str:
|
|||
|
|
"""判断剽窃等级"""
|
|||
|
|
if similarity < 0.05: # 5%
|
|||
|
|
return "原创"
|
|||
|
|
elif similarity < 0.30: # 30%
|
|||
|
|
return "抄袭(轻微)"
|
|||
|
|
else:
|
|||
|
|
return "剽窃(严重)"
|
|||
|
|
|
|||
|
|
def _generate_analysis(self, dimensions: Dict) -> str:
|
|||
|
|
"""生成详细分析"""
|
|||
|
|
analysis = "各维度相似度分析:\n"
|
|||
|
|
dim_names = {
|
|||
|
|
'text': '文本相似度',
|
|||
|
|
'keyword': '关键词相似度',
|
|||
|
|
'character': '人物相似度',
|
|||
|
|
'plot': '情节相似度'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for dim, score in dimensions.items():
|
|||
|
|
analysis += f" - {dim_names[dim]}: {score:.2%}\n"
|
|||
|
|
|
|||
|
|
return analysis
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 便捷函数
|
|||
|
|
def detect_plagiarism(original: str, modified: str) -> Dict:
|
|||
|
|
"""检测二创文章与原文的相似度"""
|
|||
|
|
detector = FinalPlagiarismDetector()
|
|||
|
|
return detector.analyze_similarity(original, modified)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
# 测试
|
|||
|
|
detector = FinalPlagiarismDetector()
|
|||
|
|
|
|||
|
|
original = """
|
|||
|
|
12 月 22 日晚,“天王嫂” 方媛在个人社交平台晒出视频分享近况。从方媛分享来看,生完三胎女儿的她在月子中心坐满 42 天月子后已经回到郭富城在香港的半山别墅。
|
|||
|
|
回家后的方媛不用操心带娃问题,毕竟娘家父母都在身边帮忙照顾孩子还有阿姨,那作为三个孩子妈妈的方媛还是很清闲的。
|
|||
|
|
别墅里已经布置好圣诞树满满节日气氛。视频中身着瑜伽服出镜的方媛,身材曲线明显,完全看不出是刚生完孩子的人。
|
|||
|
|
对于自己现在的状态,方媛本人还是比较满意的,她不忘分享自己产后保持好状态的妙招。方媛透露别看她现在肚子平坦,腰身纤细,但恢复到这种状态也是需要一定时间,而为了让肚子尽快收回去,生完三胎后的方媛有绑收腹带,她透露不仅白天绑,晚上偶尔也会绑,还有就是搭配好饮食以及运动了。
|
|||
|
|
方媛还晒出了在家带娃画面,只见方媛半蹲在婴儿车旁,一脸宠溺看向躺在车上的小女儿。在家带娃的方媛并没有化妆,素颜状态下的她气色很不错,而且,皮肤白净好水灵,好美的妈咪。
|
|||
|
|
除了肚子外,还有宝妈询问方媛生完孩子后胯部如何回到原来的状态,对此,方媛有回应。她直言胯部不如肚子好恢复,她前两胎都是半年至一年才回去的。
|
|||
|
|
再看方媛生完三胎后的近照,身着牛仔裤的她胯部很明显还没收回去,对比之前状态要宽很多,
|
|||
|
|
当然,37 岁生下三胎已经是高龄产妇的方媛,产后恢复的可以说是相当不错了,她现在体重看着都没 100 斤,作为三个孩子的妈妈,这体重属实令人羡慕。
|
|||
|
|
产后恢复的超好的方媛,她已经复工。就在今日,方媛还晒出和温碧霞身着古装一起走秀合照,两个人都好仙好美,网友看后纷纷直呼:“简直就是一场视觉盛宴啊!”
|
|||
|
|
和郭富城育有三女的方媛,三个女儿虽说都没露过全脸,但从女儿们露出的五官来看有遗传到爸爸妈妈的高颜值,有三个漂亮女儿的郭富城方媛也是很令人羡慕了
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
modified = """
|
|||
|
|
俗话说得好,想要人前风光,就得人后吃苦。
|
|||
|
|
12 月 22 号晚上,方媛发了个视频,我看完直接愣住了,这是三个娃的妈,生完三胎才 42 天,月子刚坐完,人就回到半山别墅了,视频里圣诞树挂满了灯,节日味道很浓,但我眼睛一直盯着她看。
|
|||
|
|
说真的,这恢复速度太吓人了,她穿着紧身瑜伽服站在那儿,肚子平平的,一点肉都没有,腰细得不像话,哪像刚生完孩子的人,这身材管理,我服了。
|
|||
|
|
不过方媛挺实在,没装,她自己说了,这身材不是白来的,为了收肚子,她下了狠劲,白天绑收腹带,晚上有时候也不敢摘,还得管住嘴迈开腿,听着就觉得难受。
|
|||
|
|
说实话,她这日子确实让人羡慕,回家不用操心带娃,娘家爸妈都在,还有阿姨帮忙,她就负责美美地逗逗孩子,这才是阔太太的生活,视频里她蹲在婴儿车旁边,看着小闺女,眼神温柔得很。
|
|||
|
|
那天她没化妆,素颜出镜,脸干干净净的,皮肤白得发亮,气色特别好,一点都不累,虽然已经 37 岁了,但状态比二十多岁的姑娘还好,这月子坐得真到位。
|
|||
|
|
但仔细看,还是能看出点东西,肚子是平了,可胯比以前宽了不少,她穿牛仔裤的时候,线条很明显,下半身有点妈妈的样子了,这就是生孩子的代价,骨盆开了,一时半会回不去。
|
|||
|
|
方媛也没躲着,大方承认了,她说肚子好收,胯是老大难,没个半年一年回不去,前两胎也是这么熬过来的,这话听着心酸,当妈的都不容易,就算有钱,身体的苦也得自己扛。
|
|||
|
|
现在的方媛,估计不到一百斤,看着她细胳膊细腿,我都替她担心,太瘦了,不过人家是真拼,刚出月子就开始干活了,这就叫比你有钱的人比你还努力,咱还有啥理由躺着。
|
|||
|
|
就在今天,她还去走了个秀,跟温碧霞一起,俩人都穿古装,那场面太好看了,方媛一出来,又仙又大气,网友都说是视觉盛宴,评价很高。
|
|||
|
|
三个闺女都没露全脸,但基因骗不了人,看那露出来的五官,个个都漂亮,像爹也像妈,郭富城家以后就是四个大美女,想想那画面,老郭这辈子值了。
|
|||
|
|
不得不说,方媛挺有韧劲的,从小网红到天王嫂,靠的不光是脸,三年生俩,现在又是三胎,肚子没怎么歇过,关键人家还能保持状态,该美美该工作工作,这才是狠人。
|
|||
|
|
咱们普通人看个热闹就行,没必要跟人家比,人家有保姆有营养师,咱比不了,但她那股劲儿,对自己的严格,倒是值得学学,生活是自己的,好坏都得自己扛。
|
|||
|
|
女人这辈子,不管嫁给谁,爱自己才是真的,别拿孩子当借口,放弃对美的追求,你看方媛,绑着带子也要美,这就是态度,不管多大岁数,不管生几个娃,精气神不能丢,日子过得好不好,全写在脸上,愿每个妈妈,都能活成自己想要的样子。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
result = detector.analyze_similarity(original, modified)
|
|||
|
|
print(f"综合相似度: {result['overall_similarity']:.2%}")
|
|||
|
|
print(f"剽窃等级: {result['plagiarism_level']}")
|
|||
|
|
print(result['detailed_analysis'])
|