【爬虫学习笔记】一、利用简单知识完成一次基础爬虫实践

本系列学习笔记使用《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主页浏览,内容仅作学习使用,如有纰漏,请联系我,谢谢~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值