飞道的博客

Python爬虫:博客被抄袭了还不知道?快来查查

394人阅读  评论(0)

前言

  免责声明:
    本篇博文的初衷是分享自己学习逆向分析时的个人感悟,所涉及的内容仅供学习、交流,请勿将其用于非法用途!!!任何由此引发的法律纠纷均与作者本人无关,请自行负责!!!
  版权声明:
    未经作者本人授权,禁止转载!!!


  没想到,我的博客竟然被抄袭了(•́へ•́╬),文章一丢丢都不改,原封不动的照搬,更过分的是,将作者的名字直接改成他自己的名字,然后美名其曰:“原创”;还有个网站在未经授权的情况下,直接连作者名字都抄,也不标注来源。。。这种行为本人极其鄙视。虽然客服及时做了处理,将侵权的文章删除了,但这样的惩罚措施未免也太轻了些,抄袭者没有付出什么代价,而且抄袭对他们来说百利而无一害,没有直接封号来的实在,希望这方面的制度继续完善。

  本篇博文就是在这样的背景下,分享一个博客查重脚本,通过对全网进行搜索(包括微信公众号在内),然后用doc2vec将网页向量化,通过余弦相似度对相关网页进行相似度分析,对相似度极高的网页进一步核实,一旦发现抄袭,就联系客服进行版权申诉。
  目标:通过输入关键词信息,在百度和微信公众号分别进行搜索,将相关文章保存到本地,然后再进行相似度分析,输出相似度高的文章链接,以便后面的版权申诉。
  工具:Google Chrome、Charles、PyCharm

1. 爬取百度文章

  这里以我的这篇文章《Python爬虫:逆向分析网易云音乐加密参数》为例,进行查重。

1.1 网页分析

  首先我们在百度上输入关键词网易云爬虫进行搜索,搜索结果有九百多万条,为了方便我们这里选择近一个月发表过的文章,操作就是点击右侧的搜索工具,时间选择一个月内,如下图:


  然后我们打开Chrome的调试面板,相关信息是直接放在网页上的:


  一页有十篇文章,它们的信息都放在一个id="content_left"div标签里,每篇文章也对应一个div,它们的区别是id不同,id是文章的序号,这里我们打开第一个,即id=1div标签:


  我们点击箭头标记的连接就会跳转到这篇文章,它的真实urlhttps://blog.csdn.net/weixin_39190897/article/details/108327884


  这篇文章是CSDN网站上的,文章的内容在一个id="article_content"div标签里。但是我们的搜索是进行全网搜索,搜索结果里除了有CSDN里的文章外,还有博客园、知乎、简书、哔哩哔哩、GitHub等其他网站的内容,如果我们这样对内容进行精确提取的话,需要为每个网站进行标签分析,这样未免太繁琐了。我的方法是通过正则表达式去除网页源代码,然后对剩余的内容进行保存,其实只要正则表达式写的好些,基本上就可以将文本提取出来了。


  大致流程就是上面说的,这里说一下具体如何实现:
  (1) 需要对搜索结果进行一次筛选,筛选的条件是一个月内,即先找到搜索工具的按钮,然后左移一定的距离到时间选项,然后再向下移动一定距离选择一个月内。这里使用selenium库中的动作链来模拟此动作,代码如下:

    def check(self, browser):
        """
        只检索最近一个月的文章
        :param browser:
        :return:
        """
        # 找到搜索工具按钮
        action1 = browser.find_element_by_css_selector('.search_tool')
        actions = ActionChains(browser)
        actions.click(action1)
        # 左移
        actions.move_by_offset(-495, 0).click()
        # 下移
        actions.move_by_offset(0, 110).click()
        actions.perform()
        time.sleep(2)

        return browser

  (2) 需要实现翻页的功能,即要找到下一页按钮所对应的idclass,因为上一页下一页的按钮所对应的class是相同的,所以还需要一个判断,代码如下:

	next_page = browser.find_elements_by_css_selector('.n')[-1]
	if next_page.text == '下一页 >':
		next_page.click()

  (3) 需要对网页内容进行一次粗提取,使用正则表达式,然后将提取的结果进行保存。

  使用selenium时要注意加一个延时,否则浏览器可能反应不过来,会导致下一步操作出现异常错误。

1.2 代码实现

# -*- coding: utf-8 -*-
# @Time    : 2020/9/15 15:20
# @Author  : XiaYouRan
# @Email   : youran.xia@foxmail.com
# @File    : CSDN.py
# @Software: PyCharm


from bs4 import BeautifulSoup
import requests
from selenium import webdriver
from selenium.webdriver import ActionChains
import time
import re
import os


class BauDu(object):
    def __init__(self):
        self.headers = {
   'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                                 '(KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
        self.input_kw = input('请输入要搜索的关键字: ')

    def search(self):
        """
        搜索
        :return:
        """
        url = 'http://www.baidu.com'
        browser = webdriver.Chrome()
        browser.get(url)
        time.sleep(2)

        # from selenium.webdriver.common.by import By
        # input = browser.find_element(By.ID, 'kw')
        # input = browser.find_elements_by_id('kw')
        input = browser.find_element_by_css_selector('#kw')
        input.send_keys(self.input_kw)
        button = browser.find_element_by_css_selector('#su')
        button.click()
        time.sleep(3)

        return browser

    def check(self, browser):
        """
        只检索最近一个月的文章
        :param browser:
        :return:
        """
        # 找到搜索工具按钮
        action1 = browser.find_element_by_css_selector('.search_tool')
        actions = ActionChains(browser)
        actions.click(action1)
        # 左移
        actions.move_by_offset(-495, 0).click()
        # 下移
        actions.move_by_offset(0, 110).click()
        actions.perform()
        time.sleep(2)

        return browser

    def parser(self, browser):
        """
        解析出文章的url
        :param browser:
        :return:
        """
        html = browser.page_source
        soup = BeautifulSoup(html, 'lxml')
        text_list = soup.find_all(class_='result c-container new-pmd')
        urls = []
        for text in text_list:
            tag_a = text.h3.a
            title = tag_a.get_text()
            url = tag_a['href']
            urls.append(url)
            # print(title, url)
        return urls

    def getAllurl(self, browser):
        """
        提取所有结果的url
        :param browser: 
        :return: 
        """
        url_list = []
        count = 1
        while True:
            print('{:*^30}'.format('正在抓取第%d页' % count))
            url = baidu.parser(browser)
            url_list.extend(url)
            try:
                # 下一页
                next_page = browser.find_elements_by_css_selector('.n')[-1]
                if next_page.text == '下一页 >':
                    next_page.click()
                    time.sleep(3)
                    count += 1
                else:
                    break
            except Exception as err:
                print(err)
                break
        browser.close()
        return url_list

    def getText(self, url):
        """
        提取文章
        :param url: 
        :return: 
        """
        try:
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            response.encoding = 'utf-8'
            return response.url, response.text
        except Exception as err:
            # print(err)
            # print('请求异常')
            return '', ''

    def saveArticle(self, url_list):
        """
        保存文章
        :param url_list: 
        :return: 
        """
        for index in range(len(url_list)):
            # 开始整处理网页
            blog_url, blog_text = baidu.getText(url_list[index])
            if blog_text == '找不到页面' or blog_text == '':
                # 内容为空, 该url请求失败, 内容有可能是被删了404
                continue
            # 正则提取
            pattern = re.compile('[a-zA-Z0-9</>":.;!!=_#&@\\\?\[\]()(),:+,\'%《》$、。\s\|\{\}\*\-?【】“”‘’~\^]')
            blog_text = re.sub(pattern, '', blog_text)
            
            if not os.path.exists('./articleBaidu'):
                os.mkdir('./articleBaidu')
            with open('./articleBaidu/{}.txt'.format(str(index).zfill(5)), 'w', encoding='gb18030') as f:
                f.write(blog_url + '\n' + blog_text)
            if (index + 1) % 10 == 0:
                print('已保存: %d页' % (index + 1))

1.3 代码测试

  测试代码如下:

if __name__ == '__main__':
    baidu = BauDu()
    browser = baidu.search()
    browser = baidu.check(browser)
    url_list = baidu.getAllurl(browser)
    baidu.saveArticle(url_list)

  运行结果如下,首先是模拟浏览器进行点击跳转操作:

  然后提取网页中各个文章的url


  然后将文章处理后进行保存:



  可以看到,CSDN、简书、搜狐,还有GitHub等各网站的文章都保存到了本地,每个文件对应一篇文章,每个文章分为两个部分:文章的url和文章内容。当然也就极个别网站因为编码等各种问题,提取到的内容都是乱码,这个问题可以不用管它,毕竟是很少一部分。

2. 爬取微信公众号文章

  因为微信公众号的文章禁止了百度爬虫,所以百度搜索引擎没有权限收录微信公众号里的文章。我们可以输入https://mp.weixin.qq.com/robots.txt查看到微信公众号的Robots协议:


  或许你已经发现了,百度没有收录,但是搜狗收录了,我们可以通过搜狗的搜索引擎来搜索微信公众号的文章,这里依旧用关键词网易云爬虫进行搜索:

2.1 网页分析

  分析流程是可百度一样的,我们还是打开Chrome的调试面板,看一下文章的信息:


  可以看到,每个网页也是十篇文章,都放在了一个class="news-list"ul标签里,每个文章的url在一个class="txt-box"div标签里,点击图中箭头所示的链接也会发生跳转,转到所在的公众号:

  然后提取方法就和百度的一样,直接用正则提取即可。
  相比百度文章的提取来说,这里不需要使用动作链模拟时间选择,只需要找到下一页的按钮,然后提取相关信息并保存即可。这样看来微信公众号的提取比百度还简单,其实并不是,这些都是假象。

2.2 反爬分析

  这里来到第一步,我们找到了文章在搜狗的搜索引擎中url,这里以第一篇为例,它在搜狗的搜索引擎中urlhttp://weixin.sogou.com/link?url=dn9a_-gY295K0Rci_xozVXfdMkSQTLW6cwJThYulHEtVjXrGTiVgS_ebTjeDJbzRobk7sV0-esChYTg9WI5RvlqXa8Fplpd9p9wyDv1i5nLnl71X1UfI_CJKUC0n9tWj6xepdWbSVrenOGPPw_VvyqlJvHqDryH-3JVoytoHLXnTdEvnQ15mFVP42dxeIClXiHTBUrOZ2RdaiHLZUWi_E5Yn3k7xqldQhRrZENR-zjShq5GeNOivVW7i63t4nGTYnF0v03wc3RlhWLv6O878UA..&type=2&query=%E7%BD%91%E6%98%93%E4%BA%91%E7%88%AC%E8%99%AB&token=E6921574590DD76C1712B9B460D8616317D1AC4D5F661F52,其中%后面是什么意思应该已经知道了吧,它的意思在上一篇文章里已经介绍了,这里的%E7%BD%91%E6%98%93%E4%BA%91%E7%88%AC%E8%99%AB意思就是我们输入的关键词网易云爬虫
  我通过代码去访问该链接,理论上应该跳转到该文章的公众号上,然而却出现了错误,根据PyCharm控制台输出的信息,我看到了一个链接:https://weixin.sogou.com/antispider/,很明显,被网站的反爬虫机制给禁止了,打开这个链接看一下:


  这里有两种方法来解决,第一个就是尝试进一步伪装来欺骗网站的反爬虫机制,第二个就是验证码识别。这里我尝试了去识别验证码,使用的是Tesseract-OCR以及它的Python API pytesseract,但是效果并不好,它的这个验证码不仅有很多小的字母,而且图片上加了一条类似余弦波的曲线来干扰识别。刚开始我也想尝试针对这个验证码来训练一个神经网络模型,但是随之而来的就是数据集问题,而且还得自己标注,太麻烦了。所以还是采用第一种方法,伪装自己。
  因为发生了跳转,为了方便看一下这个跳转的过程中发生了什么,所以这里我用Charles进行了抓包:


  这里的第一个不同就是这个发生跳转的url发生了稍微的变化,相比原来的,这里多了两个参数:&k=21&h=t,经过几次刷新发现,并没有什么影响,即这两个参数不是必须的。


  我们手动点击的那个链接会跳转到这里,就是上图框框里面的,它会执行这个这段JavaScript代码,然后将url进行重定向,框框中的url就是微信公众号里的文章链接,所以干脆直接在这里面提取微信公众号里文章的url,代码如下:

	reault = response.text.split(';')
	real_url = ''
    for i in reault[2:-3]:
        j = i.strip()
        real_url += j[8:-1]
    return real_url

  这段代码的意思就是先将文本以进行分割,然后去除冗余字符后再进行拼接,拼接出来的url就是我们要找的微信公众号里文章的url,最后将结果返回。

  需要注意的是,这个url后面的字符也是随机的,每次刷新都不一样,但是无妨,因为我们直接提取的是变化后的结果。

  经过多次失败地尝试,Request请求所需要的User-AgentRefererCookieHost要和上面抓包抓取的内容一样,否则就会触发反爬虫机制,这里的反爬虫机制对这些参数还是很敏感的。

	Host: weixin.sogou.com
	User-Agent: netdisk;2.2.2;pc;pc-mac;10.14.5;macbaiduyunguanjia
	Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
	Referer: http://weixin.sogou.com/weixin?type=2&s_from=input&query=%E7%BD%91%E6%98%93%E4%BA%91%E7%88%AC%E8%99%AB&ie=utf8&_sug_=n&_sug_type_=
	Accept-Encoding: gzip, deflate
	Accept-Language: zh-CN,zh;q=0.9
	Cookie: SUID=4E1BC07A2C18960A000000005F562407; SUV=1599480842999554; pgv_pvi=4547401728; ABTEST=0|1600214894|v1; weixinIndexVisited=1; IPLOC=CN3203; SNUID=590DD76C1712B9B460D8616317D1AC4D; ld=Dkllllllll2KqAy4lllllVduVhDlllllW7lbFkllllylllll9Zlll5@@@@@@@@@@; LSTMV=182%2C70; LCLKINT=1731; JSESSIONID=aaaTMPxeCI4CUf2aC0Zox
	Connection:keep-alive

  再说一下这几个参数都是干什么的:
    Host 用于指定请求资源的主机
    User-Agent 浏览器的UA标识,服务器根据此内容可以识别到用户使用的浏览器信息及操作系统地信息
    Referer 标识请求是从哪个页面发过来的
    Cookie 网站为了辨别用户进行会话跟踪而存储在本地的数据


  同理,也要对抓取文章的Request请求参数进行相应的改变:

	Host: mp.weixin.qq.com
	Referer: 这个就是搜狗搜索引擎中文章的url
	Cookie: pgv_pvi=3376051200; RK=zRKkVVUClt; ptcz=bea38e8122d337415e409f1122e019246b9cd2d50352aba7155d95384bfc9cc0; tvfe_boss_uuid=57ca5a85851f958c; pgv_pvid=3704594732; euin=owozNK-zNKosNn**; psrf_access_token_expiresAt=1607758183; tmeLoginType=2; psrf_qqrefresh_token=A8028B993A1A7526C49F5F7CEA2F8D8F; psrf_qqunionid=; psrf_qqaccess_token=48A2231BD3DAFAB5395160B26CCA59C1; psrf_qqopenid=975A7CB39D73DB46B588429325C16AE5; o_cookie=2309209368; pgv_info=ssid=s4047750795; rewardsn=; wxtokenkey=777

  这里还要注意一下,用requests.get()发起请求时最好关闭一下证书认证,即verify=False,否则会引发错误或者得不到信息。对我们输入的关键词也要重新做一下编码,处理后的结果为以%开头后面是两个十六进制的字符,否则会出现编码错误:UnicodeEncodeError: 'latin-1' codec can't encode characters in position

2.3 代码实现

# -*- coding: utf-8 -*-
# @Time    : 2020/9/16 8:13
# @Author  : XiaYouRan
# @Email   : youran.xia@foxmail.com
# @File    : WeChat.py
# @Software: PyCharm


from bs4 import BeautifulSoup
import requests
from selenium import webdriver
import time
import re
import os


class SouGou(object):
    def __init__(self):
    	# 对输入的内容做一下编码, 否则也会出错
        self.input_kw = input('请输入要搜索的关键字: ')
        self.headers = {
   'User-Agent': 'netdisk;2.2.2;pc;pc-mac;10.14.5;macbaiduyunguanjia',
                        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,'
                                  'image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
                        'Cookie': 'SUID=4E1BC07A2C18960A000000005F562407; SUV=1599480842999554; '
                                  'pgv_pvi=4547401728; ABTEST=0|1600214894|v1; weixinIndexVisited=1; '
                                  'IPLOC=CN3203; ld=olllllllll2KqAy4lllllVd6hNGlllllW7lbFkllllGlllll'
                                  '9klll5@@@@@@@@@@; LCLKINT=1528; LSTMV=180%2C76; SNUID=590DD76C1712B'
                                  '9B460D8616317D1AC4D; JSESSIONID=aaagW2SHM9h2C4PRboZox',
                        'Connection': 'keep-alive',
                        'Referer': 'http://weixin.sogou.com/weixin?type=2&s_from=input&query={}&ie=utf8'
                                   '&_sug_=n&_sug_type_='.format(str(self.input_kw.encode())[1:].replace('\\x', '%')),
                        'Host': 'weixin.sogou.com',
                        'Accept-Encoding': 'gzip, deflate',
                        'Accept-Language': 'zh-CN,zh;q=0.9'}
        self.headers_ = {
   'User-Agent': 'netdisk;2.2.2;pc;pc-mac;10.14.5;macbaiduyunguanjia',
                         'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,'
                                   'image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
                         'Cookie': 'pgv_pvi=3376051200; RK=zRKkVVUClt; ptcz=bea38e8122d337415e409f1122e0192'
                                   '46b9cd2d50352aba7155d95384bfc9cc0; tvfe_boss_uuid=57ca5a85851f958c; pgv_'
                                   'pvid=3704594732; euin=owozNK-zNKosNn**; psrf_access_token_expiresAt=16077'
                                   '58183; tmeLoginType=2; psrf_qqrefresh_token=A8028B993A1A7526C49F5F7CEA2F8'
                                   'D8F; psrf_qqunionid=; psrf_qqaccess_token=48A2231BD3DAFAB5395160B26CCA59C1;'
                                   ' psrf_qqopenid=975A7CB39D73DB46B588429325C16AE5; o_cookie=2309209368; rewar'
                                   'dsn=; wxtokenkey=777',
                         'Connection': 'keep-alive',
                         'Referer': '',
                         'Host': 'mp.weixin.qq.com',
                         'Accept-Encoding': 'gzip, deflate',
                         'Accept-Language': 'zh-CN,zh;q=0.9'}

    def search(self):
        """
        搜索
        :return:
        """
        url = 'http://weixin.sogou.com'
        browser = webdriver.Chrome()
        browser.get(url)
        time.sleep(2)

        input = browser.find_element_by_css_selector('#query')
        input.send_keys(self.input_kw)
        button = browser.find_element_by_css_selector('.swz')
        button.click()
        time.sleep(3)

        return browser

    def parser(self, browser):
        """
        解析出文章的url
        :param browser: 
        :return: 
        """
        html = browser.page_source
        soup = BeautifulSoup(html, 'lxml')
        text_list = soup.find_all(class_='txt-box')
        urls = []
        for text in text_list:
            tag_a = text.h3.a
            title = tag_a.get_text()
            url = 'http://weixin.sogou.com' + tag_a['href']
            urls.append(url)
            # print(title, url)
        return urls

    def getAllurl(self, browser):
        """
        提取所有结果的url
        :param browser: 
        :return: 
        """
        url_list = []
        count = 1
        while True:
            print('{:*^30}'.format('正在抓取第%d页' % count))
            url = sougou.parser(browser)
            url_list.extend(url)
            try:
                # 下一页
                next_page = browser.find_elements_by_css_selector('.np')[-1]
                if next_page.text == '下一页':
                    next_page.click()
                    time.sleep(3)
                    count += 1
                else:
                    break
            except Exception as err:
                print(err)
                break
        browser.close()
        return url_list

    def getRealURL(self, url):
        """
        提取真正的url
        :param url: 
        :return: 
        """
        try:
            # 忽略证书认证
            response = requests.get(url, headers=self.headers, verify=False)
            response.raise_for_status()
            response.encoding = 'utf-8'
            reault = response.text.split(';')
            real_url = ''
            for i in reault[2:-3]:
                j = i.strip()
                real_url += j[8:-1]
            return real_url
        except Exception as err:
            print(err)
            print('请求异常')
            return ''

    def getText(self, url):
        """
        提取文章
        :param url: 
        :return: 
        """
        real_url = self.getRealURL(url)
        try:
            self.headers_['Referer'] = url
            response = requests.get(real_url, headers=self.headers_, verify=False)
            response.raise_for_status()
            response.encoding = 'utf-8'
            return response.url, response.text
        except Exception as err:
            # print(err)
            # print('请求异常')
            return '', ''

    def saveArticle(self, url_list):
        """
        保存文章
        :param url_list: 
        :return: 
        """
        for index in range(len(url_list)):
            # 开始整处理网页
            blog_url, blog_text = sougou.getText(url_list[index])
            # 微信公众号的主题内容在一个id为js_content的div标签内
            # 所以这里再进行一下冗余处理
            soup = BeautifulSoup(blog_text, 'lxml')
            text_tag = soup.find(id='js_content')
            if text_tag is None:
                # 内容为空, 应该是内容被删了
                continue
            blog_text = text_tag.text
            pattern = re.compile('[a-zA-Z0-9</>":.;!!=_#&@\\\?\[\]()(),:+,\'%《》$、。\s\|\{\}\*\-?【】“”‘’~\^]')
            blog_text = re.sub(pattern, '', blog_text)
            if not os.path.exists('./articleWechat'):
                os.mkdir('./articleWechat')
            with open('./articleWechat/{}.txt'.format(str(index).zfill(5)), 'w', encoding='gb18030') as f:
                f.write(blog_url + '\n' + blog_text)
            if (index + 1) % 10 == 0:
                print('已保存: %d页' % (index + 1))

2.4 代码测试

  测试代码如下:

if __name__ == '__main__':
    sougou = SouGou()
    browser = sougou.search()
    url_list = sougou.getAllurl(browser)
    sougou.saveArticle(url_list)

  运行结果如下,警告信息不需要管它,哈哈哈哈:


3. 模型训练

  我是用doc2vec来将文本向量化,它是在word2vec的基础上进行拓展的,由于考虑了文本的上下文信息,所以效果会比单纯地word2vec更好。这里使用的是doc2vec中的DM模型,语料库用的是维基百科上的(2020年7月),训练代码如下:

# -*- coding: utf-8 -*-
# @Time    : 2020/9/17 13:03
# @Author  : XiaYouRan
# @Email   : youran.xia@foxmail.com
# @File    : WiKi.py
# @Software: PyCharm


from gensim.models import doc2vec
import logging


# 打印日志的时间 打印日志级别名称 打印日志信息
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(message)s', level=logging.INFO)


if __name__ == '__main__':
    wikidata = 'E:/Program/Python/NLP/AutomanticSummarization/data/wiki_202007_s.txt'
    # 遍历包含文档的文件: 一行 = TaggedDocument对象
    # 单词应该已经过预处理, 并用空格分隔
    # 文档标签是根据文档行号自动构建的(每个文档都有一个唯一的整数标签)
    document = doc2vec.TaggedLineDocument(source=wikidata)
    # dm=1, 使用分布式记忆模型(PV-DM), 否则, 将使用分布式词袋模型(PV-DBOW)
    # dbow_words=0, 只训练doc2vec
    # vector_size: 特征向量的维数
    # window: 窗口大小, 即句子中当前单词和预测单词之间的最大距离
    # min_count: 忽略总频率低于此频率的所有单词
    # epochs: 语料库中的迭代次数
    # workers: 使用多线程来训练模型
    model = doc2vec.Doc2Vec(documents=document, dm=1, dbow_words=0, vector_size=128, window=10, min_count=5, epochs=10, seed=128, workers=8)
    model.save('./model/doc2vec_size128_202007.model')

  有关gensim的详细操作,请参考官方文档

  我这里设置的向量维度为128,训练了10次,大概用了四十多分钟,如果想进一步提高准确率,可以将这两个值再改大一些。模型的训练信息如下:

4. 相似度分析

  我是用余弦相似度来做文本的相似度分析,余弦相似度计算的是两个向量间的夹角的余弦值,夹角越小,其余弦值就越大,说明这两个向量越相关。计算公式如下:
s i m i l a r i t y = cos ⁡ θ = A ⃗ B ⃗ ∣ ∣ A ⃗ ∣ ∣ ∣ ∣ B ⃗ ∣ ∣ = ∑ i = 1 n A ⃗ i B ⃗ i ∑ i = 1 n A ⃗ i ∑ i = 1 n B ⃗ i similarity=\cos \theta=\frac {\vec A \vec B} {\big|\big|\vec A\big|\big| \big|\big|\vec B\big|\big|}=\frac {\sum_{i=1}^n\vec A_i \vec B_i} {\sqrt {\sum_{i=1}^n\vec A_i}\sqrt {\sum_{i=1}^n\vec B_i}} similarity=cosθ=A B A B =i=1nA i i=1nB i i=1nA iB i  如果是向量是二维的,这个公式就是我们常说的向量的夹角公式:
cos ⁡ θ = x 1 x 2 + y 1 y 2 x 1 2 + y 1 2 x 2 2 + y 2 2 \cos \theta=\frac {x_1x_2+y_1y_2} {\sqrt {x_1^2+y_1^2}\sqrt {x_2^2+y_2^2}} cosθ=x12+y12 x22+y22 x1x2+y1y2  代码实现就是:

    def cosine(self, vec1, vec2):
        # 两个分母
        vec1mod = np.sqrt(vec1.dot(vec1))
        vec2mod = np.sqrt(vec2.dot(vec2))
        if vec1mod != 0 and vec2mod != 0:
            sim = vec1.dot(vec2) / (vec1mod * vec2mod)
        else:
            sim = 0
        return "{:.4}".format(sim)

  当然,也可以使用sklearn库中的cosine_similarity函数来计算:

	from sklearn.metrics.pairwise import cosine_similarity

  这里又对文本进行了处理,使用了jieba来进行分词,方便进行向量化。还用到了以前整理的一个停用词文件,当然也可以使用其他的,其目的就是将一些非必要字符在进行去除。
  代码实现如下:

# -*- coding: utf-8 -*-
# @Time    : 2020/9/19 19:26
# @Author  : XiaYouRan
# @Email   : youran.xia@foxmail.com
# @File    : Check.py
# @Software: PyCharm


from gensim.models import doc2vec
import logging
import jieba
import os
import numpy as np
import requests
import re

# 打印日志的时间 打印日志级别名称 打印日志信息
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(message)s', level=logging.INFO)


class Check(object):
    def __init__(self):
        self.sw_list = self.get_stopword(stopword_file='./stopword/stopword.txt')
        self.model = doc2vec.Doc2Vec.load('./model/doc2vec_size128_202007.model')
        self.headers = {
   'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                                      '(KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}

    def get_stopword(self, stopword_file):
        """
        加载停用词
        :param stopword_file:
        :return:
        """
        stopword_list = []
        with open(stopword_file, 'r', encoding='gb18030', errors='ignore') as f:
            line_list = f.readlines()
            for line in line_list:
                stopword_list.append(line.replace('\n', ''))
        return stopword_list

    def article2vec(self, article_file):
        """
        文本向量化
        :param article_file:
        :return:
        """
        article = []
        with open(article_file, 'r', encoding='gb18030') as f:
            line_list = f.readlines()
            article_url = line_list[0]
            for line in line_list[1:]:
                short_sentence = line.strip('\r\n')
                word_list = list(jieba.cut(short_sentence))
                for word in word_list:
                    if len(word) > 1 and word not in self.sw_list:
                        article.append(word)
        article_vec = self.model.infer_vector(doc_words=article, alpha=0.01, epochs=1000)
        return article_url, article_vec

    def cosine(self, vec1, vec2):
        """
        余弦相似度
        :param vec1:
        :param vec2:
        :return:
        """
        # 两个分母
        vec1mod = np.sqrt(vec1.dot(vec1))
        vec2mod = np.sqrt(vec2.dot(vec2))
        if vec1mod != 0 and vec2mod != 0:
            sim = vec1.dot(vec2) / (vec1mod * vec2mod)
        else:
            sim = 0
        return "{:.4}".format(abs(sim))

    def simlarity(self, article_path, article_file, article_list):
        """
        计算文章的相似度
        :param article_path:
        :param article_file:
        :param article_list:
        :return:
        """
        _, real_vec = self.article2vec(article_file=article_file)
        count = 0
        result = []
        for article in article_list:
            article = article_path + article
            article_url, article_vec = self.article2vec(article)
            sim = self.cosine(real_vec, article_vec)
            result.append([article_url, sim])
            count += 1
            if count % 25 == 0:
                print('[{0}]Processed {1} articles'.format(article_path[9:-1], count))
        print('[{}]Processed over!'.format(article_path[9:-1]))
        return result

    def saveResult(self, result_list):
        """
        将分析结果保存
        :param result_list:
        :return:
        """
        result_list = sorted(result_list, key=lambda list1: float(list1[1]), reverse=True)
        with open('./result.txt', 'w', encoding='utf-8') as f:
            count = 0
            for result in result_list:
                line = result[1] + '\t' + result[0] + '\n'
                f.write(line)
                count += 1
                if count % 50 == 0:
                    print('Writed %d articles' % count)
        print('Writed over!')

    def getText(self, url, my_article_file):
        """
        提取我的文章
        :param url:
        :param my_article_file:
        :return:
        """
        try:
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            response.encoding = 'utf-8'
            pattern = re.compile('[a-zA-Z0-9</>":.;!!=_#&@\\\?\[\]()(),:+,\'%《》$、。\s\|\{\}\*\-?【】“”‘’~\^]')
            my_article = re.sub(pattern, '', response.text)
            with open(my_article_file, 'w', encoding='gb18030') as f:
                f.write(response.url + '\n' + my_article)
        except Exception as err:
            print(err)
            print('请求异常')

  测试代码如下:

if __name__ == '__main__':
    my_article_url = 'https://blog.csdn.net/qq_42730750/article/details/108415551'
    my_article_file = './my_article.txt'
    baidu_article = './articleBaidu/'
    wechat_article = './articleWechat/'

    check = Check()
    check.getText(my_article_url, my_article_file)

    print('Processing...')
    baidu_result_list = check.simlarity(article_path=baidu_article, article_file=my_article_file,
                                        article_list=os.listdir(baidu_article))
    wechat_result_list = check.simlarity(article_path=wechat_article, article_file=my_article_file,
                                         article_list=os.listdir(wechat_article))
    print('Writting...')
    check.saveResult(baidu_result_list + wechat_result_list)

  运行结果如下:

  我对结果按照相似度从高到低进行了一个排序,结果文件中最上面的就是疑似侵权的,就比如第一个,相似度超过了98%,十分可疑,我们复制后面的链接到浏览器进一步核实:


  enmmm,是我自己的,哈哈哈哈(〃‘▽’〃),想必你会有个疑问,既然是同一篇文章,为什么相似度不是100%???这是因为算法引入了随机性,即随机初始化和随机采样,而且训练是多线程进行的,输出的结果因为排序也会引起微小的差异,会导致各个向量的训练位置漂移到任意不同的位置,详细信息请看官方给出的答复

5. 剩下的就交给客服吧

  如果发现了抄袭的文章,可以将咱们提取的侵权链接和自己文章的原链接一起发给CSDN的客服,具体操作流程可以参考这篇文章

  剩下的法律交涉就交给后台的法务部吧,哈哈哈哈o(´^`)o


  当然了,除此之外你还可以自己申诉,像博客园、微信公众号这些直接投诉即可,不过有些网站根本不搭理你,建议通过官方进行版权申诉。

结束语

  整体的想法就是这些,需要优化的部分还有很多。在进行文本相似度分析那一模块,由于文本很多,处理起来较慢,所以后期想加入多线程来提高并发性,有时间还可以再做一个GUI,加入邮件发送的功能,哈哈哈哈,侵权必究(•́へ•́╬),加油(ง •̀_•́)ง。


转载:https://blog.csdn.net/qq_42730750/article/details/108685209
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场