设为首页 收藏本站
查看: 3714|回复: 0

[经验分享] python通用论坛正文提取\python论坛评论提取\python论坛用户信息提取

[复制链接]

尚未签到

发表于 2018-8-4 14:02:47 | 显示全部楼层 |阅读模式
  本人长期出售超大量微博数据、旅游网站评论数据,并提供各种指定数据爬取服务,Message to YuboonaZhang@Yahoo.com。同时欢迎加入社交媒体数据交流群:99918768

背景

  参加泰迪杯数据挖掘竞赛,这次真的学习到了不少东西,最后差不多可以完成要求的内容,准确率也还行。总共的代码,算上中间的过程处理也不超过500行,代码思想也还比较简单,主要是根据论坛的短文本特性和楼层之间内容的相似来完成的。(通俗点说就是去噪去噪去噪,然后只留下相对有规律的日期,内容)


前期准备


  •   软件和开发环境: 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())
  

  

  处理之后的网页对比
DSC0000.png

DSC0001.png

  可以看出网页噪声少了很多,但是还是不足以从这么多噪声中提取出我们所要的内容
  由于我们不需要标签只需要标签里面的文字,所以我们可以利用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')
  

DSC0002.png

  可以看出来还是非常杂乱,但是又是十分有规律的。我们可以发现每个楼层中的文本内容实质上都差不多,可以说重复的很多,而且都是一些特定的词,比如: 直达楼层, 板凳,沙发,等这类的词,所以我们需要将这些词删掉然后再进行分析
  我所用的方法是利用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())
  

  处理之后的文本如下,规律十分明显了
DSC0003.png

  接下来就是我们进行内容提取的时候了

内容提取
  内容提取无非是找到评论块,而评论块在上面我们的图中已经十分清晰了,我们自然而然的想到根据日期来区分评论块。经过观察,所有的论坛中日期的形式只有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)
  return sub
  

  接下来就要分类讨论了


  •   如果只有楼主没有评论(即my——count==1),这个时候我们可以使用开源的正文提取软件goose来提取正文。

  • 如果有评论我们就需要根据sub的值来进行分类如果sub==2占多数(或者说比sub==3)占的多,那么我们就认为可能是用户名被删掉,删掉的原因有很多,比如去重的时候有人在楼中楼回复了导致用户名重复被删除,有可能该网站的标签比较特殊用户名在去标签的时候删除等,情况比较复杂且出现的频率不太高,暂未考虑。何况不影响我们提取评论内容,只需分类出来考虑就行

  <font color=#FF0000>  注意:下面余弦相似度这个是我开始的时候想多了!大部分情况就是:日期-评论-用户名,后来我没有考虑余弦相似度分类,代码少了,精度也没有下降。这里不删是想留下一个思考的过程。代码看看就好,最后有修改后的源码。
  </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,代码难免有不合理的地方,望各位提出宝贵意见。

个人博客
  8aoy1.cn

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-546553-1-1.html 上篇帖子: 【13】Python之常用文件操作 下篇帖子: Python JS Jquery Json 转换关系
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表