2018/6/14 山东大学学习资源聚合平台工作总结(二)

在主系统外,我还做了辅助的爬虫系统和自动审核系统作为创新实训的研究部分:

首先是爬虫系统,利用scrapy框架完成:

爬虫的技术问题大概有两个:

1.解决各大网站的反爬虫问题:

利用爬取代理IP来解决,在爬我们想要的网页之前,我们先去爬取代理IP,

    name = 'ip'
    allowed_domains = ['xicidaili.com']
    start_urls = ['http://www.xicidaili.com/wt/1/']

    custom_settings = {
        'DEFAULT_REQUEST_HEADERS': {
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
            "Accept-Encoding": "gzip, deflate",
            "Accept-Language": "zh-CN,zh;q=0.9",
            "Cache-Control": "max-age=0",
            "Connection": "keep-alive",
            "Host": "www.xicidaili.com",
            "Upgrade-Insecure-Requests": 1,
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
        },
        # 'DOWNLOADER_MIDDLEWARES': {},
        'ITEM_PIPELINES': {
            'resource_aggregation.pipelines.IpWriterPipeline': 300,
        },
        'DOWNLOAD_DELAY': 50
    }

    rules = (
        Rule(LinkExtractor(allow=r'.*/wt/(\d+)'), callback='parse_item', follow=True),
    )

利用如下大爱吗构建HTTP请求头,并且设置IP输出管道,设置爬取IP的正则表达,将爬取的IP存入文件中,以便之后使用:


然后在setting文件中配置,scrapy就可以使用这组IP:


2.许多网站上的内容是通过ajax获得的,内容并不是网页直接渲染的,如何拿到这一块内容:

我们模拟构造出请求包,向服务器发送request请求,然后捕捉response响应,解析响应内容,得到结果:

如在csdn中的换页是通过ajax来完成的,我们模拟这个换页请求:


当得到response响应后,我们解析这个响应得到包中的内容:

response包内容:



然后利用schedule设置定时任务,来定时爬取:

def run():
    schedule.every(10).hour.do(csdn_index_task)  # 每10小时爬取一次
    schedule.every(3).day.at("02:00").do(csdn_user_article_task)  # 每三天凌晨两点爬取一次
    schedule.every(2).week.do(custom_spider_task)
    schedule.every(2).day.at("10:30").do(ip_task)

    while True:
        schedule.run_pending()#保持schedule一直运行,去查询上面的任务
        time.sleep(1)

审核系统的工作是对爬虫爬取的数据以及主系统中的专栏数据做审核,自动化的审核是件困难的操作,首先什么样的文章叫做好文章,这个标准是很难定义的,我去网页查阅大量文献资源后,参考今日头条中对文章的审核过程,我对数据进行了如下方面的审核:

1.敏感词过滤算法:采用DFA(确定又穷自动机)算法,对文字进行好的、高效的过滤算法:

我们将每个字看作一个状态,将query操作看作操作转换的条件,举个例子:


比如说日本人和日本鬼子这两个词,我采用HashMap来存储:

即  日=》{本},本 =》 {鬼、人},鬼 =》 {子},字样我们可以将我们的词库变化成一个森林(包含一个个如同上图一样的树),这样在检索文章时,第一次检索的只是森林中每棵树的根节点(这个数量是少的),然后如果和每棵树的根节点吻合,就会进入这棵树中,减小了查询的范围,提升了速度。

主要的代码就是构建这座森林,和查询代码:

    /** 
     * 读取敏感词库,将敏感词放入HashSet中,构建一个DFA算法模型: 
     */  
    private Map addSensitiveWordToHashMap(Set<String> wordSet) {  
  
        // 初始化敏感词容器,减少扩容操作  
        Map wordMap = new HashMap(wordSet.size());  
  
        for (String word : wordSet) {  
            Map nowMap = wordMap;  
            for (int i = 0; i < word.length(); i++) {  
  
                // 转换成char型  
                char keyChar = word.charAt(i);  
                // 获取  
                Object tempMap = nowMap.get(keyChar);  
  
                // 如果存在该key,直接赋值  
                if (tempMap != null) {  
                    nowMap = (Map) tempMap;  
                }  
  
                // 不存在则,则构建一个map,同时将isEnd设置为0,因为他不是最后一个  
                else {  
  
                    // 设置标志位  
                    Map<String, String> newMap = new HashMap<String, String>();  
                    newMap.put("isEnd", "0");  
  
                    // 添加到集合  
                    nowMap.put(keyChar, newMap);  
                    nowMap = newMap;  
                }  
  
                // 最后一个  
                if (i == word.length() - 1) {  
                    nowMap.put("isEnd", "1");  
                }  
            }  
        }  
  
        return wordMap;  
    }  

查询代码:

  public int CheckSensitiveWord(String txt, int beginIndex, int matchType) {  
        // 敏感词结束标识位:用于敏感词只有1位的情况  
        boolean flag = false;  
  
        // 匹配标识数默认为0  
        int matchFlag = 0;  
        Map nowMap = sensitiveWordMap;  
        for (int i = beginIndex; i < txt.length(); i++) {  
            char word = txt.charAt(i);  
            // 获取指定key  
            nowMap = (Map) nowMap.get(word);  
            // 存在,则判断是否为最后一个  
            if (nowMap != null) {  
  
                // 找到相应key,匹配标识+1  
                matchFlag++;  
  
                // 如果为最后一个匹配规则,结束循环,返回匹配标识数  
                if ("1".equals(nowMap.get("isEnd"))) {  
  
                    // 结束标志位为true  
                    flag = true;  
  
                    // 最小规则,直接返回,最大规则还需继续查找  
                    if (SensitivewordFilter.minMatchTYpe == matchType) {  
                        break;  
                    }  
                }  
            }  
  
            // 不存在,直接返回  
            else {  
                break;  
            }  
        }  
        // 长度必须大于等于1,为词  
        if (matchFlag < 2 || !flag) {  
            matchFlag = 0;  
        }  
        return matchFlag;  
    }  

为了演示效果,我在两篇演示数据中加入了“法轮功”一词作为敏感词汇。

2.对于外部资源,会进行一些外部指标的审核,如访问量,采用的方法是去掉一定比例的最高分和最低分,防止极端数据去总体数据平均值的影响,然后对高于平均值的数据进行大概率的推荐、对小于平均值的概率以小概率推荐,以概率推荐的目的是为了防止数据的过拟合,让新文章、新观点也可以被系统所吸收。

代码如下:

		//当pattern为1时,即外部文件时,审核访问量指标
		if(pattern == 1){
			System.out.println("该文章访问量为:"+knowledege.view_number);
			//访问量指标,以概率进行推荐,防止过拟合,新观点无法进入
			if(Integer.parseInt(knowledege.view_number)<average_viewNum){
				//当观看量比平均值小时,已0.35概率推荐,大时以0.9概率推过
				double pass_pro = r.nextDouble();
				if(pass_pro<0.65){
					canPass = false;
					error_msg = "访问量不足";
					return canPass;
				}
			}
			else if(Integer.parseInt(knowledege.view_number)>=average_viewNum){
				double pass_pro = r.nextDouble();
				if(pass_pro<0.1){
					canPass = false;
					error_msg = "访问量不足";
					return canPass;
				}
			}
		}

3.利用百度搜索指数来确定内容的关联度,即所写内容是否与所打标签有关:

因为考虑到用户群体和国家政策缘故,我们使用百度搜索指数代替谷歌搜索指数来确定词之间的相似度。

首先,我们利用NLP技术对文章提取关键词,在我们HanLP库中所采用的关键词提取是完全基于出现次数的,这样的关键词提取不够准确,因此我对其进行了改进,将词性考虑在内,大多数的关键词都是名词和形容词,因此我对提取出的关键词进行进一步的筛选,挑选出名词词性,我还尝试利用句子的结构和依赖关系来进一步增加准确度,当由于分析整篇文章句子成分所带来的计算量过于庞大,导致系统性能下降迅速,而且带来的准确度提高并不显著,因此在最终系统中,我只利用词性进行了进一步筛选:

	private static boolean isKeywordSimilar(ArrayList<String> keywords, ArrayList<String> keywords2) {
		// TODO Auto-generated method stub
		//获取我们神经网络学习出的关键词
		ArrayList<String> AfterKeyword1 = new ArrayList<String>();
		for(int i=0;i<keywords.size();i++){
			String key = keywords.get(i).split("=")[0];
			AfterKeyword1.add(key);
		}
		System.out.println("文章学习出的关键词:");
		printArrayList(AfterKeyword1);
		System.out.println();
		System.out.println("文章作者所标注的关键词:");
		printArrayList(keywords2);
		System.out.println();
		
		//计算最相似相似度
		double scores[] = new double[keywords2.size()];
		for(int i=0;i<keywords2.size();i++){
			String s1 = keywords2.get(i);
			scores[i] = Integer.MAX_VALUE;
			for(int j=0;j<AfterKeyword1.size();j++){
				String s2 = AfterKeyword1.get(j);
				//百度距离
				double score = bs.getRelative(s1.toLowerCase(), s2.toLowerCase());
				if(score <= 0){
					continue;
				}
				if(score<scores[i]){
					scores[i] = score;
				}
			}
		}
		//为所有关系度排序
		Arrays.sort(scores);

		double sum = scores[0];
		//关联度小于0.3,认为有关联
		System.out.println("关键词关联度"+sum);
		if(sum<0.3){
			return true;
		}
		return false;
	}

Word2vec中的词相似度函数,他是基于词本身的,而不是语义相关的,比如深度学习和数据两个词,他们之间的相似度就基本为0,因为但从词的结构来看,他们毫无关系,但是却是语义相关的,所以我改用了基于百度的词间距离

其中百度搜索指数的公式如下:

其中f(x)是词x在百度搜索中出现的总次数,f(x,y)是x和y出现的总次数,N是百度搜索的总词条数,经过实验我们发现百度搜索的总词条是100000000。

其实对于这个公式还有两点需要注意,baidu不像google,显示的总词条是有限的,而我们国内要想访问google,必须翻墙,而基于网站设计的最小化原则,应考虑最低需求,因此我们只能采用百度来完成,那么当数据达到上限时该如何处理呢?

如果有一个f(x)达到了上限,而f(y)和f(x,y)并没达到上限,这个是没有影响的,因为这个公式仍然工作,但当fx和fy都到达上限的时候,分母就会为0,这时公式失效,我们考虑这种特殊情况,当两个词都达到上限时,说明这两个词都是常见值,他们更加偏向于无实义的助词,并不应该成为关键词,应该是上一步提取关键词时产生的误差,所以我们直接返回一个负值,代表这组词无效代表这组词无效,不过这里有一种情况例外,就是x与y完全相等且搜索数过多,这时我们特判这种情况关联度高。而当f(x,y) 完全与max(fx,fy)相等时,即分子为0,出现这种问题,一是都等于N(达到搜索上限),这时我们同样认为这个是极端的,和上面问题一样,认为是无实义的助词,并不应该成为关键词,返回-1。

还有一点需要注意,这里返回的值并不是关联程度,首先它是与关联程度成反相关的,即值越接近于0,关联度越大,所以我们在显示时只是单纯利用1-ngd的方式来显示结果,而且真正算法中的N是baidu的总页面数,我们只是利用baidu的搜索上限去近似,所以算出来的数并不是我们理解的关联程度,但是这是一个保序的函数,即当两个词关联度越大时,他们之间的ngd值就越接近于0,所以当我们对我们的系统进行大量实验后,我们发现当ngd值小于0.3时,表明两个词直接具有可信任程度的关联。

核心代码如下:

查找搜索数方法:

	public long search(String keyword1,String keyword2){
		//拼接关键词,形成url
		String url = "http://www.baidu.com/s?wd="+keyword1+"%20"+keyword2;
//		String url = "http://www.google.com.hk/search?q="+keyword1+"+"+keyword2;
		long total = 0;
		try {
			//解析url,发送get请求
			Document document = Jsoup.connect(url).get();
			//获取搜索数
			total = getBaiduSearchResultCount(document); 
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
		return total;
	}

获取搜索数方法:

	//利用css标签获取解析网页,获得搜索数
	private long getBaiduSearchResultCount(Document document) {
		// TODO Auto-generated method stub
		//观察百度搜索数的显示位置,div class name
        String cssQuery = "html body div div div div.nums";  
        //选择对象
        Element totalElement = document.select(cssQuery).first();  
        //获取文本
        String totalText = totalElement.text();   
        //利用正则表达只选取数字
        String regEx="[^0-9]";     
        Pattern pattern = Pattern.compile(regEx);        
        Matcher matcher = pattern.matcher(totalText);  
        totalText = matcher.replaceAll("");  
        long total = Long.parseLong(totalText);  
        return total;  
	}

NGD方法:

	//利用NGD公式计算两个词之间的关联度
	public double getRelative(String keyword1,String keyword2){
		double ngd = 0;
		if(keyword1.equals(keyword2)){
			ngd = 0;
			return ngd;
		}
		long fx = search(keyword1);
		long fy = search(keyword2);
		long fxy = search(keyword1,keyword2);
//		if(fx == N||fy == N){
//			return -1;
//		}
		double fenzi = (double) (Math.max(Math.log(fx), Math.log(fy))-Math.log(fxy));
		double fenmu = (double) (Math.log(N)-Math.min(Math.log(fx), Math.log(fy)));
		/**
		 * fenzi = 0 => fx=fxy=N => "关联大"
		 * fenmu = 0 => fx = fy = N => "都是频繁词"
		 * 
		 */
		
		if(fenmu == 0){
			ngd = -1;//有关联
		}
		else if(fenzi == 0){
				ngd = -1;
		}
		else{
			ngd = fenzi/fenmu;
			ngd = Math.abs(ngd);
		}
		return ngd;
	}

4.最后一步,我对文章进行了查重,计算与库中文章的相似度,防止多篇相似文章的出现,采用的方法是利用HanLP包计算分词,然后直接计算词向量间的余弦相似度来计算文章间的相似度:

在分词之后,我们利用词频(或者TF-IDF值)作为每维的数值,这样,我们就将文章向量化,我们可以通过计算向量间的夹角值来判断向量的相似程度。夹角越小,就代表越相似。

A是 [A1, A2, ..., An] ,B是 [B1, B2, ..., Bn] ,则余弦相似度为:



在实现过程中我们直接调用了HanLP 库中的函数,计算待审文章和库中文章的相似度,同样以概率形式通过:

	//计算文本相似度,模拟查重,对文章分词,计算余弦相似度
	private static boolean isSimilar(Knowledge knowledege, ArrayList<Knowledge> have_passed_knowledge2) {
		// TODO Auto-generated method stub
		TextSimilarity similarity = new CosineSimilarity();
		for(int i=0;i<have_passed_knowledge2.size();i++){
			Knowledge other = have_passed_knowledge2.get(i);
			String s1 = knowledege.article_content;
			String s2 = other.article_content;
			s1 = s1.replace(" ", "").replace("\n", "").replace("\r", "").replace("\'", "").replace("\"", "");
			s2 = s2.replace(" ", "").replace("\n", "").replace("\r", "").replace("\'", "").replace("\"", "");
			//计算余弦相似度
			double score = similarity.getSimilarity(knowledege.article_content, other.article_content);
//			System.out.println("文章内容关联度:"+score);
			//若文章过于相似,则以0.9概率不通过
			if(score > AVERGE_SCORE){
				double pass_pro = r.nextDouble();
				if(pass_pro>0.1){
					System.out.println("\""+knowledege.article_link+"\"与\""+other.article_link+"\" 过于相似,相似度为:"+score);
					return true;
				}
			}
		}
		//若文章不相似,以0.99概率通过
		double pass_pro = r.nextDouble();
		if(pass_pro>0.01){
			return false;
		}
		return true;
	}

我对文章的审核就从以上几个方面完成,下面是运行结果:


由于系统要去利用百度搜索指数,所以在运行速度会受网络状况影响。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭