爬虫从新闻资讯类html提取正文和发布时间算法

未完待续。。。。。。
第六版
截止目前的版本主要优化了一下几点:

  • 速度提升6倍以上
  • 正文提取噪音较多
  • 部分正文无法提取丢失
  • 部分发布时间无法提取
import re
from copy import deepcopy
from lxml import etree


"""
TO DO
如果正文中存在超链接文本,存在被剔除的风险
"""

class HtmlContentExtract:

    def __init__(self,htmltext):
        """
        :param htmltext:html文本
        抽取正文的 文章详情 发布日期
        :return
        """
        tree = etree.HTML(htmltext)
        scripts = tree.xpath('//script/text()')
        contents = tree.xpath('//*/text() | //br/following::text()[1]')

        # 剔除超链接文本
        for pattern in tree.xpath('//*'):
            if pattern.xpath('./@href'):
               text = pattern.xpath('./text()')
               if text:
                   contents.remove(text[0])

        # 剔除script 文本
        for i in scripts:
            try:
                contents.remove(i)
            except:
                pass
        self.text = contents

    def countwrap(self):
        """
        统计不同段落间的换行数,值为当前元素与下个元素的距离
        给文本从上到下,编号,排序
        根据不同段落间的换行数(间距)和排序位置 确定段落性质
        该算法会漏掉最后一个文本的计数(文本之间丢弃),

        需注意字典的键有可能重复
        :return {'data': {1: ['铜山区人民政府', 10], 2: ['铜山区人民政府', 5], 3: ['高级搜索', 9], 4: ['首页', 1], 5: ['走进铜山', 1], 6: ['信息公开', 1], 7: ['公共服务', 1], 8: ['政民互动', 1], 9: ['新闻中心', 1], 10: ['专题专栏', 1], 11: ['铜山论坛', 4], 12: ['铜山发布', 0], 13: ['扫描二维码', 1], 14: ['��', 0], 15: ['铜山区预决算公开平台', 6], 16: ['当前位置', 0], 17: [':', 0], 18: ['首页', 0], 19: [' > ', 0], 20: ['新闻中心', 0], 21: [' > ', 0], 22: ['本地新闻', 0], 23: [' > ', 0], 24: ['政务要闻', 3], 25: ['高建民检查安全生产工作', 1], 26: ['发布日期:2019-07-02\xa0作者:常成龙\xa0\xa0\xa0\xa0 点击:', 0], 27: [' \xa0\xa0\xa0\xa0字体: [ ', 0], 28: ['大', 1], 29: ['中', 1], 30: ['小', 0], 31: [' ]  ', 2], 32: ['\xa0\xa0\xa0 6月29日下午,区长高建民率公安局、住建局、城管局、生态环境局等相关单位检查安全生产工作。高建民一行实地查看了美的雍翠园建筑工地、汉王瑞祥烟花爆竹仓库、刘集镇徐州海飞箱桥制造有限公司等地,详细了解企业的安全生产工作规程、消防设施配备以及重点区域重点环节安全生产制度措施落实情况。高建民要求,要时刻绷紧安全生产这根弦,加强日常运行的安全管理,严格落实安全生产各项制度措施,严防各类安全事故发生。企业要切实担负起主体责任,把安全生产牢牢扛在肩上、记在心上、抓在手上、落实在行动上,为企业稳定、健康发展提供坚强的安全保障。各有关部门和单位要明确职责,认真落实监管职责,切实把各项安全生产措施做得更扎实,把各类安全隐患排查治理得更到位。', 4], 33: ['分享到:', 3], 34: ['关闭页面', 0], 35: ['|', 0], 36: ['打印', 0], 37: ['|', 0], 38: ['收藏', 6], 39: ['联系我们', 0], 40: [' | ', 0], 41: ['站点地图', 0], 42: [' | ', 0], 43: ['收藏本站', 1], 44: [' 主办单位:中共铜山区委 铜山区人民政府\xa0\xa0\xa0承办单位:中共铜山区委宣传部\xa0\xa0\xa0版权所有:徐州市铜山区人民政府办公室\n            ', 0], 45: ['备案序号:', 0], 46: ['苏ICP备09062975号-1', 0]}, 'ElementCount': 46}

        """

        # 对\n 进行计数
        num = 0
        # 文本与\n之间的状态,0代表上一个元素为空
        start = 0
        end = 0
        # 记录元素的位置
        position = 0

        # 缓存当前比对区间第一个文本
        NowElement = None
        # json保存统计结果
        result_json = {'data': {}}

        for i in self.text:
            realcontent = i.replace('\n', '').strip()
            if start == 0:
                if len(realcontent) > 5:
                    start = 1
                    # 将原始元素(文本)赋值给变量
                    NowElement = i
                    position += 1
                # else:
                #     num += 1
            else:
                # 中间空置计数
                if not realcontent:
                    num += 1

                # 下一个有文本,end = 1
                else:
                    end = 1
                    # 一个统计区间结束
                    if start and end:
                        # if NowElement in result_json:
                        #     print(NowElement)
                        result_json['data'][position] = {'NowElement': NowElement, 'num': num}
                        # 复位
                        num = 0
                        # 复位
                        # 将下一个比对区间的第一文本替换为上一个比对区间的后一个文本
                        NowElement = i
                        position += 1
        return result_json


    def combination(self):
        """
        将相邻的元素且间距为0 且字符长度大于5的组合
        :return:
        """

        data = self.countwrap()
        datacopy = deepcopy(data['data'])
        CombinNum = {}
        # 记录换行书为0 的序号
        nownum = 0
        firstnum = None
        # 每组的最后一个num为0的元素,记录其序号
        lastnum = 0
        for ele in data['data']:

            # 在换行等于0并且是相邻元素的时候,放入一个列表
            if data['data'][ele]['num'] == 0:
                # 整个json的第一组的第一个元素
                if nownum == 0:
                    firstnum = ele

                    CombinNum[firstnum] = [data['data'][ele]['NowElement']]
                    # 将字典中该条数据剔除
                    datacopy.pop(ele)
                    # 将当前序号赋值给零时计数器 nownum
                    nownum = ele
                # 判断是否相邻,相邻的就放入一个列表
                elif ele - nownum == 1:
                    CombinNum[firstnum].append(data['data'][ele]['NowElement'])
                    datacopy.pop(ele)
                    # 将当前序号赋值给零时计数器 nownum
                    nownum = ele
                    lastnum = ele
                # 如果是当前的一组比对完,经过几个噪音,到下一组第一个的时候,从新开辟一个新的key
                else:
                    # nownum = 0
                    firstnum = ele
                    CombinNum[firstnum] = [data['data'][ele]['NowElement']]
                    datacopy.pop(ele)
                    # 将当前序号赋值给零时计数器 nownum
                    nownum = ele
            # 每组最后一个元素即使num不是0,依旧应该添加到一组中,因为上一个元素和它中间换行数是0
            else:
                if (ele > 1) and (ele - nownum == 1):
                    # 记录每组最后一个元素与下一个元素的换行数
                    CombinNum[firstnum].append(data['data'][ele]['NowElement'] + "--{}".format(data['data'][ele]['num']))
                    datacopy.pop(ele)
        # 如果是孤立的一个元素 并且不包含时间,就剔除
        pattern = re.compile(r'(20\d{2}[_,/,\-,年]\d{1,2}[/,_,\-,月]\d{0,2})(\s{1}\d{2}:\d{2}:\d{2}){0,1}')
        CombinNumCopy = deepcopy(CombinNum)
        for element in CombinNum:
            if len(CombinNum[element]) == 1 and not re.findall(pattern,CombinNum[element][0]):
                CombinNumCopy.pop(element)
        LastCombinNum = deepcopy(CombinNumCopy)
        for element in CombinNumCopy:
            try:
                newcontent = ''.join(CombinNumCopy[element]).split('--')
                LastCombinNum[element] = {'NowElement': newcontent[0], 'num': int(newcontent[1])}
            # 兼容最后一组最后一个元素 num 为0的情况
            except:
                LastCombinNum[element] = {'NowElement': ''.join(CombinNumCopy[element]), 'num': 0}

        datacopy.update(LastCombinNum)
        return datacopy

    def exclude(self):
        """
        剔除长度小于5的垃圾文本
        剔除中文占比小于50%,英文占比大于50%的文本
        剔除离散的文本(长度小于10,并且前后的换行数大于3)
        :return(list):[(1, {'NowElement': '《广东省地方志工作条例》解读-吴川市人民政府门户网站', 'num': 26}), (11, {'NowElement': '您现在所在的位置:吴川市人民政府门户网站', 'num': 0})]
        """
        data = self.combination()
        datacopy = deepcopy(data)
        # 上一个元素的num值
        upelement = 0
        #上一个元素的index
        upindex = 0
        for i in data:
            # 其他文本长度的计算也剔除空格的影响
            content_statistics = data[i]['NowElement'].strip()

            # 剔除长度小于5的垃圾文本
            if len(content_statistics) < 5:
                datacopy.pop(i)
                # 在剔除该元素之后把该元素的换行添加到上一个元素上
                if upindex != 0:
                    datacopy[upindex]['num'] = datacopy[upindex]['num'] + data[i]['num']
                continue

            # 剔除中文占比小于50 %, 英文占比大于50 % 的文本或者数字占比小于50%
            zh = re.findall(r'[\u4E00-\u9FA5]', content_statistics)
            en = re.findall('[a-zA-Z]', content_statistics)
            num = re.findall('[0-9]+', content_statistics)
            num = ''.join(num)
            if ((len(zh)/len(content_statistics)) < 0.5) and (((len(en)/len(content_statistics)) > 0.35) or ((len(num)/len(content_statistics)) < 0.2)):
                datacopy.pop(i)
                # 在剔除该元素之后把该元素的换行添加到上一个元素上
                if upindex != 0:
                    datacopy[upindex]['num'] = datacopy[upindex]['num'] + data[i]['num']
                continue

            # 剔除离散的文本(长度小于10,并且前后的换行数大于3)
            if len(content_statistics) < 10:
                # 第一个元素
                if upelement == 0:
                    datacopy.pop(i)
                    continue
                else:
                    if (upelement > 3) and (data[i]['num'] > 3):
                        datacopy.pop(i)
                        upelement = data[i]['num']
                        # 在剔除该元素之后把该元素的换行添加到上一个元素上
                        if upindex != 0:
                            datacopy[upindex]['num'] = datacopy[upindex]['num'] + data[i]['num']
                        continue

            # 只有当当前文本是正常值的时候才修改
            upindex = i
        # 按key排序
        s = sorted(datacopy.items(), key=lambda x: x[0])
        return s


    def MainBody(self):
        """
        拼接列表中所有的文本,中介加入换行
        :return:
        """
        data = self.exclude()
        # 两个段落间有被剔除文本的用\n代替
        # old = None
        # newdata = []
        # for serial in data:
        #     if old:
        #         old[1]['num'] = old[1]['num']+(serial[0]-old[0]-1)
        #         newdata.append(old)
        #
        #
        #
        #     old = serial
        # newdata.append(data[-1])
        # print(newdata)
        # data = newdata




        #提取核心文本,通过最长文本段定位文章核心位置,通过与核心位置进行悬挂高度对比,过滤非正文文本
        # 取出最长文本,及其在列表的index
        longest_content = ''
        index = None
        for serial in data:
            article_content = serial[1]['NowElement']
            if len(article_content.strip()) > len(longest_content.strip()):
                longest_content = article_content
                index = data.index(serial)
        # 拿到列表中最大的index值 与核心文本index进行比较,核心文本应该在页面中间位置,否则该页面为空:
        max_index = len(data)
        # 当 文本列表长度小于4的时候取到的核心文本,默认就是核心位置(中间)
        if max_index > 3:
            if (index/max_index) < 1/3 or (index/max_index) > 4/5:
                return ''
        # print(longest_content)
        # 判断最长文本上一个段落和下一个段落悬挂距离,大于阀值,剔除
        # 从中心往两边推,当某个段落悬挂距离突然大于正常值,以此为节点,剔除文章两端垃圾数据
        # 拿到上面切割点
        upindex = 0
        for serial in list(reversed(data[:index])):
            if serial[1]['num'] > 1:
                upindex = data.index(serial)
                break

        # 拿到下面切割点,需从核心文本开始,它的num值就是距离下一个文本的悬挂距离 ,赋值None ,当下面条件不成立的时候取到列表最后
        downindex = None
        for serial in data[index:]:
            if serial[1]['num'] > 1:
                # 之所以要+1,是为了剔除悬挂的下方 ,如果serial是最后一个元素,那么downindex会越界,需要处理
                downindex = data.index(serial)+1
                if downindex > data.index(data[-1]):
                    downindex = None
                break

        # 切除垃圾文本后的正文
        if upindex > 0:
            data = data[upindex+1:downindex]
        else:
            # 0号元素的文本就是核心文本
            data = data[upindex:downindex]
        # # 对于第一行类似标题的问题过滤
        # if data[0][1]['num'] > 1:
        #     data.pop(0)

        # 过滤文章开头的发布时间等垃圾信息
        if '来源' in data[0][1]['NowElement']:
            if data[0][1]['num'] > 1 and len(data) > 1:
                data.pop(0)
            elif len(data[0][1]['NowElement']) < 60 and data[0][1]['num'] > 0:
                data.pop(0)


        # 换行
        content = ''
        # 不换行
        content2 = ''
        for i in data:
            if i[1]['num'] > 0:
                huanhang = '\n' * i[1]['num']
            else:
                huanhang = ''
            content += (i[1]['NowElement'] + huanhang)
            content2 += i[1]['NowElement']
        # 对正文长度小于100的进行过滤 ,依据经验确实存在就一句话的新闻比如:"也许是受台风外围影响,7月20日傍晚,从玄武湖眺望南京城市上空有一种别样的美。",但是此类新闻毫无价值,故长度定位100
        if len(content2) < 100:
            return None
        return content.strip()

    def timepaser(self):
        """
        提取详情页的时间并且解析
        :param content:
        :return:
        """
        data = self.combination()
        content = ''
        for i in data:
            content += data[i]['NowElement']
        # 必须精确到日最少,再少就不考虑了
        pattern = re.compile(r'(20\d{2}[_,/,\-,年]\d{1,2}[/,_,\-,月]\d{1,2})(\s{1}\d{2}:\d{2}(:\d{2}){0,1}){0,1}')
        # 拿到文章中提取的时间[('2017-05-08', '', ''), ('2009年10月', '', ''), ('2012年2月', '', ''), ('2012年7月', '', ''), ('2012年10月23', '', '')]
        # print(content)
        date_match = re.findall(pattern,content)
        # print(date_match)
        # 将元组转化为字符串
        date_list = []
        for date_tuple in date_match:
            date_list.append(''.join(date_tuple))
        # print(date_list)
        if len(date_list) == 0:
            return None
        elif len(date_list) == 1:
            return date_list[0]
        else:
            # 当列表中时间长度不同时:列表从前往后迭代,两两对比不同的时间的长度,返回最大的
            lastdate = None
            date_str = None
            for i in date_list:
                if lastdate:
                    if i != lastdate:
                        if len(i) > len(lastdate):
                            date_str = i
                        else:
                            date_str = lastdate
                        break
                lastdate = i
            # 如果列表内所有字符串长度相同那么返回index 0
            if not date_str:
                date_str = date_list[0]
            return date_str


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

推荐阅读更多精彩内容