本系列学习笔记使用《python3网络爬虫开发实战》第二版
该书项目地址:https://github.com/Python3WebSpider
崔庆才老师的个人网站:https://cuiqingcai.com
适千里者,三月聚粮。
文章目录
前言
本文通过一次基础的上手实践介绍了爬虫的基本逻辑和基础内容。
大约阅读时长为15分钟。
对于新手而言,可以收获:
- 一次完整的爬虫体验
- 了解HTTP请求库requests的基本用法
- 认识正则表达式并了解正则表达式库re的基本用法
- web网页基础
你需要做的准备有:
- 安装好python3,版本应在3.6以上(官网地址:https://www.python.org)
- 配置环境,推荐新手使用anoconda(官网地址:https://www.anaconda.com),B站中有许多相应的安装教程
本案例对应的学习内容为《python3网络爬虫开发实战》第二版中第1~2章的内容
提示:以下是本篇文章正文内容
一、通过业务分析明确需求
本案例的需求为抓取某网站中的文件,以便后续对文件进行文本分析、主题分类、区域偏向分析与各地发布进度等分析。
二、观察网页结构
1.列表页
包含序号、标题、字号、发文日期等内容,页面底端有序号。
按下Carl+Shift+I,打开开发者工具,查看本页面代码,如下截图:
1. 查看列表页文件内容,class="content-bottom-title"属性中存放表头内容,class="list-content"属性中存放表格内容,也就是爬取的目标:
2. 查看每个文件网址(下面统一表述为详情页),可以总结网址规律为:“http://www.echinagov.com/policy/” + " 序号 " + " .html ",这也是我们获取详情页网址的另外一种方法。
3. 继续查看代码,class="pageNum"属性中存放不同页码对应的跳转网址
查看每个列表页网址,可以总结网址规律为:“http://www.echinagov.com/node/116_” + " 页码 "
至此,列表页的分析已经完成,我们明确了从列表页能够获取的信息: 序号 | 标题 | 字号 | 发文日期 | 文件网址 | 列表页规律,设定爬取目标为以上信息,并将这些信息形成一个excel表,其中文件网址可以用于详情页内容的爬取。
2.详情页
点击某一政策,进入政策详情页,打开开发者工具,查看本页面代码,如下截图:
1. class="indexing"属性中存放有详细属性,如 主题分类 | 发文机构 | 成文日期 | 标题 | 发文日期 | 字号。
2. class="article article-policy-library"属性中存放有详细内容,class="article-header"属性中存放文件头部信息,class="article-body"属性中存放文件内容信息。
通过对详情页的分析,我们看到详情页中比列表页显示属性较全,相比之下仅缺少“序号”及“网址”属性,且序号可以从网址中提取。基于此,我们将爬取策略调整为在列表页中仅爬取网址,在详情页中爬取详细属性,形成excel表。详情页由于是文字信息,可以将每个文件以.json格式保存。
三、数据抓取
1.导入所需库并设置基础量
代码如下:
# -*- coding:utf-8 -*-
import requests
from urllib.parse import urljoin
import logging
import random
import re
from os import makedirs
from os.path import exists
from openpyxl import load_workbook
import pandas as pd
# logging用于输出运行日志
logging.basicConfig(level=logging.INFO ,
format='%(asctime)s - %(levelname)s:%(message)s')
# 设置列表页基础的url
BASE_URL = 'http://www.echinagov.com/node/116_'
# 设置总共要爬取的页数,可自由设定
TOTAL_PAGE = 52
2.定义信息获取函数及列表页网址拼接函数,获取列表页网址
代码如下:
# 信息获取函数,传入url,获取该页面信息
def scrape_page(url):
logging.info('scraping %s...', url)
try :
# 使用requests的get方法获取网页内容
response = requests.get(url )
# 使用.status_code网页状态码为200(意味着有效链接)时,使用.text返回网页抓取内容
if response.status_code == 200 :
return response.text
logging.error('get invalid status code %s while scraping %s' , response.status_code , url)
# 异常值处理,用于反馈异常原因
except requests.RequestException :
logging.error('error occurred while scraping %s ' , url , exc_info=True )
# 列表页网址拼接函数,传入页数,与BASE_URL合并,形成列表页url
def scrape_index(page):
index_url = f'{BASE_URL}{page}'
# 返回列表页url并直接调用scrape_page(url)
return scrape_page(index_url)
3.定义列表页解析函数,获取详情页网址
将scrape_page(url)函数返回的网页内容(response.text)定义为html传入parse_index(html)函数,使re(正则表达式库)进行解析匹配,获取目标信息。
# 解析列表页,使用正则表达式获取政策文件网址链接
def parse_index(html):
# 使用re.compile将正则表达式的字符串形式编译为一个Pattern对象
url_pattern = re.compile('<span.*?class="list-content-title".*?<a.*?href="(.*?)".*?title=.*?</a>.*?</span>', re.S)
# 使用正则表达式找到网页结构中所有符合要求的内容,也就是所有详情页的网址
items = re.findall(url_pattern , html )
if not items :
return [ ]
for item in items :
detail_url = item
logging.info('get detail url %s', detail_url)
yield detail_url
这里将上述代码中涉及正则表达式的两句代码展开解释:
查看网页代码,有许多区块,那么我们只能通过唯一属性来获取我们想要的目标。这里标绿的部分作为目标值(也就是网址)的定位属性,“.*?”是通配符,代码中有许多通配符,为了拿到我们需要的值,所以约定带括号的为目标值,这从上图中能够发现对应关系。
4.定义详情页爬取的函数
# 这里传入详情页网址,调用之前写好的信息获取函数
def scrape_detail(url):
return scrape_page(url)
5.定义详情页解析函数,抓取信息
这里正则表达式部分与上述相似,不再赘述。
# 解析详情页,传入的网址是为了取得政策序号
def parse_detail(html , url ):
# 获取“发文字号”的正则表达式
filenum_pattern = re.compile('<li.*?<span.*?发文字号.*?</span>(.*?)</li>', re.S)
# 获取“发文机关”的正则表达式
dep_pattern = re.compile('<li.*?<span.*?发文机关.*?</span>(.*?)</li>', re.S)
# 获取“主体分类”的正则表达式
class_pattern = re.compile('<li.*?<span.*?主题分类.*?</span>(.*?)</li>', re.S)
# 获取“成文日期”的正则表达式
makedate_pattern = re.compile('<li.*?<span.*?成文日期.*?</span>(.*?)</li>', re.S)
# 获取“发文日期”的正则表达式
data_pattern = re.compile('<li.*?<span.*?发文日期.*?</span>(.*?)</li>', re.S)
# 获取“标题”的正则表达式
title_pattern = re.compile('<div.*?article-header.*?<h1>(.*?)</h1>.*?</div>', re.S)
# 获取序号
num = re.search('http.*?policy/(.*?).htm' , str(url) )
num = num.group(1)
# 获取“发文字号”
filenum = re.findall(filenum_pattern , html ) if re.search(filenum_pattern , html) else None
dep = re.findall(dep_pattern , html ) if re.search(dep_pattern , html) else None
policy_thesis = re.findall(class_pattern , html ) if re.search(class_pattern , html) else None
makedate = re.findall(makedate_pattern , html ) if re.search(makedate_pattern , html) else None
data = re.findall(data_pattern , html ) if re.search(data_pattern , html) else None
title = re.findall(title_pattern , html ) if re.search(title_pattern , html) else None
url = url
doc = pq(html)
content = doc('.article-content')
content = content.text()
# 构造字典,这部分进包含文件的属性信息,用于后续生产.csv文件,便于统计和查看
info_dict_index = { 'num' : num ,
'filenum' : filenum ,
'dep' : dep ,
'class' : policy_thesis,
'makedate': makedate,
'data' : data ,
'title' : title ,
'url' : url }
# 构造字典,这部分包含文件内容,用于后续储存成.json格式的单个文件
info_dict = { 'num' : num ,
'filenum' : filenum ,
'dep' : dep ,
'class' : policy_thesis,
'makedate': makedate,
'data' : data ,
'title' : title ,
'url' : url ,
'content' : content}
# 返回以上两个字典
return info_dict_index , info_dict
至此,我们已经抓取到了所有目标数据,接下来进入数据存储部分。
四、数据存储
我们在上一小节中返回两部分数据, info_dict_index用于后续生产.csv文件, info_dict用于后续储存成.json格式的单个文件,我们分别为此定义两个储存函数。
1.定义csv储存函数
RESULTS_DIR = '../results'
exists(RESULTS_DIR) or makedirs(RESULTS_DIR)
def save_data(data ) :
# 使用pandas库,将字典转换成DataFrame对象
df = pd.DataFrame.from_dict( data , orient='index')
# 由于爬取到的数据行列不符合阅读习惯,使用.T进行行列转换
df = df.T
# 使用.to_csv将数据保存,其中定义了文件名,mode='a'为追加模式,不覆盖文档原内容,持续写入数据
# 注意这里需要设置encoding为'utf-8-sig'格式,确保中文不乱码(如果乱码还可以尝试使用encoding='utf-8')
df.to_csv('../getEgovPolicy.csv', mode='a', header=False, index=False ,encoding='utf-8-sig')
2.定义json储存函数
RESULTS_DIR = '../results'
exists(RESULTS_DIR) or makedirs(RESULTS_DIR)
def save_book_data(data):
# 从字典中获取num对应值,也就是序号
name = data.get('num')
# 设定文件保存路径,以序号作为文件名
data_path = f'{RESULTS_DIR}/{name}.json'
# 使用.dump将数据格式化为字符串
json.dump(data , open(data_path , 'w' , encoding='utf-8') , ensure_ascii=False , indent=2)
五、程序运行
1.定义主函数
将前边所有定义的函数串在一起:
def main(page):
# 传入列表页网址,使用scrape_index(page)返回网页结构信息
index_html = scrape_index(page)
# 将网页结构信息传入parse_index(index_html)解析,获取详情页网址列表(一页有10个政策文件网址)
detail_urls = parse_index(index_html)
# 循环每个详情页网址
for detail_url in detail_urls :
# 传入详情页网址,使用scrape_index(page)返回网页结构信息
detail_html = scrape_detail(detail_url)
# 将网页结构信息传入parse_detail(index_html)解析,返回 info_dict_index、info_dict两部分数据
data = parse_detail(detail_html , detail_url )
# 状态信息展示
logging.info('detail detaildata %s', data)
# 将info_dict_index信息传入csv储存函数
save_data(data[0])
# 将info_dict信息传入json储存函数
save_book_data(data[1])
logging.info('data saved successfully')
2.使用进程池,多线程抓取
为加快抓取速度,我们采用多线程的方式进行抓取:
if __name__ == '__main__' :
pool = multiprocessing.Pool()
pages = range(2 , TOTAL_PAGE)
# 将不同的列表页传入进程池中
pool.map(main , pages)
pool.close()
pool.join()
3.运行结果
- 数据表
- json文件
- json文件内容
总结
以上就是最基本的爬虫方法,本文仅仅简单的介绍了一些库的基本使用方法,在代理池使用、网页解析、数据存储等方面,还不够快捷。
例如,有Beautiful Soup、XPath、pyquery、parsel等更加好用的网页解析库,在存储方面有MySQL、MongoDB、Reids等数据库能够更灵活的保存数据,在后续的学习当中都会逐渐接触到。
完整代码可在我的GitHub主页浏览,内容仅作学习使用,如有纰漏,请联系我,谢谢~