基础爬虫实战: 抓取静态网页的信息【爬虫】

基础爬虫实战: 抓取静态网页的信息

实现一个爬虫来爬取一个静态网页的信息。

文章源地址:修能的博客

完整代码请参见本文末尾,或点击此处,或者进入GitHub自取

项目概述

准备工作

  • Python3.6及以上,并且环境配置完全。
  • 了解Python多进程的基本原理
  • 了解PythonHTTP请求库requests的基本用法。
  • 了解正则表达式的用法和Python中正则表达式re的基本用法。

爬取目标

需要爬取的链接是:https://ssr1.scrape.center/,这个网站包含了一些电影信息。

我们点击其中一部电影就可以看到详细的信息。

要完成的目标

  • 利用requests库爬取这个站点每一页的电影列表,顺着列表在爬取每个电影的详情页。
  • 用正则表达式提取每部电影的名称、封面、类别、上映时间、评分、剧情简介等内容。
  • 把以上爬取的内容保存为JSON文件。
  • 使用多进程实现爬取的加速。

开始爬取

爬取列表页

第一步: 分析页面

要想爬取一个网站我们就要了解要爬取网站的构成。
使用开发者工具查看页面。

发现每部电影对应的是一个div节点,而且这些节点的class属性都有el-card这个值。

注意到每个列表有十个div节点,说明一页有十部电影。

选中第一个电影的名称,可以发现这个名称其实是一个h2节点(其实就是这个节点中的二级标题),在h2节点的外面包含一个a节点,a节点有一个href属性,这是一个超链接,其中的href的值为:/detail/1,这是一个相对于网站的根URL的地址,可以还原成:https://ssr1.scrape.center/detail/1,这就是详情页的URL。所以得出结论:只要获取到了href属性的值,我们就可以获取到电影的详情页URL。,之后我们就可以在详情页中进行爬取信息了。

第二步: 分析翻页的操作逻辑

进行翻页时,我们可以观察到网站的URL从https://ssr1.scrape.center/page/1转换成了https://ssr1.scrape.center/page/2,这就是翻页的逻辑,我们也顺利的得到了各页的URL。

代码实现

完成对列表页的爬取的实现,步骤是:

  • 遍历所有页码,构造出10页的索引页URL。
  • 从每个索引页中分析提取出每个电影的详情页URL。

Code:

###########part_1###########
import requests
import logging
import re
from urllib.parse import urljoin

###########part_2###########
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s - %(levelname) : %(message)s')

BASE_URL = 'https://ssr1.scrape.center'
TOTAL_PAGE = 10

part_1中,我们引入了requests库来进行网页的抓取、logging库来输出信息、re库来实现正则表达式对页面信息的匹配和urllib.parse.urljoin()方法来做对URL的拼接。

part_2中,首先是使用logging.basicConfig()方法定义日志输出级别和输出格式,BASE_PAGE为当前爬取站点的根URL,TOTAL_PAGE设定了需要爬取页面的总数.

Code:

###########part_3###########
def scrape_page(url):
    logging.info('scraping %s...',url)
    try:
        response = requests.get(url)
        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 occured while scraping %s',url,exc_info=True)   

def scrape_index(page):
    index_url = f'{BASE_URL}/page/{page}'
    return scrape_page(index_url)  

def parse_index(html):
    pattern = re.compile('<a.*?href="(.*?)".*?class="name">')
    items = re.findall(pattern,html)
    if not items:
        return []
    for item in items:
        detail_url = urljoin(BASE_URL,item)
        logging.info('get detail url %s',detail_url)
        yield detail_url

def main():
    for page in range(1,TOTAL_PAGE+1):
        index_html = scrape_index(page)
        detail_urls = parse_index(index_html)
        logging.info('detail urls %s',list(detail_urls))

if __name__ == "__main__":
    main()

这是一个较为通用的爬取页面方法,sreape_page,接受一个url参数,返回页面的HTML代码。如果状态码是不是200,如果是直接返回页面的HTML代码,如果不是,则输出对应的错误日志信息。

scrape_index(page)方法就是列表页的爬取方法,按照翻页的逻辑,实现URL的拼接。

parse_index(html)方法接受一个参数html,级列表页的HTML的代码。
pattern参数指明了正则表达式的匹配模式,即提取a节点参数href的值,然后通过yield逐个返回详情页的URL。

最后将上述的函数组合就可以爬取到详情页了。

Result:

2023-07-12 21:18:00,427 - INFO : scraping https://ssr1.scrape.center/page/1...
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/1
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/2
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/3
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/4
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/5
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/6
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/7
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/8
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/9
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/10
2023-07-12 21:18:00,593 - INFO : detail urls ['https://ssr1.scrape.center/detail/1', 'https://ssr1.scrape.center/detail/2', 'https://ssr1.scrape.center/detail/3', 'https://ssr1.scrape.center/detail/4', 'https://ssr1.scrape.center/detail/5', 'https://ssr1.scrape.center/detail/6', 'https://ssr1.scrape.center/detail/7', 'https://ssr1.scrape.center/detail/8', 'https://ssr1.scrape.center/detail/9', 'https://ssr1.scrape.center/detail/10']
2023-07-12 21:18:00,593 - INFO : scraping https://ssr1.scrape.center/page/2...

......

详情页爬取成功了。

爬取详情页

第一步: 分析页面

通过开发者工具分析发现以下特征:

  • 封面: img节点,class属性为cover,src属性为封面图片的URL。
  • 名称: h2节点,其内容是电影名称。
  • 类别: span节点,其内容是电影的名称。其外层是button节点,再外层是div节点,其class属性categories
  • 上映时间: span节点,其内容是上映的时间+“上映”,提取时注意去掉“上映”两字。外层是div节点,其class属性m-v-sm info,正则匹配时匹配info
  • 评分: p节点,其内容是评分,class属性score
  • 剧情简介: p节点,其内容是剧情简介。其外层是h3节点,其内容是“剧情简介”。再外层是属性为dramadiv节点
代码实现

Code:

###########part_4###########

def scrape_detail(url):
    return scrape_page(url)

def parse_detail(html):
    cover_pattern = re.compile('class="item.*?<img.*?src="(.*?)".*?class="cover">',re.S)
    name_pattern = re.compile('<h2.*?>(.*?)</h2>')
    categories_pattern = re.compile('<button.*?category.*?<span>(.*?)</span>.*?</button>',re.S)
    published_at_pattern = re.compile('(\d{4}-\d{2}-\d{2})\s?上映')
    drama_at_pattern = re.compile('<div.*?drama.*?>.*?<p.*?>(.*?)</p>',re.S)
    score_pattern = re.compile('<p.*?score.*?>(.*?)</p>',re.S)

    cover = re.search(cover_pattern,html).group(1).strip() if re.search(cover_pattern,html) else None
    name = re.search(name_pattern,html).group(1).strip() if re.search(name_pattern,html) else None
    categories = re.findall(categories_pattern,html) if re.findall(categories_pattern,html) else None
    published_at = re.search(published_at_pattern,html).group(1) if re.search(published_at_pattern,html) else None
    drama_at = re.search(drama_at_pattern,html).group(1).strip() if re.search(drama_at_pattern,html) else None
    score = float(re.search(score_pattern,html).group(1).strip())if re.search(score_pattern,html) else None

    return {
        'cover': cover,
        'name': name,
        'categories': categories,
        'published_at': published_at,
        'drmar_at': drama_at,
        'score': score
    }

定义函数scrape_detail(),该方法返回详情页的html代码。单独实现函数scrape_detail(),可以增强程序的灵活性和易读性。

定义函数parse_detail(html),对html代码进行匹配,最后返回一个字典对象。

将数据保存

这里采用JSON格式来保存数据,编写以下保存函数以及初始设置。

Code:

import json
from os import makedirs
from os.path import exists

RESULTS_DIR = 'results'
exists(RESULTS_DIR) or makedirs(RESULTS_DIR)

def save_data(data):
    name = data.get('name')
    data_path = f'{RESULTS_DIR}/{name}.json'
    json.dump(data,open(data_path,'w',encoding='utf-8'),ensure_ascii=False,indent=2)
运行爬虫

文章源地址:修能的博客
改写main()
Code:

def main():
    for page in range(1,TOTAL_PAGE+1):
        index_html = scrape_index(page)
        detail_urls = parse_index(index_html)
        for detail_url in detail_urls:
            detail_html = scrape_detail(detail_url)
            data = parse_detail(detail_html)
            logging.info('get detail data %s',data)
            logging.info('saving data to json file...')
            save_data(data)
            logging.info('data saved successfully!^-^')

运行结果:

多线程加速

<待完善>将十页的爬取同时进行,为每一页开一个进程来爬取。而且因为这10个列表页面正好可以提前构造成一个列表,所以选用多进程里面的进程池Pool来实现这个过程。

改写main()

Code:

def main(page):
    index_html = scrape_index(page)
    detail_urls = parse_index(index_html)
    for detail_url in detail_urls:
        detail_html = scrape_detail(detail_url)
        data = parse_detail(detail_html)
        logging.info('get detail data %s', data)
        logging.info('saving data to json file...')
        save_data(data)
        logging.info('data saved successfully!^-^')


# 该代码还需学习
if __name__ == "__main__":
    pool = multiprocessing.Pool()
    pages = range(1, TOTAL_PAGE + 1)
    pool.map(main, pages)
    pool.close()
    pool.join()

完整代码

Code:

###########part_1###########
import requests
import logging
import re
import json
from os import makedirs
from os.path import exists

from urllib.parse import urljoin
import multiprocessing

###########part_2###########
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s - %(levelname)s : %(message)s')

BASE_URL = 'https://ssr1.scrape.center'
TOTAL_PAGE = 10

RESULTS_DIR = 'results'
exists(RESULTS_DIR) or makedirs(RESULTS_DIR)


###########part_3###########
def scrape_page(url):
    logging.info('scraping %s...', url)
    try:
        response = requests.get(url)
        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 occured while scraping %s', url, exc_info=True)


def scrape_index(page):
    index_url = f'{BASE_URL}/page/{page}'
    return scrape_page(index_url)


def parse_index(html):
    pattern = re.compile('<a.*?href="(.*?)".*?class="name">')
    items = re.findall(pattern, html)
    if not items:
        return []
    for item in items:
        detail_url = urljoin(BASE_URL, item)
        logging.info('get detail url %s', detail_url)
        yield detail_url


###########part_4###########

def scrape_detail(url):
    return scrape_page(url)


def parse_detail(html):
    cover_pattern = re.compile('class="item.*?<img.*?src="(.*?)".*?class="cover">', re.S)
    name_pattern = re.compile('<h2.*?>(.*?)</h2>')
    categories_pattern = re.compile('<button.*?category.*?<span>(.*?)</span>.*?</button>', re.S)
    published_at_pattern = re.compile('(\d{4}-\d{2}-\d{2})\s?上映')
    drama_at_pattern = re.compile('<div.*?drama.*?>.*?<p.*?>(.*?)</p>', re.S)
    score_pattern = re.compile('<p.*?score.*?>(.*?)</p>', re.S)

    cover = re.search(cover_pattern, html).group(1).strip() if re.search(cover_pattern, html) else None
    name = re.search(name_pattern, html).group(1).strip() if re.search(name_pattern, html) else None
    categories = re.findall(categories_pattern, html) if re.findall(categories_pattern, html) else None
    published_at = re.search(published_at_pattern, html).group(1) if re.search(published_at_pattern, html) else None
    drama_at = re.search(drama_at_pattern, html).group(1).strip() if re.search(drama_at_pattern, html) else None
    score = float(re.search(score_pattern, html).group(1).strip()) if re.search(score_pattern, html) else None

    return {
        'cover': cover,
        'name': name,
        'categories': categories,
        'published_at': published_at,
        'drmar_at': drama_at,
        'score': score
    }


def save_data(data):
    name = data.get('name')
    data_path = f'{RESULTS_DIR}/{name}.json'
    json.dump(data, open(data_path, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)


def main(page):
    index_html = scrape_index(page)
    detail_urls = parse_index(index_html)
    for detail_url in detail_urls:
        detail_html = scrape_detail(detail_url)
        data = parse_detail(detail_html)
        logging.info('get detail data %s', data)
        logging.info('saving data to json file...')
        save_data(data)
        logging.info('data saved successfully!^-^')


if __name__ == "__main__":
    pool = multiprocessing.Pool()
    pages = range(1, TOTAL_PAGE + 1)
    pool.map(main, pages)
    pool.close()
    pool.join()

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值