大虾

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

客户提供了公众号文章的永久链接,并在远程数据库中保存了原创的文章,要求采集目标公众号文章和原创文章有多少重复的,以便判定是否侵权。

程序设计

每天都有大几千的公众号文章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=(.*?)&amp;mid=(.*?)&amp;idx=(.*?)&amp;sn=(.*?)&amp', 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()))])看似优雅,实则难懂。

posted on 2022-11-30 15:09  一灯编程  阅读(59)  评论(0编辑  收藏  举报