一、前言
在写这篇博客之前,笔者是爬取了深圳市政务公开、政府公报、政府工作、新闻报道、政策解读等所有文件,由于这些网页的大体结构都差不多,本文则主要介绍爬取政务公开文件(包括市政府令、市政府文件、市政府函、市政府办公厅文件、市政府办公厅函、部门规范性文件),网站地址可点击此处查看,也可参考下图所示。

二、获取文件URL列表
1.获取各类文件的URL
如上图所示,可以看到政务公开文件有很多个不同的类别,点击各个类别右侧的更多就能到相应类型的网址,下面是政务公开文件各个类型的网址。
1 | target_urls = [ |
2.获取每类文件的总页数
如下图所示,可以看到左侧菜单就是政务公开文件的各个类型,正文部分就是相应文件的列表,下方圈注的则是每类文件的总页数。通过点击下一页,可以看到每页的URL其实就是在最初的URL后面加上了页数,比如:
http://www.sz.gov.cn/zfwj/zfwjnew/szfl_139222/index_1.htm
http://www.sz.gov.cn/zfwj/zfwjnew/szfl_139222/index_3.htm
http://www.sz.gov.cn/zfwj/zfwjnew/szfl_139222/index_6.htm
所以首先我们需要获取每类文件的总页数,然后再到每页上获取文件的URL列表。

审查网页元素定位到总页数的所在位置,然后通过解析html的文本获取总的页数,详细的代码可参考下面。首先需要说明get_html_text函数,它的作用是获取网页的html文本内容,返回str类型,该函数是爬取所有网页数据的基础。而get_total_page_num函数的作用则是获取每类文件的总页数。
1 | def get_html_text(url, params=None, proxies=None, total=3): |
获取到了总的页数之后,需要拼接出每一个网页的URL,就是单纯的字符串操作,直接放代码了。
1 | def get_all_pages_urls(self, url, page_num): |
3.获取每个网页上的文件URL
首先同样需要获取网页的html文本,然后需要通过xpath解析得到每页文件的超链接,这里主要使用的是lxml包中的etree解析HTML,代码如下。由于每个文件的超链接都不一定是完整的URL,所以部分是需要拼接的。1
2
3
4
5
6
7
8
9
10
11def get_info_urls_of_public(self, url):
"""
爬取政府文件网页上通知文件的链接
:param url: target url
:return: info_urls list
"""
page_content = get_html_text(url)
html = etree.HTML(page_content)
urls = html.xpath('//div[@class="zx_ml_list"]/ul/li/div/a/@href')
urls = list(map(lambda x: 'http://www.sz.gov.cn' + x.split('..')[-1] if 'http' not in x else x, urls))
return urls
三、爬取文件内容
1.爬取文件的基本信息和内容
如下图所示,每个文件包含有索引号、分类、发布机构、名称、发布日期、文号和主题词等基本信息,这些都是需要爬取的。

同样通过审查元素定位到它们的位置,然后通过xpath解析得到相应的值,具体的实现代码如下所示。值得注意的是,获取到了这些基本信息后,需要对其进行encode编码,不然后面保存到excel中会出错。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40def get_notification_infos(self, url):
"""
在通知文件页面爬取通知的内容
:param url: info url
:return: base_infos, content
"""
page_content = get_html_text(url)
html = etree.HTML(page_content)
# ['索引号', '省份', '城市', '文件类型', '文号', '发布机构', '发布日期', '标题', '主题词']
index = html.xpath('//div[@class="xx_con"]/p[1]/text()')
aspect = html.xpath('//div[@class="xx_con"]/p[2]/text()')
announced_by = html.xpath('//div[@class="xx_con"]/p[3]/text()')
announced_date = html.xpath('//div[@class="xx_con"]/p[4]/text()')
title = html.xpath('//div[@class="xx_con"]/p[5]/text()')
document_num = html.xpath('//div[@class="xx_con"]/p[6]/text()')
key_word = html.xpath('//div[@class="xx_con"]/p[7]/text()')
base_infos = [index, ['广东省'], ['深圳市'], aspect, document_num, announced_by, announced_date, title, key_word]
# encode是为了保存Excel,否则出错
base_infos = list(map(lambda x: x[0].encode('utf-8') if len(x) > 0 else ' ', base_infos))
# print('This is basic info: ', base_infos)
paragraphs = html.xpath('//div[@class="news_cont_d_wrap"]//p') # 段落信息
contents = []
for paragraph in paragraphs:
contents.append(paragraph.xpath('string(.)').strip())
contents = '\n'.join(contents)
# deal with attachments
attachments = []
script_str = html.xpath('//div[@class="fjdown"]/script/text()')[0]
# if there are attachments, then get attachments from script
if script_str.find('var linkdesc="";') == -1:
attach_names = script_str.split('var linkdesc="')[-1].split('";')[0].split(';')
attach_urls = script_str.split('var linkurl="')[-1].split('";')[0].split(';')
suffix = url.split('/')[-1]
for k in range(len(attach_urls)):
attach_url = url.replace(suffix, attach_urls[k].split('./')[-1])
attach_name = attach_names[k].replace('/', '-').replace('<', '(').replace('>', ')')
# print(attach_name, attach_url)
attachments.append([attach_name, attach_url])
# print(contents)
return base_infos, contents, attachments
2.下载相应的附件
上面代码中的最后部分是在处理附件信息,因为附件是通过javascript生成的,因此是无法直接通过xpath获取到附件的URL信息的。只能分割html文本字符串,找出附件的URL和附件名信息。下面的代码就是传入附件URL和名字,进行附件下载,而且任何类型的附件都是可以下载的,包括word、excel、pdf、mp4等等。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21def download_file(file_url, filename, total=3):
"""
下载文件
:param file_url: 文件URL
:param filename: 文件名
:param total: 最大下载次数(防止失败)
:return: bool value
"""
try:
res = requests.get(file_url)
if res.status_code == 200:
fp = open(filename, mode='wb')
fp.write(res.content)
fp.close()
else:
print('File Not Found:', file_url)
except Exception:
if total > 0:
return download_file(file_url, filename, total - 1)
return False
return True
四、保存结果
1.保存单个文件内容到word
在前面的步骤中,我们已经爬取到了文件的内容,这里就将内容存入到word文档中,并以通知的名称作为保存的word文件名。但是,由于windows中一些特殊符号是无法保存为文件名的,则需要将它们都替换掉(比如”/“、”<”、”>”等),以及文件名不能过长。之前对文件的基本信息进行了encode编码,所以此处需要decode解码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43def write_notification_to_docx(self, save_dir, base_infos, contents, attachments):
"""
保存通知的详细信息到word文件
:param save_dir: 保存的路径
:param base_infos: 通知的基本信息('索引号', '省份', '城市', '文件类型', '文号', '发布机构', '发布日期', '标题', '主题词')
:param contents: 通知内容
:param attachments: 附件信息
:return: None
"""
title = bytes.decode(base_infos[-2]).replace('/', '-').replace(' ', '') \
.replace('<', '(').replace('>', ')').replace('"', '-')
# 下载附件
if len(attachments) > 0:
# 有附件则要新建目录
save_dir = save_dir + '/' + title
os.mkdir(save_dir)
for attachment in attachments:
suffix = attachment[1].split('.')[-1]
attach_name = attachment[0] + '.' + suffix if suffix not in attachment[0] else attachment[0]
# 不下载mp4视频,速度太慢
if suffix != 'mp4':
download_file(file_url=attachment[1], filename=save_dir + '/' + attach_name)
# 处理文件名过长
filename = save_dir + '/' + title + '.docx'
filename = save_dir + '/...' + title[-50:] + '.docx' if len(filename) > 180 else filename
# 写入word文档
write_word_file(filename=filename,
title=title, data_list=[contents])
def write_word_file(filename, title, data_list):
"""
write text data to word file
:param filename: word filename, like '/path/filename.doc' or '/path/filename.docx'
:param title: title for word file, or maybe None
:param data_list: paragraphs of word content
:return: None
"""
doc = docx.Document()
if title:
doc.add_heading(title, 0)
for data in data_list:
doc.add_paragraph(data)
doc.save(filename)
2.保存所有文件基本信息到excel
为了方便查看,笔者这里将所有文件的基本信息都已写入到了excel文件中,主要依托下面的write_excel_file函数,作用是向已经存在的excel文件中增加新的类别sheet,并写入每类文件的基本信息。注意一定先新建要保存的excel文件,不然保存的时候就会提示文件不存在错误。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48def run(self, excel_name, sheet_name, save_dir, target_url):
"""
主程序运行的入口
:param excel_name: 保存的Excel表名
:param sheet_name: 保存的Excel表sheet名
:param save_dir: 保存文本的目录
:param target_url: 要爬取的home url
:return: None
"""
base_info_list = []
# 以下三个需要根据网页的 html 结构来设置
prefix = 'createPageHTML('
suffix = ');'
delimiter = ','
num = get_total_page_num(target_url, prefix, suffix, delimiter)
page_urls = self.get_all_pages_urls(target_url, num)
for page_url in page_urls:
# info_urls = self.get_info_urls_of_public(page_url) # 政府文件用
info_urls = self.get_info_urls_of_policy(page_url) # 政策解读用
for info_url in info_urls:
print('Get Information From ', info_url)
if '' != info_url:
base_infos, contents, attachments = self.get_notification_infos(info_url)
# 剔除掉没有爬到内容的通知
if contents.strip() != '':
self.write_notification_to_docx(save_dir, base_infos, contents, attachments)
base_info_list.append(base_infos)
# 写入信息到Excel
if len(base_info_list) > 0:
columns = ['索引号', '省份', '城市', '文件类型', '文号', '发布机构', '发布日期', '标题', '主题词']
write_excel_file(filename=excel_name, data_list=base_info_list,
sheet_name=sheet_name, columns=columns)
def write_excel_file(filename, data_list, sheet_name='Sheet1', columns=None):
"""
write list data to excel file, supporting add new sheet
:param filename: excel filename, like '/path/filename.xlsx' or '/path/filename.xls'
:param data_list: list data for saving
:param sheet_name: excel sheet name, default 'Sheet1'
:param columns: excel column names
:return: None
"""
writer = pd.ExcelWriter(filename)
frame = pd.DataFrame(data_list, columns=columns)
book = load_workbook(writer.path)
writer.book = book
frame.to_excel(excel_writer=writer, sheet_name=sheet_name, index=None)
writer.close()
最终保存的excel结果可以参看下图,可以下方看到sheet名表示着不同类别的文件,而且文件的基本信息都是很完整的。

五、致谢(含源代码)
最后,非常感谢大家的观看,有问题的小伙伴也可以在下方评论留言。这里笔者已将所有源代码都公开共享到了github上,包括爬取政务公开、政府公报、新闻报道、政策解读等的所有文件,有需要的可点击此处前往下载。

...
...