参加泰迪杯数据挖掘竞赛,此次真的学习到了很多东西,最后差很少能够完成要求的内容,准确率也还行。总共的代码,算上中间的过程处理也不超过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