案例02--scrapy综合练习--中大网校

一 抓取需求

# 抓取需求

start_url = 'http://ks.wangxiao.cn/'
抓取 首页 各分类下 ---> 各种考试 ---> 考点练习里 各科目的 练习题

eg:
工程类 ---> 一级建造师 ---> 建设工程经济 --->章节 


# 存储需求: 文件存储   题目中可能包含图片 md格式比较合适
按照分类 依次 存为文件夹目录,最后题目中 章节 最底层的小节名 为文件名,存储为markdown格式的文件

eg:
工程类
  - 一级建造师
    - 建设工程经济
      - 第1篇工程经济
        - 第1章资金时间价值计算及应用
         - ...
            -4.利率.md
            
            -4.利率--img
                -1.jpg
            
            
# 图片存放位置:文件名 同级的 '文件名--img' 文件夹里

二 基础分析

2.1 首页分析

# 首页分析
1.从首页的源码中,能够直接获取到  各分类 和 考试、以及第二页的 链接
   eg: 工程类、 一级建造师、一级建造师的链接(但默认是到 模拟考试的 http://ks.wangxiao.cn/TestPaper/list?sign=jz1)
        
        
2.链接分析  
需要的是考点练习,而不是模拟考试,但用不着 非得从页面获取,直接根据链接规律自己替换
模拟考试:http://ks.wangxiao.cn/TestPaper/list?sign=jz1  
考点练习:http://ks.wangxiao.cn/exampoint/list?sign=jz1
    
第二个路径 由 TestPaper,替换为 exampoint 就行

2.2 章节存储分析

### 0 前提
按照 篇章/章节/小节/考点,为文件路径 进行文件存储


### 1 分析
eg: http://ks.wangxiao.cn/exampoint/list?sign=jz1 下考题网页结构
根据不同考试的不同科目,有可能出现 多种层级结构  

# 情况一:
篇章名   ul[@class="chapter-item"]
  
    章节名   ul[@class="section-item"]
        
        小节名   ul[@class="section-item"]
            
            考点  ul[@class="section-point-item"]

# 情况二:
篇章名   ul[@class="chapter-item"]
  
    章节名   ul[@class="section-item"]
        
        考点  ul[@class="section-point-item"]    
        
# 情况三:
篇章名   ul[@class="chapter-item"]
  
    考点  ul[@class="section-point-item"]  
    
# 情况四:
篇章名   ul[@class="chapter-item"]   # 有些篇章 直接就是考点



### 2 总结:
若是按照正向的顺序 一层一层 向下查找,中间的section-item 不知道多少层
而且 for循环 迭代的 嵌套层数 也特别麻烦,每层的名字都需要获取到,直到最底层。

故反向查找  就很比较轻松一些,借助 xpath的语法 ancestor(n.祖先) 获取祖先节点
   # ancestor  默认获取所有的祖先节点,返回的是包含各个父节点正向顺序的 列表 
     [根节点, ..., 父父父节点,父父节点,父节点]
        
     
   # ancestor::标签名  一直找到标签名为祖先节点为止
     [标签名, ..., 父父父节点,父父节点,父节点]
    
   

### 3 解决
1.先 获取 最底层考点,依次获取 考点名、以及发送答题(第三层页面)的请求 等等
point = .xpath(".//ul[@class="section-point-item"]") 


2.根据最底层考点,向上 获取祖先节点,获取篇章名等
ancestor_ul = point.xpath("./ancestor::ul")
或 
ancestor_ul = point.xpath('./ancestor::ul[@class="section-item" or class="chapter-item"]')

2.3 题库json分析

# 前提:在最底层考点层时,有“开始答题”,发送新的异步请求,返回的题库json数据

# question_list_url:https://ks.wangxiao.cn/practice/listQuestions


# 核心:其他题型(单选、多选、简述 n个题目 + n个答案)  和 材料题(1个背景材料 + n个题目 + n个答案)


# json结构解析:
{
    "Data":[
        {
            "questions":[      # 其他题型,题目和答案 都在该questions中
                {
                    "content":"题干",
                    "options":["题选项"],
                    "textAnalysis":"答案解析"
                },            
            ],   
            "materials":null
        },
        
        {
            "questions":null,
            "materials":[      # 材料题,题目和答案 都在该materials中
                {
                    "material":{"content":"背景材料" ...},
                	"questions":[
                		{
                    		"content":"题干",
                        	"options":["题选项"],
                    		"textAnalysis":"答案解析"
                        }, 
                    ]
                }
            ]      
        }
    ]
    
    ...
}


# 总结:
1.直接遍历data 判断 "questions" 是否为空,从而区分 材料题 和 其他题型
2.材料题 获取题目、选项和答案,逻辑 和其他题型一样,可以封装成一个解析函数

三 实现代码

3.1 spiders/ks.py

import json
import scrapy
import os


class KsSpider(scrapy.Spider):
    name = 'ks'
    allowed_domains = ['wangxiao.cn']
    start_urls = ['http://ks.wangxiao.cn/']

    def parse(self, response, **kwargs):
        li_list = response.xpath('//ul[@class="first-title"]/li')
        for li in li_list:
            # 一级类目  eg:工程类
            first_title = li.xpath('./p/span/text()').extract_first()

            a_list = li.xpath('./div/a')
            for a in a_list:
                # 二级类目  eg: 一级建造师
                second_title = a.xpath('./text()').extract_first()
                href = a.xpath('./@href').extract_first()
                # 第二页链接 模拟考试 ,替换成 考点练习
                href = response.urljoin(href).replace('TestPaper', 'exampoint')

                # yield scrapy.Request(
                #     url=href,
                #     callback=self.parse_second,
                #     meta={
                #         'first_title': first_title,
                #         'second_title': second_title
                #     }
                # )

                # 测试: 减少请求,固定单一网页url
                yield scrapy.Request(
                    url='http://ks.wangxiao.cn/exampoint/list?sign=jz1',
                    callback=self.parse_second,
                    meta={
                        'first_title': '工程类',
                        'second_title': '一级建造师'
                    }
                )
                return    # 直接返回,就不会yield新请求

    def parse_second(self, response):
        first_title = response.meta.get('first_title')
        second_title = response.meta.get('second_title')

        a_list = response.xpath('//div[@class="filter-content"]/div[2]/a')
        for a in a_list:
            third_title = a.xpath('./text()').extract_first()
            href = response.urljoin(a.xpath('./@href').extract_first())  # 科目

            yield scrapy.Request(
                url=href,
                callback=self.parse_third,
                meta={
                    'first_title': first_title,
                    'second_title': second_title,
                    'third_title': third_title
                }
            )
            return  # 测试

    def parse_third(self, response):
        first_title = response.meta.get('first_title')
        second_title = response.meta.get('second_title')
        third_title = response.meta.get('third_title')

        chapters = response.xpath('.//ul[@class="chapter-item"]')
        for chapter in chapters:
            point_uls = chapter.xpath('.//ul[@class="section-point-item"]')
            if not point_uls:
                # 没有最底层,直接是"chapter-item",就开始答题
                file_name = ''.join(chapter.xpath('./li[1]//text()').extract()).strip().replace(' ', '')
                file_path = os.path.join(first_title, second_title, third_title)

                top = chapter.xpath('./li[2]/text()').extract_first().split('/')[-1]
                sign = chapter.xpath('./li[3]/span/@data_sign').extract_first()
                subsign = chapter.xpath('./li[3]/span/@data_subsign').extract_first()

                list_question_url ='https://ks.wangxiao.cn/practice/listQuestions'
                data = {
                    'examPointType': "",
                    'practiceType': "2",
                    'questionType': "",
                    'sign': sign,
                    'subsign': subsign,
                    'top': top
                }

                yield scrapy.Request(
                    url=list_question_url,
                    method='POST',
                    body=json.dumps(data),
                    headers={
                        'Content-Type': 'application/json; charset=UTF-8'
                    },
                    callback=self.parse_fourth,
                    meta={
                        'file_path': file_path,
                        'file_name': file_name
                    }
                )

            for point in point_uls:
                # 1.在最底层节点,向上获取 中间层的 篇章名,拼接目录
                item_list = point.xpath('./ancestor::ul')

                title_list = []
                for item in item_list:
                    title = ''.join(item.xpath('./li[1]//text()').extract()).strip().replace(' ', '')
                    title_list.append(title)

                file_path = os.path.join(first_title, second_title, third_title, *title_list)

                # 2.在最底层节点,获取本层的 考点名, 构成文件名
                file_name = ''.join(point.xpath('./li[1]//text()').extract()).strip().replace(' ', '')

                # 3.发生新请求 题目url,开始答题 获取请求需要的参数
                top = point.xpath('./li[2]/text()').extract_first().split('/')[-1]
                sign = point.xpath('./li[3]/span/@data_sign').extract_first()
                subsign = point.xpath('./li[3]/span/@data_subsign').extract_first()

                list_question_url ='https://ks.wangxiao.cn/practice/listQuestions'
                data = {
                    'examPointType': "",
                    'practiceType': "2",
                    'questionType': "",
                    'sign': sign,
                    'subsign': subsign,
                    'top': top
                }

                yield scrapy.Request(
                    url=list_question_url,
                    method='POST',
                    body=json.dumps(data),
                    # 注意:发送json格式数据,请求头要带这个 指明传输的数据格式类型
                    headers={
                        'Content-Type': 'application/json; charset=UTF-8'
                    },
                    callback=self.parse_fourth,
                    meta={
                        'file_path': file_path,
                        'file_name': file_name
                    }
                )

    def parse_fourth(self, response):
        file_path = response.meta.get('file_path')
        file_name = response.meta.get('file_name')

        data = response.json().get('Data')
        for item in data:
            questions = item.get('questions')
            if questions:  # 其他题型
                for question in questions:
                    question_info = self.parse_questions(question)
                    yield {
                        'file_path': file_path,
                        'file_name': file_name,
                        'question_info': question_info
                    }
            else:  # 材料题
                materials = item.get('materials')
                for mater in materials:
                    material = mater.get('material')
                    content = "背景材料:" + '\n' + material.get('content') + '\n'  # 背景材料

                    questions = mater.get('questions')
                    question_list = []
                    for question in questions:
                        question_content = self.parse_questions(question)
                        question_list.append(question_content)

                    question_info = content + '\n'.join(question_list)

                    yield {
                        'file_path': file_path,
                        'file_name': file_name,
                        'question_info': question_info
                    }

    def parse_questions(self, question):
        content = "问题内容:" + '\n' + question.get("content")  # 题干
        options = question.get("options")  # 选项
        options_list = []
        answer_list = []
        for opt in options:
            opt_name = opt.get('name')
            opt_content = opt.get("content")

            is_right = opt.get('isRight')
            if is_right:
                answer_list.append(opt_name)
            options_list.append(opt_name + '.' + opt_content)

        analysis = "参考解析:" + '\n' + question.get("textAnalysis")  # 答案解析
        analysis = "参考答案:" + ','.join(answer_list) + '\n' + analysis  # 拼接 答案和答案解析

        content = content + '\n' + '\n'.join(options_list) + '\n' + analysis + '\n'  # 将题干 选项 答案及解析 全拼在一起
        return content

3.2 pipeline.py

from itemadapter import ItemAdapter
from scrapy.pipelines.images import ImagesPipeline
import os
from lxml import etree
from scrapy import Request


class WangxiaoPipeline:
    def process_item(self, item, spider):
        file_path = item.get('file_path')
        file_name = item.get('file_name')
        question_info = item.get('question_info')

        if not os.path.exists(file_path):
            os.makedirs(file_path)

        with open(os.path.join(file_path, file_name + '.md'), mode='a', encoding='utf-8') as f:
            f.write(question_info)
        print(question_info)
        return item


class WangxiaoImagePipeline(ImagesPipeline):
    def get_media_requests(self, item, info):
        # 下载 题中的图片到本地
        question_info = item.get('question_info')
        tree = etree.HTML(question_info)
        img_url_list = tree.xpath('//img/@src')
        for img_url in img_url_list:
            yield Request(
                url=img_url,
                meta={
                    'img_url': img_url,
                    'file_path': item.get('file_path'),
                    'file_name': item.get('file_name')
                }
            )

    def file_path(self, request, response=None, info=None, *, item=None):
        # 图片存放位置:文件名 同级的 '文件名--img' 文件夹里
        img_url = request.meta.get('img_url')
        file_path = request.meta.get('file_path')
        file_name = request.meta.get('file_name')
        return os.path.join(file_path, file_name + '--img', img_url.split('/')[-1])

    def item_completed(self, results, item, info):
        if results:  # 如果有图片下载. 才往里走
            for status, pic_info in results:
                if status:
                    # 将md文件中的 图片 由网络地址,替换成本地图片地址
                    # 网络地址 pic_info中有   pic_info.get('url')
                    http_url = pic_info.get('url')
                    # 本地地址 pic_info.get('path') ,再操作为 相对地址
                    local_url = os.path.join(*pic_info.get('path').split('\\')[-2:])
                    item['question_info'] = item.get('question_info').replace(http_url, local_url)
        return item

3.3 setings.py

BOT_NAME = 'wangxiao'

SPIDER_MODULES = ['wangxiao.spiders']
NEWSPIDER_MODULE = 'wangxiao.spiders'

USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'

ROBOTSTXT_OBEY = False

LOG_LEVEL = 'WARNING'


DOWNLOAD_DELAY = 3

COOKIES_ENABLED = False

DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
    'Cookie':'账号cookie'
}

ITEM_PIPELINES = {
   'wangxiao.pipelines.WangxiaoImagePipeline': 300,
   'wangxiao.pipelines.WangxiaoPipeline': 301,
}


# 下载图片的总路径
IMAGES_STORE = './'
posted @ 2024-05-08 23:11  Edmond辉仔  阅读(15)  评论(0编辑  收藏  举报