通用论坛正文提取\论坛数据提取\论坛评论提取\论坛用户信息爬虫

本人长期出售超大量微博数据、旅游网站评论数据,并提供各种指定数据爬取服务,Message to YuboonaZhang@Yahoo.com。同时欢迎加入社交媒体数据交流群:99918768

背景

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

前期准备

  1. 软件和开发环境: Pycharm,Python2.7,Linux系统

  2. 用的主要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

接下来就要分类讨论了

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

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

<font color=#FF0000 size=4 face="黑体">
注意:下面余弦相似度这个是我开始的时候想多了!大部分情况就是:日期-评论-用户名,后来我没有考虑余弦相似度分类,代码少了,精度也没有下降。这里不删是想留下一个思考的过程。代码看看就好,最后有修改后的源码。
</font>

  1. 还有就是最常见的内容,就是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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,417评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,591评论 18 139
  • 抓取京东商城商品评论信息,并对这些评论信息进行分析和可视化。下面是要抓取的商品信息,一款女士文胸。这个商品共有红色...
    _hahaha阅读 6,113评论 2 7
  • 想来许久不见,定会甚是想念? 想来这么久了又见,仍然有些害怕 今日有些犯困,听着歌就睡着了,后来大约播了首欢快的歌...
    洛云端阅读 181评论 2 0
  • 摘抄于百度 100 Continue 初始的请求已经接受,客户应当继续发送请求的其余部分。(HTTP 1.1新)1...
    麒缘伊声阅读 449评论 0 0