大规模异步新闻爬虫【5】:网页正文的提取

2023-05-16

前面我们实现的新闻爬虫,运行起来后很快就可以抓取大量新闻网页,存到数据库里面的都是网页的html代码,并不是我们想要的最终结果。最终结果应该是结构化的数据,包含的信息至少有url,标题、发布时间、正文内容、来源网站等。

所以,爬虫不仅要干下载的活儿,清理、提取数据的活儿也得干。所以说嘛,写爬虫是综合能力的体现。

一个典型的新闻网页包括几个不同区域:

新闻网页区域
新闻网页区域

我们要提取的新闻要素包含在:

  • 标题区域
  • meta数据区域(发布时间等)
  • 配图区域(如果想把配图也提取)
  • 正文区域

而导航栏区域、相关链接区域的文字就不属于该新闻的要素。

新闻的标题、发布时间、正文内容一般都是从我们抓取的html里面提取的。如果仅仅是一个网站的新闻网页,提取这三个内容很简单,写三个正则表达式就可以完美提取了。然而,我们的爬虫抓来的是成百上千的网站的网页。对这么多不同格式的网页写正则表达式会累死人的,而且网页一旦稍微改版,表达式可能就失效,维护这群表达式也是会累死人的。

累死人的做法当然想不通,我们就要探索一下好的算法来实现。

1. 标题的提取

标题基本上都会出现在html的 <title> 标签里面,但是又被附加了诸如频道名称、网站名称等信息;

标题还会出现在网页的“标题区域”。

那么这两个地方,从哪里提取标题比较容易呢?

网页的“标题区域”没有明显的标识,不同网站的“标题区域”的html代码部分千差万别。所以这个区域并不容易提取出来。

那么就只剩下 <title> 标签了,这个标签很容易提取,无论是正则表达式,还是lxml解析都很容易,不容易的是如何去除频道名称、网站名称等信息。

先来看看, <title> 标签里面都是设么样子的附加信息:

  • 上海用“智慧”激活城市交通脉搏,让道路更安全更有序更通畅_浦江头条_澎湃新闻-The Paper
  • “沪港大学联盟”今天在复旦大学成立_教育_新民网
  • 三亚老人脚踹司机致公交车失控撞墙 被判刑3年_社会
  • 外交部:中美外交安全对话9日在美举行
  • 进博会:中国行动全球瞩目,中国担当世界点赞_南方观澜_南方网
  • 资本市场迎来重大改革 设立科创板有何深意?-新华网

观察这些title不难发现,新闻标题和频道名、网站名之间都是有一些连接符号的。那么我就可以通过这些连接符吧title分割,找出最长的部分就是新闻标题了。

这个思路也很容易实现,这里就不再上代码了,留给小猿们作为思考练习题自己实现一下。

2. 发布时间提取

发布时间,指的是这个网页在该网站上线的时间,一般它会出现在正文标题的下方——meta数据区域。从html代码看,这个区域没有什么特殊特征让我们定位,尤其是在非常多的网站版面面前,定位这个区域几乎是不可能的。这需要我们另辟蹊径。
跟标题一样,我们也先看看一些网站的发布时间都是怎么写的:

  • 央视网2018年11月06日 22:22
  • 时间:2018-11-07 14:27:00
  • 2018-11-07 11:20:37 来源: 新华网
  • 来源:中国日报网 2018-11-07 08:06:39
  • 2018年11月07日 07:39:19
  • 2018-11-06 09:58 来源:澎湃新闻

这些写在网页上的发布时间,都有一个共同的特点,那就是一个表示时间的字符串,年月日时分秒,无外乎这几个要素。通过正则表达式,我们列举一些不同时间表达方式(也就那么几种)的正则表达式,就可以从网页文本中进行匹配提取发布时间了。

这也是一个很容易实现的思路,但是细节比较多,表达方式要涵盖的尽可能多,写好这么一个提取发布时间的函数也不是那么容易的哦。小猿们尽情发挥动手能力,看看自己能写出怎样的函数实现。这也是留给小猿们的一道练习题。

3. 正文的提取

正文(包括新闻配图)是一个新闻网页的主体部分,它在视觉上占据中间位置,是新闻的内容主要的文字区域。正文的提取有很多种方法,实现上有复杂也有简单。本文介绍的方法,是结合老猿多年的实践经验和思考得出来的一个简单快速的方法,姑且称之为“节点文本密度法”。

我们知道,网页的html代码是由不同的标签(tag)组成了一个树状结构树,每个标签是树的一个节点。通过遍历这个树状结构的每个节点,找到文本最多的节点,它就是正文所在的节点。根据这个思路,我们来实现一下代码。

3.1 实现源码


#!/usr/bin/env python3
#File: maincontent.py
#Author: veelion
import re
import time
import traceback
import cchardet
import lxml
import lxml.html
from lxml.html import HtmlComment
REGEXES = {
    'okMaybeItsACandidateRe': re.compile(
        'and|article|artical|body|column|main|shadow', re.I),
    'positiveRe': re.compile(
        ('article|arti|body|content|entry|hentry|main|page|'
         'artical|zoom|arti|context|message|editor|'
         'pagination|post|txt|text|blog|story'), re.I),
    'negativeRe': re.compile(
        ('copyright|combx|comment|com-|contact|foot|footer|footnote|decl|copy|'
         'notice|'
         'masthead|media|meta|outbrain|promo|related|scroll|link|pagebottom|bottom|'
         'other|shoutbox|sidebar|sponsor|shopping|tags|tool|widget'), re.I),
}
class MainContent:
    def __init__(self,):
        self.non_content_tag = set([
            'head',
            'meta',
            'script',
            'style',
            'object', 'embed',
            'iframe',
            'marquee',
            'select',
        ])
        self.title = ''
        self.p_space = re.compile(r'\s')
        self.p_html = re.compile(r'<html|</html>', re.IGNORECASE|re.DOTALL)
        self.p_content_stop = re.compile(r'正文.*结束|正文下|相关阅读|声明')
        self.p_clean_tree = re.compile(r'author|post-add|copyright')
    def get_title(self, doc):
        title = ''
        title_el = doc.xpath('//title')
        if title_el:
            title = title_el[0].text_content().strip()
        if len(title) < 7:
            tt = doc.xpath('//meta[@name="title"]')
            if tt:
                title = tt[0].get('content', '')
        if len(title) < 7:
            tt = doc.xpath('//*[contains(@id, "title") or contains(@class, "title")]')
            if not tt:
                tt =  doc.xpath('//*[contains(@id, "font01") or contains(@class, "font01")]')
            for t in tt:
                ti = t.text_content().strip()
                if ti in title and len(ti)*2 > len(title):
                    title = ti
                    break
                if len(ti) > 20: continue
                if len(ti) > len(title) or len(ti) > 7:
                    title = ti
        return title
    def shorten_title(self, title):
        spliters = [' - ', '–', '—', '-', '|', '::']
        for s in spliters:
            if s not in title:
                continue
            tts = title.split(s)
            if len(tts) < 2:
                continue
            title = tts[0]
            break
        return title
    def calc_node_weight(self, node):
        weight = 1
        attr = '%s %s %s' % (
            node.get('class', ''),
            node.get('id', ''),
            node.get('style', '')
        )
        if attr:
            mm = REGEXES['negativeRe'].findall(attr)
            weight -= 2 * len(mm)
            mm = REGEXES['positiveRe'].findall(attr)
            weight += 4 * len(mm)
        if node.tag in ['div', 'p', 'table']:
            weight += 2
        return weight
    def get_main_block(self, url, html, short_title=True):
        ''' return (title, etree_of_main_content_block)
        '''
        if isinstance(html, bytes):
            encoding = cchardet.detect(html)['encoding']
            if encoding is None:
                return None, None
            html = html.decode(encoding, 'ignore')
        try:
            doc = lxml.html.fromstring(html)
            doc.make_links_absolute(base_url=url)
        except :
            traceback.print_exc()
            return None, None
        self.title = self.get_title(doc)
        if short_title:
            self.title = self.shorten_title(self.title)
        body = doc.xpath('//body')
        if not body:
            return self.title, None
        candidates = []
        nodes = body[0].getchildren()
        while nodes:
            node = nodes.pop(0)
            children = node.getchildren()
            tlen = 0
            for child in children:
                if isinstance(child, HtmlComment):
                    continue
                if child.tag in self.non_content_tag:
                    continue
                if child.tag == 'a':
                    continue
                if child.tag == 'textarea':
                    # FIXME: this tag is only part of content?
                    continue
                attr = '%s%s%s' % (child.get('class', ''),
                                   child.get('id', ''),
                                   child.get('style'))
                if 'display' in attr and 'none' in attr:
                    continue
                nodes.append(child)
                if child.tag == 'p':
                    weight = 3
                else:
                    weight = 1
                text = '' if not child.text else child.text.strip()
                tail = '' if not child.tail else child.tail.strip()
                tlen += (len(text) + len(tail)) * weight
            if tlen < 10:
                continue
            weight = self.calc_node_weight(node)
            candidates.append((node, tlen*weight))
        if not candidates:
            return self.title, None
        candidates.sort(key=lambda a: a[1], reverse=True)
        good = candidates[0][0]
        if good.tag in ['p', 'pre', 'code', 'blockquote']:
            for i in range(5):
                good = good.getparent()
                if good.tag == 'div':
                    break
        good = self.clean_etree(good, url)
        return self.title, good
    def clean_etree(self, tree, url=''):
        to_drop = []
        drop_left = False
        for node in tree.iterdescendants():
            if drop_left:
                to_drop.append(node)
                continue
            if isinstance(node, HtmlComment):
                to_drop.append(node)
                if self.p_content_stop.search(node.text):
                    drop_left = True
                continue
            if node.tag in self.non_content_tag:
                to_drop.append(node)
                continue
            attr = '%s %s' % (
                node.get('class', ''),
                node.get('id', '')
            )
            if self.p_clean_tree.search(attr):
                to_drop.append(node)
                continue
            aa = node.xpath('.//a')
            if aa:
                text_node = len(self.p_space.sub('', node.text_content()))
                text_aa = 0
                for a in aa:
                    alen = len(self.p_space.sub('', a.text_content()))
                    if alen > 5:
                        text_aa += alen
                if text_aa > text_node * 0.4:
                    to_drop.append(node)
        for node in to_drop:
            try:
                node.drop_tree()
            except:
                pass
        return tree
    def get_text(self, doc):
        lxml.etree.strip_elements(doc, 'script')
        lxml.etree.strip_elements(doc, 'style')
        for ch in doc.iterdescendants():
            if not isinstance(ch.tag, str):
                continue
            if ch.tag in ['div', 'h1', 'h2', 'h3', 'p', 'br', 'table', 'tr', 'dl']:
                if not ch.tail:
                    ch.tail = '\n'
                else:
                    ch.tail = '\n' + ch.tail.strip() + '\n'
            if ch.tag in ['th', 'td']:
                if not ch.text:
                    ch.text = '  '
                else:
                    ch.text += '  '
            # if ch.tail:
            #     ch.tail = ch.tail.strip()
        lines = doc.text_content().split('\n')
        content = []
        for l in lines:
            l = l.strip()
            if not l:
                continue
            content.append(l)
        return '\n'.join(content)
    def extract(self, url, html):
        '''return (title, content)
        '''
        title, node = self.get_main_block(url, html)
        if node is None:
            print('\tno main block got !!!!!', url)
            return title, '', ''
        content = self.get_text(node)
        return title, content
  

3.2 代码解析

跟新闻爬虫一样,我们把整个算法实现为一个类:MainContent。

首先,定义了一个全局变量: REGEXES。它收集了一些经常出现在标签的class和id中的关键词,这些词标识着该标签可能是正文或者不是。我们用这些词来给标签节点计算权重,也就是方法calc_node_weight()的作用。

MainContent类的初始化,先定义了一些不会包含正文的标签 self.non_content_tag,遇到这些标签节点,直接忽略掉即可。

本算法提取标题实现在get_title()这个函数里面。首先,它先获得 <title> 标签的内容,然后试着从 <meta> 里面找title,再尝试从 <body> 里面找id和class包含title的节点,最后把从不同地方获得的可能是标题的文本进行对比,最终获得标题。对比的原则是:

  • <meta> <body> 里面找到的疑似标题如果包含在 <title> 标签里面,则它是一个干净(没有频道名、网站名)的标题;
  • 如果疑似标题太长就忽略
  • 主要把 <title> 标签作为标题

<title> 标签里面获得标题,就要解决标题清洗的问题。这里实现了一个简单的方法: clean_title()。

在这个实现中,我们使用了lxml.html把网页的html转化成一棵树,从body节点开始遍历每一个节点,看它直接包含(不含子节点)的文本的长度,从中找出含有最长文本的节点。这个过程实现在方法:get_main_block()中。其中一些细节,小猿们可以仔细体会一下。

其中一个细节就是,clean_node()这个函数。通过get_main_block()得到的节点,有可能包含相关新闻的链接,这些链接包含大量新闻标题,如果不去除,就会给新闻内容带来杂质(相关新闻的标题、概述等)。

还有一个细节,get_text()函数。我们从main block中提取文本内容,不是直接使用text_content(),而是做了一些格式方面的处理,比如在一些标签后面加入换行符合 \n ,在table的单元格之间加入空格。这样处理后,得到的文本格式比较符合原始网页的效果。

爬虫知识点

1. cchardet模块
用于快速判断文本编码的模块

2. lxml.html模块
结构化html代码的模块,通过xpath解析网页的工具,高效易用,是写爬虫的居家必备的模块。

3. 内容提取的复杂性
我们这里实现的正文提取的算法,基本上可以正确处理90%以上的新闻网页。
但是,世界上没有千篇一律的网页一样,也没有一劳永逸的提取算法。大规模使用本文算法的过程中,你会碰到奇葩的网页,这个时候,你就要针对这些网页,来完善这个算法类。


来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/69913713/viewspace-2645822/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/69913713/viewspace-2645822/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

大规模异步新闻爬虫【5】:网页正文的提取 的相关文章

  • vncserver Can&#39;t find file /root/.vnc/192.168.1.3:1.pid You&#39;ll have to kill the Xvnc process ...

    CentOS 6 5 修改IP后VNC链接失败 xff0c 提示 xff1a The connection was refused by host computer 尝试删除之前的服务时提示以下信息 xff1a Can 39 t find
  • 国内首个网友可以开发应用的开放式网络操作系统

    国内首个面向电脑爱好者的应用开放平台 X在线电脑 xff0c 提供傻瓜化的网络应用程序开发工具 应用梦工厂 xff0c 让不擅长编程的电脑爱好者们 xff0c 也能开发OA ERP等企业应用 xff0c 并且能快速部署到X在线电脑 X在线电
  • linux多级菜单脚本教程,Linux下使用readline库编程实现多级CLI菜单

    一 背景 CLI是一种快速简洁的人机交互方式 xff0c 优秀的CLI 如 mysql vtysh gdb 带给我们非常好的体验 那么CLI都是如何开发出来的 xff1f 二 相关知识 2 1 CLI vs GUI 文章 1 纵观CLI与G
  • 白盒交换机NOS列表(picos/SnapRoute/ONL)

    WIKI NOS xff1a https en wikipedia org wiki Network operating system Examples JUNOS used in routers and switches from Jun
  • powershell

    常用单行命令 目录 查看当前目录的大小 xff0c 并排序输出 du h max depth 61 1 sort nr 自动选择单位 du m max depth 61 1 sort nr 选择M为单位 转载于 https www cnbl
  • 私有云对企业来说有什么好处

    企业是一个受控集团 xff0c 只有良好的管理 决策 xff0c 一个企业才有成功的希望 xff0c 所以管理在企业中占有重要的地位 私有云的使用是一只无形的手 xff0c 它控制着日常工作中的资源和效率 1 企业拥有基础设施 xff0c
  • JavaScript禁用页面刷新

    JavaScript禁用页面刷新代码如下 xff1a 禁用F5刷新 document onkeydown 61 function if event keyCode 61 61 116 event keyCode 61 0 event can
  • java 整除(/) 求余(%) 运算

    1 java 整除 求余 运算 1 求余 System out println 11 2 顾名思义就是11除2的余数 gt 1 System out println 11 2 结果 gt 1 System out println 11 2
  • C# 解决窗体闪烁

    C 解决窗体闪烁 在Windows窗体上造成 闪烁 的窗体上有很多控制 造成这种闪烁的原因有两个 xff1a 1 当控件需要被绘制时 xff0c Windows发送一个控件两个消息 第一个 xff08 WM ERASEBKGND xff09
  • OVN – OVN OpenStack(二)

    OpenStack networking ovn 项目为Neutron提供了一个基于ML2的OVN插件 xff0c 它使用OVN组件代替了各种Neutron的Python agent xff0c 也不再使用 RabbitMQ xff0c 而
  • 飞秋无法显示局域网好友

    1 飞秋无法显示局域网好友 无法查看网上邻居 无法适用共享打印机的问题是由于开启了 局域网隐身 的缘故 xff0c 打开 360安全卫士 xff1e 功能大全 xff1e 网络优化 xff1e 流量防火墙 xff1e 局域网防护 xff0c
  • pandas 按照某一列进行排序

    pandas排序的方法有很多 xff0c sort values表示根据某一列排序 pd sort values 34 xxx 34 inplace 61 True 表示pd按照xxx这个字段排序 xff0c inplace默认为False
  • 关系数据库和NoSQL结合使用:MySQL + MongoDB

    Home Page 作者使用一个案例来说明MySQL 43 MongoDB结合使用 xff0c 发挥各自所长 xff0c 并且认为他们互补性很强 当然 xff0c 这其中不可避免引入DDD中的编程设计模式 Repository仓储模式 xf
  • 查看网卡信息及状态和网卡日志信息

    查看网卡信息 1 mii tool v w em1 em2 l0 em1 negotiated 100baseTx FD link ok product info vendor 00 aa 00 model 57 rev 1 basic m
  • 筛选出sql 查询结果中 不包含某个字符

    select from table1 where patindex 39 关键字 39 aa 61 0 select from table1 where charindex 39 关键字 39 aa 61 0 select from tab
  • IE8正式版引发VS2005和VS2008添加变量向导出错的解决方案

    1 解决办法1 xff1a 2 卸载IE8 3 解决办法2 xff1a xff08 自己使用的方法 xff09 4 5 打开注册表编辑器 6 7 选择 HKEY CURRENT USER Software Microsoft Windows
  • Visual Studio 2010中文旗舰版+大家所关心的

    下载地址 xff08 VS2010不含MSDN xff09 xff1a http download microsoft com download 2 4 7 24733615 AA11 42E9 8883 E28CDCA88ED5 X16
  • CSS列表

    CSS列表属性可以放置 改变列表项的标志 xff0c 或者将图像作为列表项标志 list style xff1a 简写属性 用于把所有用于列表的属性设置在一个声明中 list style image xff1a 将图像设置为列表项的标志 U
  • Lodash源码讲解-compact函数

    原文首发于Lodash源码讲解 这是我们阅读Lodash源码的第3篇博客 xff0c 在这篇文章里我们来学习一下Lodash的compact方法 compact函数内部没有依赖别的函数 xff0c 让我们先来看一下compact函数的源码
  • CentOS 6.5下Squid代理服务器的安装与配置

    一 系统环境 操作系统 xff1a CentOS release 6 5 Squid版本 xff1a squid 3 1 10 20 el6 5 3 x86 64 SELINUX 61 disabled HTTP Service stope

随机推荐

  • 修改VNCSERVER的分辨率

    使用VNC远程连接时 xff0c 最大化窗口后仍旧在中间显示一个小屏幕 xff0c 并没有随着窗口最大化 xff0c 解决该问题需要首先在VNC窗口标题栏右键 gt Options gt Scaling 选择第二项 xff1a Scale
  • XMLHttpRequest - 原始AJAX初步

    我们知道 xff0c 传统的Web应用是request response形式的 xff0c 即浏览器向服务器发送请求 xff0c 服务器进行处理 xff0c 然后再对浏览器响应 这种形式最大的缺点就是 xff1a 客户端需要等服务器处理完之
  • Python面向对象编程 - 一个记事本程序范例(二)

    给程序加上控制台菜单 menu py import sys from notebook import Notebook Note class Menu 39 39 39 Display a menu and respond to choic
  • 个人代码库の自动粘合桌面边缘

    using System Windows Forms using System namespace public partial class form 必要事件 xff1a No 1 xff1a 窗体的 Move 事件 No 2 xff1a
  • 完全参照系统自带的DatePickerDialog、TimePickerDialog的源代码仿写的DateTimePickerDialog...

    完全参照系统自带的DatePickerDialog TimePickerDialog的源代码仿写的DateTimePickerDialog 具有同时选择日期 时间的功能 在2 2 2 3平台 xff0c 显示的效果可能会有一个大背景框在后面
  • Tracking your habits in Org-mode

    纯属记录 在org mode中 xff0c 你可以跟踪你的周期性事务或辅助培养习惯 xff0c 比如每天阅读半小时 xff0c 每天完成后org mode会予以记录 如果你正计划每月培养一个好习惯 xff0c 也可以使用这个功能来记录你的完
  • 算法的力量

    算法的力量 李开复 真正学懂计算机的人 xff08 不只是 编程匠 xff09 都对数学有相当的造诣 xff0c 既能用科学家的严谨思维来求证 xff0c 也能用工程师的务实手段来解决问题 而这种思维和手段的最佳演绎就是 算法 虽然在摩尔定
  • Xstream序列化实体

    Java序列化的日期为是很标准 xff0c XStream中转换为标准的做法 import java text DateFormat import java text ParseException import java text Simp
  • Booksort 启发式函数很重要h(s1)<=h(s2)+cost(s1,s2);

    Problem Description The Leiden University Library has millions of books When a student wants to borrow a certain book he
  • FreeBSD的基本系统下Shell设置(转)

    FreeBSD的基本系统下Shell设置 转 64 more 64 命令解释程序shell是与用户关系最密切的应用程序 xff0c 用户主要通过shell使用系统 在每次登录系统之后 xff0c 就启动了一个与用户交互的shell xff0
  • 20050410:他们不过是一群政客

    从来就没有把台湾人民的利益放在眼里 xff0c 甚至不惜拿他们来做挡箭牌 xff0c 一边还对日本谄媚有加 转载于 https www cnblogs com yidinghe archive 2005 04 10 134987 html
  • 排列组合公式

    注 xff1a 排列与元素的顺序有关 xff0c 组合与顺序无关 xff0e 如231与213是两个排列 xff0c 2 xff0b 3 xff0b 1的和与2 xff0b 1 xff0b 3的和是一个组合 xff0e 1 xff0e 排列
  • Mybatis初始化机制

    对于任何框架而言 xff0c 在使用前都要进行一系列的初始化 xff0c MyBatis也不例外 本章将通过以下几点详细介绍MyBatis的初始化过程 1 MyBatis的初始化做了什么 2 MyBatis基于XML配置文件创建Config
  • ValidationSummary控件

    ValidationSummary控件用于在页面中的一处地方显示所有验证错误的列表 这个控件在使用大的表单时特别有用 如果用户在页面底部的表单字段中输入了错误的值 xff0c 那么这个用户可能永远也看不到错误信息 不过 xff0c 如果使用
  • 数据特征分析——概述

    一 6个基础分析思路 xff1a 1 分布分析 2 对比分析 3 统计分析 4 帕累托分析 5 正态性检验 6 相关性分析 二 分布分析 分布分析 xff1a 研究数据的分布特征和分布类型 xff0c 分定量数据 定性数据区分基本统计量 三
  • Filebeat 6.0 把日志直接输入到ES中如何自定义index

    临时搭建了一套EFK xff08 elasticsearch xff0c filebeat xff0c kibana xff09 xff0c filebeat 6 0 默认的index 是filebeat 43 时间 xff0c 这样无法满
  • 人在异乡

    为什么到了上海五年 xff0c 还是没有归属感 to be continued
  • 怎么求矩阵对应的基

    怎么求矩阵对应的基呢 xff1f 对矩阵做初等行变换 xff0c 化为上三角形 或 对角型 xff0c 主对角元素不为0的列即为该矩阵的一组基 A 61 这个矩阵对应的一个基 为 其实 xff0c 将第二行的 1 倍加到第一行上 xff0c
  • javascript弹出窗口代码

    1 最基本的弹出窗口代码 lt SCRIPT LANGUAGE 61 34 javascript 34 gt lt window open 39 page html 39 gt lt SCRIPT gt 因为着是一段javascripts代
  • 大规模异步新闻爬虫【5】:网页正文的提取

    前面我们实现的新闻爬虫 xff0c 运行起来后很快就可以抓取大量新闻网页 xff0c 存到数据库里面的都是网页的html代码 xff0c 并不是我们想要的最终结果 最终结果应该是结构化的数据 xff0c 包含的信息至少有url xff0c