客户提供了公众号文章的永久链接,并在远程数据库中保存了原创的文章,要求采集目标公众号文章和原创文章有多少重复的,以便判定是否侵权。
程序设计
每天都有大几千的公众号文章url保存到远程数据库中,并要求及时统计近似值,原则上当前的url当前都要消化完毕,如果能在1个小时内消化更佳。大几千的url也不算什么,不过为了满足客户的时效性,打算使用三进程:
- 进程1 数据库交互
该进程负责和数据库交互,读取要爬取的url、采集到数据后写入数据库一级更新其他必要的数据 - 进程2 数据处理
请求url拿到数据后的数据结构组织、近似值计算以及保存到数据库之前的数据去重 - 进程3 url请求
该进程只负责请求页面拿数据,提取必要的信息,如文章标题、公众号名称和id、文章发布日期,以及是否原创和是否有作者、公众号二维码等信息
url请求
使用requests库即可,请求公众号文章不要求请求头和cookies,不要太简单,只需要注意
- 纯图的文章和纯视频的文章时,标题和内容样式的class和id与文字文章有所不同
- 二维码和文章发布时间是js加载的,需要从页面正则提取
文章发布时间 提取正则
re.compile('if\(!window.__second_open__\){e\(0,"(.*?)",0,document.getElementById\("publish_time"\)', re.X)
二维码url提取正则
# 二维码
def get_qrcode(self):
url_patter = re.compile('<meta property="og:url" content="(.*?)"')
r1 = re.findall(url_patter, self.text)
if r1:
url_groups = re.search('biz=(.*?)&mid=(.*?)&idx=(.*?)&sn=(.*?)&', self.text)
url = 'https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=200&__biz={0}&mid={1}&idx={2}&sn={3}'.format(url_groups.group(1), url_groups.group(2), url_groups.group(3), url_groups.group(4))
return url
此外,在url请求环节,还设计了单线程和多线程+是否使用代理的双模式,通过txt配置来控制,客户可以使用单线程、多线程,多线程时,还可以使用当前地址或者代理。
数据处理
没啥特别的,近似值算法上,使用了杰卡德算法:A和B交集/A和B的并集,不过客户强烈要求使用A和B的交集/B,其中A是原创文章,B是目标文章。贴一下代码:
class JaccardSimilarity(object):
"""
jaccard相似度
"""
def __init__(self, content_x1, content_y2):
self.s1 = content_x1 # 原文章
self.s2 = content_y2 # 目标文章
@staticmethod
def extract_keyword(content): # 提取关键词
# # 正则过滤 html 标签 忽略原html标签和转义
# re_exp = re.compile(r'(<style>.*?</style>)|(<[^>]+>)', re.S)
# content = re_exp.sub(' ', content)
# # html 转义符实体化
# content = html.unescape(content)
# 切割
seg = [i for i in jieba.cut(content, cut_all=True) if i != '']
# 提取关键词
keywords = jieba.analyse.extract_tags("|".join(seg), topK=200, withWeight=False)
return keywords
def main(self):
# 去除停用词
jieba.analyse.set_stop_words('stopwords.txt')
# 分词与关键词提取
keywords_x = self.extract_keyword(self.s1)
keywords_y = self.extract_keyword(self.s2)
# jaccard相似度计算
intersection = len(list(set(keywords_x).intersection(set(keywords_y))))
union = len(list(set(keywords_x).union(set(keywords_y))))
# union = len(list(set(keywords_x).union(set(keywords_y)))) # 原算法union为并集
# a = len(set(keywords_x))
b = len(set(keywords_y))
# 除零处理
# sim = float(intersection)/union if union != 0 else 0 # 原算法 xy交集/xy并集
sim = float(intersection)/b if b != 0 else 0
return sim
注:算法实现引用自https://blog.csdn.net/qq_42280510/article/details/102857696
这里又有点小问题,因为最终成程序要打包成exe,jieba模块在打包时依赖词典等文件,所以需要简单设置:
1、dict.txt依赖,jieba提供了设置词典路径的接口,直接使用。
先从jieba安装目录下找到dict.txt,拷贝到程序的当前目录,然后使用代码加载一次:
import jieba
jieba.set_dictionary('./dict.txt')
jieba.initialize()
2、idf.txt依赖
虽然官方也提供了接口jieba.analyse.set_idf_path()来设置idf.txt的路径,奈何怎么设置都不成功,也看了源码,analyse貌似也没有重载的入口,也有可能我找不到的原因。总之,最后通过修改源码的方式才实现了打包时文件的依赖。
步骤1 先找到:
class TFIDF(KeywordExtractor):
def __init__(self, idf_path='.\idf.txt'): # 原代码中idf_path=None 现修改为当前目录的idf.txt
self.tokenizer = jieba.dt
self.postokenizer = jieba.posseg.dt
self.stop_words = self.STOP_WORDS.copy()
self.idf_loader = IDFLoader(idf_path or DEFAULT_IDF)
self.idf_freq, self.median_idf = self.idf_loader.get_idf()
def set_idf_path(self, idf_path):
new_abs_path = _get_abs_path(idf_path)
if not os.path.isfile(new_abs_path):
raise Exception("jieba: file does not exist: " + new_abs_path)
self.idf_loader.set_new_path(new_abs_path)
self.idf_freq, self.median_idf = self.idf_loader.get_idf()
步骤2 主程序文件中再加载
import jieba.analyse
jieba.analyse.set_idf_path('./idf.txt')
也尝试过通过配置pyinstaller
的.spec
文件,试图讲txt文件一起打包进来,最终没成功,虽然才选择了这个最简单的方式:改源码。
数据库交互
这个没啥要记录的,使用pymysql操作远程mysql而已。在优化之前封装的pymysql操作库时,简化了一个模块:
def insert(self, table_name=None, single=True, data_list: list = []):
cur = self.conn.cursor()
for data in data_list:
sql = f'insert into {table_name}({",".join([key for key in data.keys()])}) values({",".join(["%s" for _ in range(len(data.keys()))])})'
cur.execute(sql, data)
self.conn.commit()
cur.close()
把insert中的sql语句给优化了,",".join([key for key in data.keys()])
和",".join(["%s" for _ in range(len(data.keys()))])
看似优雅,实则难懂。