首页 > 其他分享 >案例02--scrapy综合练习--中大网校

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

时间:2024-05-08 23:23:56浏览次数:15  
标签:02 title -- list get question 网校 item file

一 抓取需求

# 抓取需求

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 = './'

标签:02,title,--,list,get,question,网校,item,file
From: https://www.cnblogs.com/Edmondhui/p/18181133

相关文章

  • Mysql-Mvcc原理
    0.背景在mysql的并发访问中,有几个典型的并发场景:读-读:无需处理,都是读取,不会对数据有影响。写-写:由于都涉及到数据的修改,不可能乱改,所以没有较好的方式来处理,一般都得加锁。读-写:读写场景,加锁当然ok。不过读操作是很频繁的,一但写数据就不让读取了,这种情况是让人很难受的......
  • 实时股票数据API接口websocket接入方法
    一、使用websocket的协议提升传输速度实时金融股票API接口对于投资者和交易员来说至关重要。通过使用WebSocket接入方法,可以轻松获取实时金融股票API接口的数据并及时做出决策。WebSocket是一种高效的双向通信协议,它允许数据的实时推送,避免了不断的轮询请求。这种接入方法具有多......
  • 2024年5月8日
    今天学习了web页面顶部栏的使用和连接的使用和跳转,对web页面进行了美化<template><divclass="common-layout"><el-container><el-headerclass="el-header"><imgsrc="../photos/logo.png"width="200&q......
  • movie
    importrequestsimportpymongofromqueueimportQueuefromlxmlimportetreeimportthreadingdefhandle_request(url):"""处理request函数:paramurl::return:response.text"""#自定义请求头headers={"......
  • [题解]CF1907G Lights
    CF1907GLights我们可以把灯抽象成节点,而开关抽象成无向边(重边算作\(1\)条)。显然每个连通块要么是一棵树,要么是一棵基环树。对于基环树,我们把它看做若干棵树处理,最后我们再考虑如何处理环。如下图,这是一棵树,黄色的点表示亮灯。我们选定任意一条边,可以改变子节点和父节点的状......
  • 路径规划-PRM算法(1)
    probabilisticroadmap(PRM)算法是一类用于机器人路径规划的算法,最早在[1]中被提出,属于随机规划器的一种,其数据的主要形式为无向图(另一种RRT基于树)。[^2]将PRM算法分成了两个阶段:learning阶段和query阶段。其中learning阶段主要在configuration空间(机械臂的话是\(C\)......
  • [笔记]拓扑排序
    对于一个有向无环图(DAG)的顶点按顺序排成一个序列的过程,就是拓扑排序(TopologicalSort)。具体来说,这个序列必须满足:每个顶点正好出现\(1\)次。如果图上存在一条\(A\toB\)的路径,那么\(A\)一定在\(B\)之前。注意:拓扑排序结果可能不唯一。具体做法就是每次在图中寻找\(1\)个入......
  • 236. 二叉树的最近公共祖先(leetcode)
    https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/description/要点是后序遍历,这样就可以从树底开始搜索,寻找最近公共祖先classSolution{public://返回的是最近公共祖先,若返回null则说明没有TreeNode*lowestCommonAncestor(TreeNode*r......
  • 麒麟 V10 一键安装 Oracle 11GR2(231017)单机版 2
    https://www.modb.pro/db/1762008192972820480安装准备1、安装好操作系统,建议安装图形化2、配置好网络3、挂载本地ISO镜像源4、上传必须软件安装包(安装基础包,补丁包:33991024、35574075、35685663、6880880)5、上传一键安装脚本:OracleShellInstall✨偷懒可以直接下载本......
  • 2024-05-08 js 常见案例
    1.表单验证functionvalidateForm(){varname=document.forms["myForm"]["name"].value;if(name==""){alert("Namemustbefilledout");returnfalse;}//更多的验证.........