12分钟

任务 2 新闻数据获取及保存

任务目的

这一步的目标是实现新闻数据的爬取,并将获取到的新闻数据保存在本地文件中。这里需要用到requests和lxml模块,前者用于获取页面内容,后者用于对页面中的关键信息进行提取,最终保存提取到的所有文本内容。

任务步骤

1.新闻数据提取代码构建

创建新闻数据提取文件get_news.py

参考 任务1 最后新建main.py的操作,在项目目录中执行下方命令,创建获取新闻数据的get_news.py文件:

vim get_news.py

按下I键进入编辑模式,复制下方的代码,粘贴到文件中:

import random
import time
from urllib.parse import urljoin

import requests
from lxml.etree import HTML

# 指定要爬取的初始页面地址
BASE_URL = "https://cn.chinadaily.com.cn/"


def get_content_by_xpath(url, xpath):
    """根据URL地址和XPath提取匹配内容"""
    # 定义请求头
    headers = {"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"}
    # 发送get请求并接收数据
    resp = requests.get(url=url, headers=headers)
    # 将接收到的字符串数据转换成XPath解析对象
    html_tree1 = HTML(resp.content)
    # 根据XPath指令返回匹配的内容
    content = html_tree1.xpath(xpath)
    return content


def save_top_news(num=10, filename="top_news_content.txt"):
    """保存最近的n篇新闻"""
    # 从中国日报主页获取疫情专区的URL
    # //div[@class='tou-right']/a/@href
    resp1 = get_content_by_xpath(url=BASE_URL, xpath="//div[@class='tou-right']/a/@href")
    static_url = "//cn.chinadaily.com.cn/a/202001/31/WS5e33aa27a3107bb6b579c52f.html"
    target_url_part1 = resp1[0] if resp1[0] == static_url else static_url
    # 结合初始页面,拼接出疫情专题页面的完整地址
    target_url1 = urljoin(BASE_URL, target_url_part1)

    # 从疫情专区获取最新的num篇新闻报道URL
    # //div[@class='eve'][1]//div[@class='ove']/div[@class='ove-right']/h1/a/@href
    url_list = get_content_by_xpath(url=target_url1, xpath="//div[@class='eve']//div[@class='ove']/div[@class='ove-right']/h1/a/@href")
    if num > len(url_list):
        raise Exception("疫情专区的新闻数量少于设定值")
    # 如果新闻数量大于num,保留最新的num篇新闻
    xpath_str = "//div[@class='eve'][position()<%d]//div[@class='ove']/div[@class='ove-right']/h1/a/@href" % (num+1, )
    top_url_part_list = get_content_by_xpath(url=target_url1, xpath=xpath_str)
    # 打印获取到的新闻报道URL
    print(top_url_part_list)

    # 创建一个新文件并将其内容清空,作为保存新闻内容的文件
    with open(filename, "w", encoding='utf-8') as f:
        f.truncate()

    # 遍历获取的新闻URL并将新闻内容保存到本地文件
    for url_part in top_url_part_list:
        # 让爬虫程序随机休眠一段时间,模拟用户访问网站的行为
        pause_time = random.random()
        print("爬虫程序随机休眠%s秒" % (pause_time+1))
        time.sleep(pause_time)
        target_url2 = urljoin(target_url1, url_part)
        # 从最新的新闻报道页面解析出报道文本并保存到本地文件
        # 标题://h1
        # 内容://div[@class='container']//div[@id='Content']/p/text()
        title = get_content_by_xpath(url=target_url2, xpath="//h1/text()")
        content = get_content_by_xpath(url=target_url2, xpath="//div[@id='Content']/p/text()")
        content = [p+"\n" for p in content]
        # 打印标题和内容
        print("文章【{}】解析成功。".format(title[0]))
        # 将新闻内容写入本地文件
        with open(filename, "a+", encoding='utf-8') as f:
            f.write(title[0])
            f.write("\n")
            f.writelines(content)
            f.write("\n")

    print("新闻内容全部提取完成!")
    return filename

完成实验代码的粘贴后,按下 ESC键切换到命令模式,并在英文模式下使用命令:wq保存文件并退出编辑器。

2.get_news模块代码解析

接下来将会对此模块的具体功能进行介绍

注意:下方的代码介绍与上方粘贴的代码相同,主要是为了帮助学员理解代码功能,不需学员进行操作

get_content_by_xpath函数介绍

(1)定义请求头。

函数中首先使用下方命令,定义了请求头headers

headers = {"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"}

headers的定义格式为字典形式,发送请求时将其作为参数携带,可以将发送的请求模拟成浏览器的请求形式,以便获取到与浏览器中一致的内容。

(2)发送get请求并接收数据。

接下来使用requests发送一个get类型的请求:

resp = requests.get(url=url, headers=headers)

此处包含了两个参数,url对应要请求的URL地址,headers对应上方定义的请求头。

此函数主要通过save_top_news函数调用,每次调用时都会填写具体的URL地址,返回的结果是请求网页中的具体内容。

(3)通过XPath匹配内容。

对于获取到的网页内容,结合下面两行的代码将会实现具体的内容提取操作:

html_tree1 = HTML(resp.content)
content = html_tree1.xpath(xpath)
return content

其中第一行会将接收到的字符串数据转换成XPath解析对象,对于这一类型的对象,可以通过XPath表达式,提取节点中的数据,这个数据可以是URL地址或具体的文本信息。

第二行和第三行的内容,就是传入具体的XPath表达式,并将获取到的数据以文本的形式返回。

save_top_news函数介绍

(1)获取疫情专区URL。

此函数第一部分的内容是从中国日报网的 首页 获取疫情专区的URL,此部分对应的代码如下:

resp1 = get_content_by_xpath(url=BASE_URL, xpath="//div[@class='tou-right']/a/@href")
target_url_part1 = resp1[0]
target_url1 = urljoin(BASE_URL, target_url_part1)

第一行调用了上方说明的get_content_by_xpath方法,传入了人民日报首页作为URL地址,同时指定了XPath表达式:

//div[@class='tou-right']/a/@href

此表达式的具体功能是: 从页面中的任意位置找到包含类tou-right的div,获取其中的a标签的href属性的值

通过此表达式,可以获取到页面中疫情专区的URL地址:

4-2-1 获取疫情专区URL

由于获取到的结果为列表形式,所以通过target_url_part1 = resp1[0]提起到结果中的第一条数据,此时获取到的结果为字符串形式。

注:为了避免疫情过后实验结果发生变化,实际的项目代码中在这里进行了一步条件判断。如果获取到的结果发生变化,说明页面的展示专题做了调整,或是进行了重构,此时会将返回结果替换为疫情专区的固定访问地址,保证接下来的实验可以顺利进行。

通过urljoin方法,将其与BASE_URL进行拼接,最终组合出页面的完整地址:

https://cn.chinadaily.com.cn/a/202001/31/WS5e33aa27a3107bb6b579c52f.html

访问此地址,可以进入到疫情专区的页面,说明疫情专区URL的获取功能已经实现。

(2)获取最新的新闻报道URL。

中间一部分的代码主要是用于获取最新的新闻报道URL(默认指定获取最新的10篇新闻)。

首先会进行一步判断,确认页面中的专区新闻数量大于设定值:

url_list = get_content_by_xpath(url=target_url1, xpath="//div[@class='eve']//div[@class='ove']/div[@class='ove-right']/h1/a/@href")
if num > len(url_list):
raise Exception("疫情专区的新闻数量少于设定值")

如果新闻数量小于设定值,将会抛出异常:疫情专区的新闻数量少于设定值。

注:实验中的默认设置的获取新闻数量为10篇,当前的新闻数量远高于判断数量,但仍然保留此部分代码,是为了保证手动替换num的值后,不会因为新闻数量小于导致程序抛出异常错误。

如果新闻数量满足需求,接下来会保留最新的num篇新闻:

xpath_str = "//div[@class='eve'][position()<%d]//div[@class='ove']/div[@class='ove-right']/h1/a/@href" % (num+1, )
top_url_part_list = get_content_by_xpath(url=target_url1, xpath=xpath_str)

这一部分的代码与获取疫情专区URL的步骤相同,首先拼接XPath表达式,然后调用get_content_by_xpath函数,获取满足条件的URL地址列表,获取到的结果存储在列表top_url_part_list中。

(3)保存新闻内容到本地文件。

函数的最后部分是保存新闻内容到本地文件中,第一步会先创建一个新文件并将其内容清空,作为保存新闻内容的文件:

with open(filename, "w", encoding='utf-8') as f:
    f.truncate()

清空内容的主要目的是避免列表中已经包含了同名文件,影响最终的实验效果。

接下来通过for循环,对提取到的URL地址列表进行遍历,遍历代码中首先有一段随机睡眠1~2秒的代码块:

pause_time = random.random()
print("爬虫程序随机休眠%s秒" % (pause_time+1))
time.sleep(pause_time)

设置随机睡眠时间的主要目的是模拟用户访问网站的行为,如果没有这一部分的代码。一方面过于频繁的请求,会给被请求方的服务器造成巨大的负载压力,严重时甚至会导致网站瘫痪;另一方面过于频繁的请求可能会让网站将请求识别为机器人,进而进行请求限制,影响接下来数据的获取。

下一行代码再次使用了urljoin方法,目的同样是拼接出新闻页面的具体URL地址:

target_url2 = urljoin(target_url1, url_part)

接下来一部分两次调用get_content_by_xpath函数,通过XPath表达式分别用于获取文章的标题和正文内容:

title = get_content_by_xpath(url=target_url2, xpath="//h1/text()")
content = get_content_by_xpath(url=target_url2, xpath="//div[@id='Content']/p/text()")
content = [p+"\n" for p in content]
# 打印标题和内容
print("文章【{}】解析成功。".format(title[0]))

获取到的多行正文会以列表形式展示,通过第三行的命令,会在每一行正文的尾部插入换行符\n。完成以上操作后,会在控制台上打印文章解析成功的提示信息。

接下来这一部分的代码,会将新闻的内容写入本地文件:

# 将新闻内容写入本地文件
with open(filename, "a+", encoding='utf-8') as f:
    f.write(title[0])
    f.write("\n")
    f.writelines(content)
    f.write("\n")

写入文件的过程中,会再次用换行符\n将新闻的标题和正文分隔开。

等到完成所有新闻内容的获取和保存后,执行以下两行代码:

print("新闻内容全部提取完成!")
return filename

第一行在控制台上打印提示信息,第二行会返回生成的文件名,方便下一步定位文本文件,并通过文件的内容生成词云图。

稍后执行提取新闻内容这一部分的代码时,将会在控制台展示类似下方的提示信息:

4-2-2 提取新闻内容

至此新闻数据的获取及保存的功能已经实现,下面将实现结合新闻数据,生成词云图的功能。