論壇評論提取\論壇內容提取\論壇使用者資訊提取

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

背景

參加泰迪杯資料探勘競賽,這次真的學習到了不少東西,最後差不多可以完成要求的內容,準確率也還行。總共的程式碼,算上中間的過程處理也不超過500行,程式碼思想也還比較簡單,主要是根據論壇的短文字特性和樓層之間內容的相似來完成的。(通俗點說就是去噪去噪去噪,然後只留下相對有規律的日期,內容)

  PS:(本人長期出售超大量微博資料、旅遊網站評論資料,並提供各種指定資料爬取服務,Message to [email protected]。同時歡迎加入社交媒體資料交流群:99918768)

前期準備

軟體和開發環境: Pycharm,Python2.7,Linux系統
用的主要Python包: jieba, requests, BeautifulSoup, goose, selenium, PhantomJS, pymongo等(部分軟體的安裝我前面的部落格有介紹)

網頁預處理

首先因為網站很多是動態的,直接用bs4是獲取不到有些資訊的,所以我們使用selenium和phantomjs將檔案儲存在本地,然後再處理。

相關的程式碼是

def save(baseUrl):
driver = webdriver.PhantomJS()
driver.get(baseUrl) # seconds
try:
element = WebDriverWait(driver, 10).until(isload(driver) is True)
except Exception, e:
print e
finally:
data = driver.page_source  # 取到載入js後的頁面content
driver.quit()
return data

由於網頁中存在著大量的噪音(廣告,圖片等),首先我們需要將與我們所提取內容不一致的所有噪聲儘可能去除。我們首先選擇將一些帶有典型噪聲意義的噪聲標籤去除,比如script等,方法我們選擇BeautifulSoup來完成。

程式碼大概是這樣

    for element in soup(text=lambda text: isinstance(text, Comment)):
element.extract()
[s.extract() for s in soup('script')]
[s.extract() for s in soup('meta')]
[s.extract() for s in soup('style')]
[s.extract() for s in soup('link')]
[s.extract() for s in soup('img')]
[s.extract() for s in soup('input')]
[s.extract() for s in soup('br')]
[s.extract() for s in soup('li')]
[s.extract() for s in soup('ul')]
print (soup.prettify())

處理之後的網頁對比

可以看出網頁噪聲少了很多,但是還是不足以從這麼多噪聲中提取出我們所要的內容

由於我們不需要標籤只需要標籤裡面的文字,所以我們可以利用BeautifulSoup提取出文字內容再進行分析

for string in soup.stripped_strings:
print(string)
with open(os.path.join(os.getcwd()) "/data/3.txt", 'a') as f:
f.writelines(string.encode('utf-8') '\n')

可以看出來還是非常雜亂,但是又是十分有規律的。我們可以發現每個樓層中的文字內容實質上都差不多,可以說重複的很多,而且都是一些特定的詞,比如: 直達樓層, 板凳,沙發,等這類的詞,所以我們需要將這些詞刪掉然後再進行分析

我所用的方法是利用jieba分詞來對獲取的網頁文字進行分詞,統計出出現詞頻最高的詞,同時也是容易出現在噪聲文章中的詞語,程式碼如下

import jieba.analyse
text = open(r"./data/get.txt", "r").read()
dic = {}
cut = jieba.cut_for_search(text)
for fc in cut:
if fc in dic:
dic[fc]  = 1
else:
dic[fc] = 1
blog = jieba.analyse.extract_tags(text, topK=1000, withWeight=True)
for word_weight in blog:
# print (word_weight[0].encode('utf-8'), dic.get(word_weight[0], 'not found'))
with open('cut.txt', 'a') as f:
f.writelines(word_weight[0].encode('utf-8')   "    "   str(dic.get(word_weight[0], 'not found'))   '\n')

統計出來然後經過我們測試和篩選得出的停用詞有這些

回帖
積分
帖子
登入
論壇
註冊
離線
時間
作者
簽到
主題
精華
客戶端
手機
下載
分享

目前統計的詞大約200左右。

然後還有去除重複文字的工作

# 去重函式
def remove_dup(items):
pattern1 = re.compile(r'發表於')
pattern2 = re.compile('\d{4}-\d{1,2}-\d{1,2} \d{2}:\d{2}:\d{2}')
pattern3 = re.compile('\d{1,2}-\d{1,2} \d{2}:\d{2}')
pattern4 = re.compile('\d{4}-\d{1,2}-\d{1,2} \d{2}:\d{2}')
pattern5 = re.compile(r'[^0-9a-zA-Z]{7,}')
# 用集合來作為容器,來做一部分的重複判斷依據,另外的部分由匹配來做
# yield用於將合適的文字用生成器得到迭代器,這樣就進行了文字的刪除,在函式外面
# 可以用函式進行文字的迭代
seen = set()
for item in items:
match1 = pattern1.match(item)
match2 = pattern2.match(item)
match3 = pattern3.match(item)
match4 = pattern4.match(item)
match5 = pattern5.match(item)
if item not in seen or match1 or match2 or match3 or match4 or match5:
yield item
seen.add(item)  # 向集合中加入item,集合會自動化刪除掉重複的專案

在經過觀察處理後的網頁文字,我們發現還有一項噪聲無法忽略,那就是純數字。因為網頁文字中有很多純數字但是又不重複,比如點贊數等,所以我準備用正則匹配出純數字然後刪除。但是這樣就會出現問題…因為有些使用者名稱是純數字的,這樣我們會把使用者名稱刪掉的。為了解決這個問題我們使用保留字元數大於7的純數字,這樣既刪除了大部分的沒用資訊又儘可能的保留了使用者名稱

相關的程式碼如下

st = []
for stop_word in stop_words:
st.append(stop_word.strip('\n'))
t = tuple(st)
# t,元組,和列表的區別是,不能修改使用(,,,,),與【,,,】列表不同
lines = []
# 刪除停用詞和短數字實現
for j in after_string:
# 如果一行的開頭不是以停用詞開頭,那麼讀取這一行
if not j.startswith(t):
# 如何一行不全是數字,或者這行的數字數大於7(區別無關數字和數字使用者名稱)讀取這一行
if not re.match('\d $', j) or len(j) > 7:
lines.append(j.strip())
# 刪除所有空格並輸出
print (j.strip())

處理之後的文字如下,規律十分明顯了

接下來就是我們進行內容提取的時候了

內容提取

內容提取無非是找到評論塊,而評論塊在上面我們的圖中已經十分清晰了,我們自然而然的想到根據日期來區分評論塊。經過觀察,所有的論壇中日期的形式只有5種(目前只看到5種,當然後期可以加上)。我們可以用正則匹配出日期所在的行,根據兩個日期所在行數的中間所夾的就是評論內容和使用者名稱來完成我們的評論內容提取。

傳入我們處理後的文字然後就匹配出日期所在行數

# 匹配日期返回get_list
def match_date(lines):
pattern1 = re.compile(r'發表於')
pattern2 = re.compile('\d{4}-\d{1,2}-\d{1,2} \d{2}:\d{2}:\d{2}')
pattern3 = re.compile('\d{1,2}-\d{1,2} \d{2}:\d{2}')
pattern4 = re.compile('\d{4}-\d{1,2}-\d{1,2} \d{2}:\d{2}')
pattern5 = re.compile(r'發表日期')
pre_count = -1
get_list = []
# 匹配日期文字
for string in lines:
match1 = pattern1.match(string)
match2 = pattern2.match(string)
match3 = pattern3.match(string)
match4 = pattern4.match(string)
match5 = pattern5.match(string)
pre_count  = 1
if match1 or match2 or match3 or match4 or match5:
get_dic = {'count': pre_count, 'date': string}
get_list.append(get_dic)
# 返回的是匹配日期後的資訊
return get_list

因為有回帖和沒有回帖處理方式也不一樣所以我們需要分類進行討論。因為我們知道評論的內容是在兩個匹配日期的中間,這樣就有一個問題就是最後一個評論的內容區域不好分。但是考慮到大部分的最後一個回帖都是一行我們可以暫取值為3(sub==3,考慮一行評論和一行使用者名稱),後來想到一種更為科學的方法,比如判斷後面幾行的文字密度,如果很小說明只有一行評論的可能性更大。

下面的程式碼是獲取日期所在行數和兩個日期之間的行數差

# 返回my_count
def get_count(get_list):
my_count = []
date = []
# 獲取時間所在行數
for i in get_list:
k, t = i.get('count'), i.get('date')
my_count.append(k)
date.append(t)
if len(get_list) > 1:
# 最後一行暫時取3
my_count.append(my_count[-1]   3)
return my_count
else:
return my_count
# 獲取兩個時間所在的行數差
def get_sub(my_count):
sub = []
for i in range(len(my_count) - 1):
sub.append(my_count[i   1] - my_count[i])
return sub

接下來就要分類討論了

如果只有樓主沒有評論(即my——count==1),這個時候我們可以使用開源的正文提取軟體goose來提取正文。
如果有評論我們就需要根據sub的值來進行分類如果sub==2佔多數(或者說比sub==3)佔的多,那麼我們就認為可能是使用者名稱被刪掉,刪掉的原因有很多,比如去重的時候有人在樓中樓回覆了導致使用者名稱重複被刪除,有可能該網站的標籤比較特殊使用者名稱在去標籤的時候刪除等,情況比較複雜且出現的頻率不太高,暫未考慮。何況不影響我們提取評論內容,只需分類出來考慮就行

<font color=#FF0000 size=4 face=”黑體”>
注意:下面餘弦相似度這個是我開始的時候想多了!大部分情況就是:日期-評論-使用者名稱,後來我沒有考慮餘弦相似度分類,程式碼少了,精度也沒有下降。這裡不刪是想留下一個思考的過程。程式碼看看就好,最後有修改後的原始碼。
</font>

還有就是最常見的內容,就是sub==3佔多數的情況。因為大部分的評論都是一行文字,所以我們需要考慮的的是sub==3的時候獲取的評論文字在哪一行。通俗來說就是這三行的內容是日期-評論-使用者名稱,還是日期-使用者名稱-評論呢?雖然大部分是第一種情況,但是第二種情況我們也不能忽略。怎麼判斷這兩種情況呢?這確實讓我思考了很長一段時間,後來想到可以用餘弦相似度來解決這個問題.科普餘弦相似度可以看這裡。簡單來說就是使用者名稱的長度都是相似的,但是評論的內容長度差異就非常大了。比如使用者名稱長度都是7個字元左右,但是評論的長度可以數百,也可以只有一個。所以我們可以兩兩比較餘弦相似度,然後取平均,相似度大的就是使用者名稱了。這樣我們就可以區分出評論內容進行提取了!這就是主要的思想。剩下的就是程式碼的實現了。

簡單貼一下相關的程式碼

# 利用goose獲取正文內容
def goose_content(my_count, lines, my_url):
g = Goose({'stopwords_class': StopWordsChinese})
content_1 = g.extract(url=my_url)
host = {}
my_list = []
host['content'] = content_1.cleaned_text
host['date'] = lines[my_count[0]]
host['title'] = get_title(my_url)
result = {"post": host, "replys": my_list}
SpiderBBS_info.insert(result)
# 計算餘弦相似度函式
def cos_dist(a, b):
if len(a) != len(b):
return None
part_up = 0.0
a_sq = 0.0
b_sq = 0.0
for a1, b1 in zip(a, b):
part_up  = a1 * b1
a_sq  = a1 ** 2
b_sq  = b1 ** 2
part_down = math.sqrt(a_sq * b_sq)
if part_down == 0.0:
return None
else:
return part_up / part_down
# 判斷評論內容在哪一行(可能在3行評論塊的中間,可能在三行評論塊的最後)
def get_3_comment(my_count, lines):
get_pd_1 = []
get_pd_2 = []
# 如果間隔為3取出所在行的文字長度
test_sat_1 = []
test_sat_2 = []
for num in range(len(my_count)-1):
if my_count[num 1] - 3 == my_count[num]:
pd_1 = (len(lines[my_count[num]]), len(lines[my_count[num] 2]))
get_pd_1.append(pd_1)
pd_2 = (len(lines[my_count[num]]), len(lines[my_count[num] 1]))
get_pd_2.append(pd_2)
for i_cos in range(len(get_pd_1)-1):
for j_cos in range(i_cos 1, len(get_pd_1)):
# 計算文字餘弦相似度
test_sat_1.append(cos_dist(get_pd_1[j_cos], get_pd_1[i_cos]))
test_sat_2.append(cos_dist(get_pd_2[j_cos], get_pd_2[i_cos]))
# 計算餘弦相似度的平均值
get_mean_1 = numpy.array(test_sat_1)
print (get_mean_1.mean())
get_mean_2 = numpy.array(test_sat_2)
print (get_mean_2.mean())
# 比較大小返回是否應該按
if get_mean_1.mean() >= get_mean_2.mean():
return 1
elif get_mean_1.mean() < get_mean_2.mean():
return 2
# 獲取評論內容
def solve__3(num, my_count, sub, lines, my_url):
# 如果get_3_comment()返回的值是1,那麼說明最後一行是使用者名稱的可能性更大,否則第一行是使用者名稱的可能性更大
if num == 1:
host = {}
my_list = []
host['content'] = ''.join(lines[my_count[0] 1: my_count[1] sub[0]-1])
host['date'] = lines[my_count[0]]
host['title'] = get_title(my_url)
for use in range(1, len(my_count)-1):
pl = {'content': ''.join(lines[my_count[use]   1:my_count[use   1] - 1]), 'date': lines[my_count[use]],
'title': get_title(my_url)}
my_list.append(pl)
result = {"post": host, "replys": my_list}
SpiderBBS_info.insert(result)
if num == 2:
host = {}
my_list = []
host['content'] = ''.join(lines[my_count[0] 2: my_count[1] sub[0]])
host['date'] = lines[my_count[0]]
host['title'] = get_title(my_url)
for use in range(1, len(my_count) - 1):
pl = {'content': ''.join(lines[my_count[use]   2:my_count[use   1]]), 'date': lines[my_count[use]],
'title': get_title(my_url)}
my_list.append(pl)
result = {"post": host, "replys": my_list}
SpiderBBS_info.insert(result)

展望

提取的準確率應該要分析更多的bbs網站,優化刪除重複詞(太粗暴),優化停用詞,針對短文字沒回復情況的優化,準確提取樓主的使用者名稱等,無奈時間太緊無法進一步優化。才疏學淺,剛學了幾個月python,程式碼難免有不合理的地方,望各位提出寶貴意見。

撒一波廣告

本人長期出售抓取超大量微博資料的程式碼,並提供微博資料打包出售,Message to [email protected]

個人部落格

8aoy1.cn

相關文章

伺服器 最新文章