參加泰迪杯數據挖掘競賽,此次真的學習到了很多東西,最後差很少能夠完成要求的內容,準確率也還行。總共的代碼,算上中間的過程處理也不超過500行,代碼思想也還比較簡單,主要是根據論壇的短文本特性和樓層之間內容的類似來完成的。(通俗點說就是去噪去噪去噪,而後只留下相對有規律的日期,內容)
PS:(本人長期出售超大量微博數據、旅遊網站評論數據,並提供各類指定數據爬取服務,Message to YuboonaZhang@Yahoo.com。同時歡迎加入社交媒體數據交流羣:99918768)html
首先由於網站不少是動態的,直接用bs4是獲取不到有些信息的,因此咱們使用selenium和phantomjs將文件保存在本地,而後再處理。python
相關的代碼是web
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來完成。app
代碼大概是這樣ide
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())
處理以後的網頁對比函數
能夠看出網頁噪聲少了不少,可是仍是不足以從這麼多噪聲中提取出咱們所要的內容post
因爲咱們不須要標籤只須要標籤裏面的文字,因此咱們能夠利用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
接下來就要分類討論了
<font color=#FF0000 size=4 face="黑體">
注意:下面餘弦類似度這個是我開始的時候想多了!大部分狀況就是:日期-評論-用戶名,後來我沒有考慮餘弦類似度分類,代碼少了,精度也沒有降低。這裏不刪是想留下一個思考的過程。代碼看看就好,最後有修改後的源碼。
</font>
簡單貼一下相關的代碼
# 利用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 YuboonaZhang@Yahoo.com