通用论坛正文爬取
这是今年和队友一起参加第五届泰迪杯的赛题论文,虽然最终只获得了一个三等奖。但是在这个过程中和队友也一起学到了不少东西,特此记录。
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代码:
#encoding:utf-8 ''' Created on 2017年4月12日 ''' from collections import defaultdict import re ''' function to calculate distance use define formula, (len(i)*len(j)+1)/(overlap*overlap+1)-1 parameter url1{url,xpath,feanum} url2{url,xpath,feanum} split /t maybe have counter with /table ''' def dist(url1, url2): values1=url1.split('\t') values2=url2.split('\t') #得到xpath xpath_val1=values1[1][2:].split('/') xpath_val2=values2[1][2:].split('/') #得到两个xpath特征个数最小的一个 size = len(xpath_val1) if len(xpath_val1) < len(xpath_val2) else len(xpath_val2) #得到overlap overlap=0 for i in range(size): x1=re.sub(r'\[+\]','',re.sub(r'((\d+))','',xpath_val1[i])) x2=re.sub(r'\[+\]','',re.sub(r'((\d+))','',xpath_val2[i])) if( x1==x2): overlap+=1 return ((len(xpath_val1)*len(xpath_val2)+1)/(overlap**2+1)-1) #将所有的样本装入 all_points中 def init_sample(path): all_points=[] lines = open(path) for i in lines: a=[] a.append(i) all_points.append(a) return all_points all_points=init_sample('../../train_bbs_urls.txt') ''' take radius = 8 and min.points = 8 ''' E = 0 minPts = 8 #find out the core points other_points =[] core_points=[] plotted_points=[] for point in all_points: point.append(0) # assign initial level 0 total = 0 for otherPoint in all_points: distance = dist(otherPoint[0],point[0]) if distance<=E: total+=1 if total > minPts: core_points.append(point) plotted_points.append(point) else: other_points.append(point) #find border points border_points=[] for core in core_points: for other in other_points: if dist(core[0],other[0])<=E: border_points.append(other) plotted_points.append(other) other_points.remove(other) #implement the algorithm cluster_label=0 print len(core_points) a=0 for point in core_points: if point[1]==0: cluster_label +=1 point[1]=cluster_label for point2 in plotted_points: distance = dist(point2[0],point[0]) if point2[1] ==0 and distance<=E: # print (point, point2 ) point2[1] =point[1] for i in plotted_points: print i[0],' ',i[1] output=i[0].replace('\n','')+'\t'+str(i[1]).strip() open('dbscan.txt','a+').write('\n'+output.encode('utf-8')) #after the points are asssigned correnponding labels, we group them cluster_list = {} for point in plotted_points: va=point[0].split('\t') start=va[0].find('//') stop=va[0].find('/',start+2) name=va[0][start+2:stop] if name not in cluster_list: cluster_list[name] =point[1] # else: # core=cluster_list.get(point[1]).split('\t') # if name!=core[len(core)-1]: # cluster_list[point[1]] =cluster_list.get(point[1])+'\t'+name other_list = {} for point in other_points: print 'aaaa' va=point[0].split('\t') start=va[0].find('//') stop=va[0].find('/',start+2) name=va[0][start+2:stop] if name not in other_list: print name other_list[name] =point[1] # for i in cluster_list.keys(): # print 'i=',i # output=str(i)+'\t'+str(cluster_list.get(i)) # print output # open('dbscantype.txt','a+').write('\n'+output.encode('utf-8')) # # for i in other_list.keys(): # print 'i=',i # output=str(i)+'\t'+str(cluster_list.get(i)) # print output # open('other_list.txt','a+').write('\n'+output.encode('utf-8'))