Python 网络爬虫实战:爬取人民日报新闻文章

发布于 2020-05-22  19 次阅读


写爬虫之前,先回顾一下爬取的思路。

 

首先,要 明确自己要爬取什么内容 ,需求明确了,后面才能有的放矢;然后,要 分析目标网站,包括 URL 结构,HTML 页面,网络请求及返回的结果等,目的就是要找到我们要爬取的目标位于网站的哪一个位置,我们如何才能获取到,从而指定爬取策略;接下来,就是 编码环节 了,使用代码去发起网络请求,解析网页内容,提取目标数据,并进行数据存储;最后,测试代码,完善程序,如添加一些输入输出的交互,使程序使用更加方便,添加异常捕获和处理的部分,使程序的鲁棒性更佳等。

 

一、明确需求

他的需求其实很简单,就是人民日报每天有一份报纸,每份报纸中有若干个版面,每个版面有若干文章。他希望将这些文章全部爬取下来,按一定的规则存储在本地(具体要求如下图所示)。

 

 

 

 
 

二、分析目标网站 

1.  URL 组成结构 

 

人民日报网站的 URL 的结构还是比较直观的,基本上什么重要的参数,比如日期,版面号,文章编号什么的,都在 URL 中有所体现,构成的规则也很简单,像这样

 

版面目录:http://paper.people.com.cn/rmrb/html/2019-05/06/nbs.D110000renmrb_01.htm

 

文章内容:http://paper.people.com.cn/rmrb/html/2019-05/06/nw.D110000renmrb_20190506_5-01.htm

 

在版面目录的链接中,"/2019-05/06/" 表示日期,后面的 "_01" 表示这是第一版面的链接。

 

在文章内容的链接中,"/2019-05/06/" 表示日期,后面的 "_20190506_5_01" 表示这是 2019 年 5 月 6 日报纸的第 1 版第 5 篇文章

 

值得注意的是,在日期的 "月" 和 "日" 以及 "版面号" 的数字,若小于 10,需在前面补 "0" ,而文章的篇号则不必。

 

了解到这个之后,我们可以按照这个规则,构造出任意一天报纸中人一个版面的链接,以及任意一篇文章的链接。

 

如:2018 年 6 月 5 日第 4 版的目录链接为:

 

http://paper.people.com.cn/rmrb/html/2019-05/06/nbs.D110000renmrb_01.htm

 

2018 年 6 月 1 日第 2 版第 3 篇文章的链接为:

 

http://paper.people.com.cn/rmrb/html/2018-06/01/nw.D110000renmrb_20180601_3-02.htm

 

点击访问,发现果然是这样。至此,网站的 URL 组成结构分析完成。

 

 
 

 

2.  分析网页 HTML 结构

 

在 URL 分析中,我们也发现了,网站的页面跳转是通过 URL 的改变完成的,不涉及到诸如 Ajax 这样的动态加载方法。也就是说,它的所有数据是一开始就加载好的,我们只需要去 html 中提取相应得数据即可。

 

PS:如果是使用了 Ajax的话,它网站一开始展示的数据是不全的,只有在你触发了某些操作,如页面浏览到最底部,或者点击查看更多按钮等等等等后,它才会向服务器发出请求,获取剩余的其他数据。如果是这种方式的话,我们的爬取策略也就不是从 HTML 中找了,而是直接向服务器发请求,然后解析服务器返回的 json 文件了,具体方法可以参考《Python网络爬虫实战:爬取知乎话题下 18934 条回答数据》。

 

好了,言归正传,我们来分析一下我们的目标网站。按 F12 召唤出开发者工具(点击图中 1 处的小箭头,然后点击网页中的内容,可以在 html 源码中快速找到相应的位置)。

 

 

 

通过这种方法,我们可以知道,版面目录存放在一个 id = "pageList" 的 div 标签下,class = "right_title1" 或 "right_title2" 的 div 标签中,每一个 div 表示一个版面,而版面的链接就在 id = "pageLink" 的 a 标签中。

 

 

 

使用同样的方法,我们可以知道,文章目录存放在一个 id = "titleList" 的 div 标签下的 ul 标签中,其中每一个 li 标签表示一篇文章,而文章的链接就在 li 标签下的 a 标签中。

 

 

 

进入文章内容页面之后,我们可以知道,文章标题存放在 h1 ,h2 ,h3 标签中(有的文章标题只用到了 h1 标签,而有的文章有副标题可能会用到 h2 或 h3 标签), 正文部分存放在 id = "ozoom" 的 div 标签下的 p 标签里。

 

至此,目标网站的 HTML 页面分析完成。

 

 
 

 

3. 制定爬取策略

 

通过分析 URL 组成结构以及目标网站的 HTML 结构,我们已经完成了爬虫的前期调研工作,接下来需要根据网站的特点制定相应的爬取策略,然后评估每种方法的优劣和难易程度,最终选择一种最佳方案,进行编码实现。

 

策略一:第一遍,先爬取版面目录,将每一个版面的链接保存下来;第二遍,依次访问每一个版面的链接,将该版面的文章链接保存下来;第三遍,依次访问每一个文章链接,将文章的标题和正文保存到本地。

 

策略二:由于我们已经知道了文章链接的构成方式,所以我们或许可以跳过目录的爬取,直接循环构造文章链接,爬取文章内容。

 

经过一番比较,以及简单的编码测试,我决定使用策略一来完成此爬虫。

 

主要原因是,虽然策略二逻辑比较简单也比较方便,但是需要解决两个问题,1. 每天的报纸版面数不同,而且每个版面的文章数也不同,在构造 URL 的时候,如何保证文章不重不漏?2. 有些特殊情况下,文章的编号并不是连续的,该如何解决这个问题?

 

所以,综合考虑,我觉得策略一可能会更加稳定更加保险一些(如果大家想到了解决策略二问题的方法,可以尝试一下,或者如果想到有其他策略,也欢迎留言,我们一起探讨)。

 

 
 

 

三、编码环节

接下来就到了实际编码环节了,话不多说,直接开始。

 

首先导入本项目用到的库:

 

import requests

import bs4

import os

import datetime

import time

其中,requests 库主要用来发起网络请求,并接收服务器返回的数据;bs4 库主要用来解析 html 内容,是一个非常简单好用的库;os 库主要用于将数据输出存储到本地文件中。

 

def fetchUrl(url):

'''

功能:访问 url 的网页,获取网页内容并返回

参数:目标网页的 url

返回:目标网页的 html 内容

'''

 

headers = {

'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',

'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',

}

 

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

r.raise_for_status()

r.encoding = r.apparent_encoding

return r.text

fetchUrl 函数用于发起网络请求,它可以访问目标 url ,获取目标网页的 html 内容并返回。

 

这里其实是应该做异常捕获的(我为了简单就省掉了,吐舌头)。因为网络情况比较复杂,可能会因为各种各样的原因而访问失败, r.raise_for_status() 这句代码其实就是在判断是否访问成功的,如果访问失败,返回的状态码不是 200 ,执行到这里时会直接抛出相应的异常。

 

def getPageList(year, month, day):

'''

功能:获取当天报纸的各版面的链接列表

参数:年,月,日

'''

url = 'http://paper.people.com.cn/rmrb/html/' + year + '-' + month + '/' + day + '/nbs.D110000renmrb_01.htm'

html = fetchUrl(url)

bsobj = bs4.BeautifulSoup(html,'html.parser')

pageList = bsobj.find('div', attrs = {'id': 'pageList'}).ul.find_all('div', attrs = {'class': 'right_title-name'})

linkList = []

 

for page in pageList:

link = page.a["href"]

url = 'http://paper.people.com.cn/rmrb/html/' + year + '-' + month + '/' + day + '/' + link

linkList.append(url)

 

return linkList

 getPageList 函数,用于爬取当天报纸的各版面的链接,将其保存为一个数组,并返回。

 

def getTitleList(year, month, day, pageUrl):

'''

功能:获取报纸某一版面的文章链接列表

参数:年,月,日,该版面的链接

'''

html = fetchUrl(pageUrl)

bsobj = bs4.BeautifulSoup(html,'html.parser')

titleList = bsobj.find('div', attrs = {'id': 'titleList'}).ul.find_all('li')

linkList = []

 

for title in titleList:

tempList = title.find_all('a')

for temp in tempList:

link = temp["href"]

if 'nw.D110000renmrb' in link:

url = 'http://paper.people.com.cn/rmrb/html/' + year + '-' + month + '/' + day + '/' + link

linkList.append(url)

 

return linkList

  getPageList 函数,用于爬取当天报纸的某一版面的所有文章的链接,将其保存为一个数组,并返回。

 

def getContent(html):

'''

功能:解析 HTML 网页,获取新闻的文章内容

参数:html 网页内容

'''

bsobj = bs4.BeautifulSoup(html,'html.parser')

 

# 获取文章 标题

title = bsobj.h3.text + '\n' + bsobj.h1.text + '\n' + bsobj.h2.text + '\n'

#print(title)

 

# 获取文章 内容

pList = bsobj.find('div', attrs = {'id': 'ozoom'}).find_all('p')

content = ''

for p in pList:

content += p.text + '\n'

#print(content)

 

# 返回结果 标题+内容

resp = title + content

return resp

getContent 函数,用于访问文章内容页,爬取文章的标题和正文,并返回。

 

def saveFile(content, path, filename):

'''

功能:将文章内容 content 保存到本地文件中

参数:要保存的内容,路径,文件名

'''

# 如果没有该文件夹,则自动生成

if not os.path.exists(path):

os.makedirs(path)

 

# 保存文件

with open(path + filename, 'w', encoding='utf-8') as f:

f.write(content)

saveFile 函数用于将文章内容保存到本地的指定文件夹中。

 

def download_rmrb(year, month, day, destdir):

'''

功能:爬取《人民日报》网站 某年 某月 某日 的新闻内容,并保存在 指定目录下

参数:年,月,日,文件保存的根目录

'''

pageList = getPageList(year, month, day)

for page in pageList:

titleList = getTitleList(year, month, day, page)

for url in titleList:

 

# 获取新闻文章内容

html = fetchUrl(url)

content = getContent(html)

 

# 生成保存的文件路径及文件名

temp = url.split('_')[2].split('.')[0].split('-')

pageNo = temp[1]

titleNo = temp[0] if int(temp[0]) >= 10 else '0' + temp[0]

path = destdir + '/' + year + month + day + '/'

fileName = year + month + day + '-' + pageNo + '-' + titleNo + '.txt'

 

# 保存文件

saveFile(content, path, fileName)

download_rmrb 函数是需求中要求的主函数,可以根据 year,month,day 参数,下载该天的全部报纸文章内容,并按照规则保存在指定的路径 destdir 下。

 

 

if __name__ == '__main__':

'''

主函数:程序入口

'''

year = "2019"

month = "05"

day = "06"

destdir = "D:/data"

 

download_rmrb(year, month, day, destdir)

print("爬取完成:" + year + month + day)

至此,程序的主体功能已经完成。

 

 
 

 

四、完善程序

通过上一章节的编码,我们已经实现了 通过调用 download_rmrb 函数,来下载特定日期的全部文章内容。但是同时我们也可以发现,在程序入口 main 函数中,日期是在代码中写死的,也就是说,如果我们想爬取其他日期的报纸,就必须修改源码。

 

很不方便不是嘛,我们把它做一下改进,日期使用交互式的方式由用户输入。 

 

 

if __name__ == '__main__':

'''

主函数:程序入口

'''

# 爬取指定日期的新闻

newsDate = input('请输入要爬取的日期(格式如 20190502 ):')

 

year = newsDate[0:4]

month = newsDate[4:6]

day = newsDate[6:8]

 

download_rmrb(year, month, day, 'D:/data')

print("爬取完成:" + year + month + day)

 
 

 

 

 

 
 

 

方便了一些是不是?但是问题又来了,如果我想一次性爬一个月的,或者一年的怎么办?我岂不是要手动输入几十次几百次?所以,程序我们还可以改进一下,用户输入其实日期和结束日期,程序爬取期间的日期的所有文章。

 

def gen_dates(b_date, days):

day = datetime.timedelta(days = 1)

for i in range(days):

yield b_date + day * i

 

 

def get_date_list(beginDate, endDate):

"""

获取日期列表

:param start: 开始日期

:param end: 结束日期

:return: 开始日期和结束日期之间的日期列表

"""

 

start = datetime.datetime.strptime(beginDate, "%Y%m%d")

end = datetime.datetime.strptime(endDate, "%Y%m%d")

 

data = []

for d in gen_dates(start, (end-start).days):

data.append(d)

 

return data

 

 

if __name__ == '__main__':

'''

主函数:程序入口

'''

# 输入起止日期,爬取之间的新闻

beginDate = input('请输入开始日期:')

endDate = input('请输入结束日期:')

data = get_date_list(beginDate, endDate)

 

for d in data:

year = str(d.year)

month = str(d.month) if d.month >=10 else '0' + str(d.month)

day = str(d.day) if d.day >=10 else '0' + str(d.day)

download_rmrb(year, month, day, 'data')

print("爬取完成:" + year + month + day)

# time.Sleep(3) # 怕被封 IP 爬一爬缓一缓,爬的少的话可以注释掉

实际运行测试一下,假设我们要爬取 2019年 4月份的全部报纸内容,那我们在开始日期中输入20190401,在结束日期中输入 20190501,回车运行。

 

 

 

等待一段时间后,程序便已经运行完成,我们去文件夹中看一下,我们爬到的内容吧。

 

 

 

 爬到的内容整整齐齐的排列在文件夹中,真的是很舒胡啊,随便打开一个文档看一下,也没问题。

 

 

 

 本此爬虫到这里也就算是全部完成了。

 

 

————————————————

版权声明:本文为CSDN博主「机灵鹤」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/wenxuhonghe/article/details/90047081


若金色的阳光停止了它耀眼的光芒,你的一个微笑,将照亮我的整个世界