基于DBSCAN聚类算法的通用论坛正文提取

这是今年和队友一起参加第五届泰迪杯的赛题论文,虽然最终只获得了一个三等奖。但是在这个过程中和队友也一起学到了不少东西,特此记录。

1、  简单介绍

赛题的目的,是让参赛者对于任意 BBS 类型的网页,获取其 HTML 文本内容,设计一个智能提取该页面的主贴、所有回帖的算法

http://www.tipdm.org/jingsa/1030.jhtml?cName=ral_100#sHref赛题地址。

2、  前期准备

由于之前没有接触过爬虫,我和队友首先了解了目前主流的用于爬虫的语言和框架,最终选择了对初学者比较友好的Python中bs4框架。之后便是学习了一些简单的Python用于爬虫的基本知识,正则表达式,url包等。

对于赛题,我们首先了解到爬虫分为静态网页、动态网页和web service,我们只对其中的静态网页进行了研究,对于动态网页的比较复杂,由于时间比较紧张,没有深入研究,对于一些网站的反扒,也没有深入了解。所以接下来主要说在如何设计一个通用的静态网页爬虫框架。(我想这也是我们失分的一部分吧)

思路:

         对于一个普通的网站,我们可能采用正则表达式来抓取我们想要的内容,但是做到通用性显然有点强人所难。首先我们从剖析整个网页结构也就是DOM树,然后对DOM进行分析,得到主贴节点和回帖节点的特征,对相似网页的特征进行聚类,其中聚类算法选择了DBSCAN(因为他可以自动分成几类,不需要人为设定)。然后形成一个统一的模板,这样就会减少了我们的工作量。

3、  整体流程

在官方给定的177个url的基础上,我们自行爬取了736个论坛的url。然后使用736个网页进行聚类,形成模板,使用177个url进行测试

对爬取的736个url进行分析,得到以下结果。


可以看出,大多数论坛网站是由开源框架编写,discuz占多数。但是不同版本的开源框架,结构也会不同,因此不能使用同一个模板。

 

结构相似度计算:

   首先我们对网页结构进行解析,得到主贴节点和回帖节点的XPATH值


单个网页的XPTH特征可以表述为: 
然后采用dbscan聚类算法,其中两个网页距离的定义如下


 
其中 表示网页i中特征的个数, 表示网页j中特征的个数;overlap 表示两个网页相同的特征的个数,当两个网页相同特征个数越多时公式(2)的值越趋近于0。
注:在聚类之前,对每一个xpath进行的预处理,去处了如数字、符号等无关特征


内容相似度计算:
主要是对URL进行相似度计算。
     ,分析URL的后半部分。
整体网页相似度计算:
 


其中S1,S2是网页或簇中心, 是特征i的权重, 是特征i的相似度。通过DBSCAN聚类算法得到初始簇之后,并根据以后的测试数据来不断的更新特征库,从而能动态的更新权重,获得更好的聚类效果。


正文提取流程


通过URL和 XPath模板匹配,可以完成对论坛页面的识别和过滤,进而对论坛中正文信息进行识别和抽取。同时,我们可以看到当测试的不同网站越来越多时,XPath库和模板库将会越来越丰富,这是一个不断学习的过程。

不同参数聚类结果:

E=0,minPts = 4

E=0,minPts =8

簇类别

比重

网页类别

簇类别

比重

网页类别

1

0.667

discuz

1

0.705

discuz

8

0.089

非开源

5

0.092

phpwind

5

0.0278

phpwind

2

0.041

dvbbs

2

0.0222

dvbbs

6

0.023

非开源

10

0.0222

非开源

10

0.023

非开源

        E=1,minPts = 4

        E=1,minPts = 8

簇类别

比重

网页类别

簇类别

比重

网页类别

1

0.630

Discuz

1

0.628

Discuz

3

0.205

非开源

3

0.129

非开源

9

0.123

非开源

2

0.087

dvbbs

4

0.0871

phpwind

4

0.051

phpwind

2

0.051

dvbbs

9

0.021

非开源





















不同参数得到的簇数量:

不同参数得到的簇数量:

参数

E=0,minPts = 4

E=0,minPts =8

E=1,minPts = 4

E=1,minPts = 8

簇个数

23

18

16

14

簇中论坛总数

173

173

194

194

离群点

23

23

10

10

 

测试结果:

论坛网站

测试帖子

成功抽取

guba.sina.com.cn

13

13

club.autohome.com.cn

11

11

club.qingdaonews.com

9

9

bbs.tianya.cn

8

8

bbs.360.cn

5

5

bbs1.people.com.cn

5

0

bbs.pcauto.com.cn

5

5

bbs.dospy.com

4

5

bbs.hsw.cn

4

4

itbbs.pconline.com.cn

4

4

www.dddzs.com

4

4

bbs.hupu.com

4

4

bbs.ent.qq.com

3

0

bbs.e23.cn

3

3

bbs.lady.163.com

1

0

www.099t.com

1

0

部分抽取结果:



总结:用的方法比较传统,只能做到大部分论坛抽取,但是随着数量的积累,效果越好。没有用的现在比较火的nlp(应该有同学会用到了),对结果没有进行过多的过滤。只对正文和发帖时间,主从贴进行细分,对发帖人没有得到有效的解决方法。需要学习的地方还很多。如有错误,欢迎指正。

DBSCAN代码:

[html]  view plain  copy
  1. <pre code_snippet_id="2437644" snippet_file_name="blog_20170607_1_2749775" name="code" class="html">#encoding:utf-8  
  2. '''  
  3. Created on 2017年4月12日  
  4. '''  
  5. from collections import defaultdict    
  6. import re    
  7.    
  8. '''  
  9. function to calculate distance  use define formula,  
  10. (len(i)*len(j)+1)/(overlap*overlap+1)-1  
  11. parameter   
  12. url1{url,xpath,feanum}  
  13. url2{url,xpath,feanum}  
  14. split /t maybe have counter with /table   
  15. '''  
  16. def dist(url1, url2):    
  17.     values1=url1.split('\t')  
  18.     values2=url2.split('\t')  
  19.     #得到xpath  
  20.     xpath_val1=values1[1][2:].split('/')  
  21.     xpath_val2=values2[1][2:].split('/')  
  22.     #得到两个xpath特征个数最小的一个  
  23.     size = len(xpath_val1) if len(xpath_val1) < len(xpath_val2) else len(xpath_val2)  
  24.     #得到overlap  
  25.     overlap=0      
  26.     for i in range(size):  
  27.         x1=re.sub(r'
    +
    ','',re.sub(r'((\d+))','',xpath_val1[i]))  
  28.         x2=re.sub(r'
    +
    ','',re.sub(r'((\d+))','',xpath_val2[i]))  
  29.         if( x1==x2):  
  30.             overlap+=1  
  31.     return ((len(xpath_val1)*len(xpath_val2)+1)/(overlap**2+1)-1)  
  32.     
  33. #将所有的样本装入 all_points中   
  34.   
  35. def init_sample(path):  
  36.     all_points=[]    
  37.     lines = open(path)  
  38.     for i in lines:     
  39.         a=[]  
  40.         a.append(i)  
  41.         all_points.append(a)    
  42.     return all_points  
  43. all_points=init_sample('../../train_bbs_urls.txt')  
  44.   
  45. '''  
  46. take radius = 8 and min.points = 8    
  47. '''  
  48. E = 0    
  49. minPts = 8   
  50.        
  51. #find out the core points    
  52. other_points =[]    
  53. core_points=[]    
  54. plotted_points=[]    
  55. for point in all_points:    
  56.     point.append(0)  # assign initial level 0    
  57.     total = 0  
  58.     for otherPoint in all_points:    
  59.         distance = dist(otherPoint[0],point[0])    
  60.         if distance<=E:    
  61.             total+=1         
  62.     if total > minPts:    
  63.         core_points.append(point)    
  64.         plotted_points.append(point)    
  65.     else:    
  66.         other_points.append(point)    
  67.   
  68. #find border points    
  69. border_points=[]    
  70. for core in core_points:    
  71.     for other in other_points:    
  72.         if dist(core[0],other[0])<=E:    
  73.             border_points.append(other)    
  74.             plotted_points.append(other)    
  75.             other_points.remove(other)  
  76.           
  77. #implement the algorithm    
  78. cluster_label=0    
  79. print len(core_points)       
  80. a=0  
  81. for point in core_points:    
  82.     if point[1]==0:    
  83.         cluster_label +=1    
  84.         point[1]=cluster_label           
  85.     for point2 in plotted_points:    
  86.         distance = dist(point2[0],point[0])    
  87.         if point2[1] ==0 and distance<=E:    
  88. #             print (point, point2 )   
  89.             point2[1] =point[1]    
  90.            
  91. for i in plotted_points:  
  92.     print i[0],'    ',i[1]  
  93.     output=i[0].replace('\n','')+'\t'+str(i[1]).strip()  
  94.     open('dbscan.txt','a+').write('\n'+output.encode('utf-8'))   
  95.           
  96. #after the points are asssigned correnponding labels, we group them    
  97. cluster_list = {}  
  98. for point in plotted_points:    
  99.     va=point[0].split('\t')  
  100.     start=va[0].find('//')  
  101.     stop=va[0].find('/',start+2)  
  102.     name=va[0][start+2:stop]      
  103.     if name not in cluster_list:  
  104.         cluster_list[name] =point[1]  
  105. #     else:  
  106. #         core=cluster_list.get(point[1]).split('\t')  
  107. #         if name!=core[len(core)-1]:  
  108. #             cluster_list[point[1]] =cluster_list.get(point[1])+'\t'+name  
  109. other_list = {}  
  110. for point in other_points:    
  111.     print 'aaaa'  
  112.     va=point[0].split('\t')  
  113.     start=va[0].find('//')  
  114.     stop=va[0].find('/',start+2)  
  115.     name=va[0][start+2:stop]      
  116.     if name not in other_list:  
  117.         print name  
  118.         other_list[name] =point[1]  
  119.           
  120. # for i in cluster_list.keys():  
  121. #     print 'i=',i  
  122. #     output=str(i)+'\t'+str(cluster_list.get(i))  
  123. #     print output  
  124. #     open('dbscantype.txt','a+').write('\n'+output.encode('utf-8'))   
  125. #   
  126. # for i in other_list.keys():  
  127. #     print 'i=',i  
  128. #     output=str(i)+'\t'+str(cluster_list.get(i))  
  129. #     print output  
  130. #     open('other_list.txt','a+').write('\n'+output.encode('utf-8'))   
  131. </pre><br>  
  132. <br>  
  133. <pre></pre>  
  134. <p></p>  
  135. <pre></pre>  
  136. <br>  
  137. <br>  
  138. <p></p>  
  139. <p><br>  
  140. </p>  
  141. <p><br>  
  142. </p>  
  143. <br>  
  144. <p><span style="font-size:10pt; font-family:宋体"><br>  
  145. </span></p>  
  146. <pre></pre>  
  147.      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值