通用論壇本文爬取
這是今年和隊友一起參加第五屆泰迪杯的賽題論文,雖然最終只獲得了一個三等獎。但是在這個過程中和隊友也一起學到了不少東西,特此記錄。
1、 簡單介紹
賽題的目的,是讓參賽者對於任意 BBS 類型的網頁,擷取其 HTML 常值內容,設計一個智能提取該頁面的主貼、所有回帖的演算法。
http://www.tipdm.org/jingsa/1030.jhtml?cName=ral_100#sHref賽題地址。
2、 前期準備
由於之前沒有接觸過爬蟲,我和隊友首先瞭解了目前主流的用於爬蟲的語言和架構,最終選擇了對初學者比較友好的python中bs4架構。之後便是學習了一些簡單的Python用於爬蟲的基本知識,Regex,url包等。
對於賽題,我們首先瞭解到爬蟲分為靜態網頁、動態網頁和web service,我們只對其中的靜態網頁進行了研究,對於動態網頁的比較複雜,由於時間比較緊張,沒有深入研究,對於一些網站的反扒,也沒有深入瞭解。所以接下來主要說在如何設計一個通用的靜態網頁爬蟲架構。(我想這也是我們失分的一部分吧)
思路:
對於一個普通的網站,我們可能採用Regex來抓取我們想要的內容,但是做到通用性顯然有點強人所難。首先我們從剖析整個網頁結構也就是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)-1parameter 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_pointsall_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=0for 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'+nameother_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'))