Scrapy入门(基于案例教学,由浅入深)

一、Scrapy的基本介绍,安装和项目创建

Scrapy 是用 Python 实现的一个为了爬取网站数据、提取结构性数据而编写的应用框架。

Scrapy 常应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。

通常我们可以很简单的通过 Scrapy 框架实现一个爬虫,抓取指定网站的内容或图片

关于这部分的内容我推荐大家看菜鸟教程的Scrapy入门教程,我觉得写得很好。里面包括了Scrapy的工作原理,安装以及项目创建。所以本节内容我就不赘述了。

补充
win+R 输入cmd打开控制台
在你想创建项目的地方,用window的资源管理器可以复制路径,然后在控制台中输入“cd 路径”,再换盘,即可到达你想要的文件路径下。
这时候再输入命令“scrapy startproject mySpider”即可在指定位置完成项目的创建

二、Scrapy shell和Xpath

在正式编写爬虫之前,我们需要先学习一下scrapy shell和xpath,这对我们后续爬取帮助很大。

2.1 Scrapy shell

scrapy shell提供一个交互式的环境,为我们提供了一个方便的纠错调试手段

scrapy 不是必须要学的,想学习scrapy shell的可以看这篇文章,或者csdn或百度上搜scrapy shell。

2.2 Xpath

xpath在scrapy爬虫中非常重要,它可以帮助我们快速的定位到我们要爬取的内容,再用scrapy中的函数就可以快速的爬取下来。

我推荐从菜鸟教程的Xpath教程上来学习xpath,或者csdn或百度上搜xpath就可找到很多介绍的文章了。

学习完xpath,我们就可以继续了。

三、Scrapy框架爬虫实战

前面都是比较无聊的知识学习,通过下面的实操,相信大家会对scrapy框架获得更深的理解。

3.1 项目介绍

我们接下来爬取好大夫在线中问诊的医患对话数据https://www.haodf.com/bingcheng/list.html

首先我们先来到主页面,如下
在这里插入图片描述
我们要按照左边绿色框中的咨询分类,找到右边经典问诊中的每一条数据,并进入,爬取其中的对话数据。

为了保证由浅入深的学习,我们编写两个spider来爬取数据。(等会会说spider是什么
其中:

第一个spider爬取主页左边绿色框中的咨询分类(例如:{内科: 心血管内科}, {外科: 神经外科}等),并把它保存为一个json格式的文件(json就是一种数据存储格式,像excel那样保存数据)

第一个爬虫这是一个很简单的爬虫,目的是让大家快速实践xpath和scrapy。学习了第一个spider,就已经掌握了基本的爬虫了。

---------------------------------分割线-------------------------------------

第二个爬虫就比较复杂了,由于好大夫问诊的医患对话数据页面做了一些反爬虫处理(需要登录,需要鼠标模拟点击,需要游览器解析response后才能爬取),所以我们还额外使用了selenium模块来使用携带cookie的游览器来自动登录,模拟点击以及解析得到正确的html。与此同时,第二个爬虫还涉及了多级页面的处理。

3.2 第一个爬虫(scrapy)

在这里插入图片描述
上图这是scrapy很经典的一幅图,创建完scapy项目后,我们项目文件夹下应为下图所示,它们其实是一一对应
在这里插入图片描述
我们在spiders文件夹下可以编写spider,然后在控制台中输入命令

scrapy crawl 爬虫名 -O data.json

就可以爬取数据并保存下来了(保存在data.json文件中)

所以编写一个最简单的爬虫你甚至只要会写spider就够了。

直接给代码

first_spider.py

import scrapy
import re
from myspider.items import MultilevelItem



class Multilevel_Page_Spider(scrapy.Spider):
    name = "MultilevelPage" # 爬虫名
    primary = '' # 咨询的一级分类,如内科
    secondary = '' # 咨询的二级分类,如心血管内科
    allowed_domains = ["www.haodf.com"] # 爬虫范围只能在这个域名下

    def start_requests(self):
        init_url = 'https://www.haodf.com/bingcheng/list.html' # 从主页面开始爬
        yield scrapy.Request(url=init_url, callback=self.parse)

    def parse(self, response, *args):
        item = MultilevelItem()
        for i in response.xpath('/html/body/div[3]/div[2]/div[1]/div//ul'):
            self.primary = i.xpath('li[1]/text()').get()
            for j in i.xpath('li[2]//span'):
                self.secondary = j.xpath('a/text()').get()
                item['primary_classification'] = self.primary
                item['secondary_classification'] = self.secondary
                item['classification_URL'] = j.xpath('a/@href').extract()[0]
                yield item

items.py 用法上网搜搜就知道了

import scrapy


class MultilevelItem(scrapy.Item):
    primary_classification = scrapy.Field(serializer=str)        # 一级分类的名字
    secondary_classification = scrapy.Field(serializer=str)      # 二级分类的名字
    classification_URL = scrapy.Field(serializer=str)            # 每个类别的url

3.3 第二个爬虫(scrapy+selenium)

Dynamic_page.py 这里面还对对话数据做了处理,把出现电话,视频的数据丢掉,把多句对话加标点拼接,同时还爬取病例信息和问诊建议等等。一些二级咨询含100页数据,也都会逐页爬取。

import scrapy
import re
from myspider.items import DynamicItem
import json


# with open("../../data.json", 'r', encoding="utf-8") as f:
#     content = json.load(f)
#
# for data in content:
#     primary = data['primary_classification']
#     print(type(primary))
#     secondary = data['secondary_classification']
#     print(type(secondary))
#     start_url = 'https:' + data['classification_URL']
#     print(primary, secondary, start_url)


class Dynamic_Page_Spider(scrapy.Spider):
    name = 'DynamicPage'
    # primary = ''
    # secondary = ''
    inter_URL = ''
    allowed_domains = ["www.haodf.com"]

    def start_requests(self):
        with open("../../data.json", 'r', encoding="utf-8") as f:
            content = json.load(f)
        for data in content:
            item = DynamicItem()
            item['PrimaryClassify'] = data['primary_classification']
            item['SecondaryClassify'] = data['secondary_classification']
            start_url = 'https:' + data['classification_URL']
            self.inter_URL = start_url
            yield scrapy.Request(url=start_url, callback=self.parse, meta = {'item': item})

    def parse(self, response):
        page_flag = True
        page = response.xpath('/html/body/div[3]/div[2]/div[2]/div/div[2]/div/div[2]/div//a')
        if len(page) <= 1:  # page页数不超过1页或无数据
            pass  
        else:  # page页数超过1
            page = page.reverse()
            page_num = page[2].xpath('text()').get()
            try:
                page_num = int(page_num)
                page_flag = True
            except ValueError:
                page_flag = False
            if page_flag is True:
                for i in range(page_num):
                    dynamic_pages_URL = self.inter_URL + "?p={}".format(i + 1)
                    yield scrapy.Request(url=dynamic_pages_URL, callback=self.parse_pages, meta = {'item': response.meta['item']})
            else:
                pass

    def parse_pages(self, response):
        dialogue_pages = response.xpath('/html/body/div[3]/div[2]/div[2]/div/div[2]/div/ul//li')
        if len(dialogue_pages) < 1:
            pass
        else:
            source_item = response.meta['item']
            primary = source_item['PrimaryClassify']
            secondary = source_item['SecondaryClassify']
            for pageDialogue in dialogue_pages:
                item = DynamicItem()
                item['PrimaryClassify'] = primary
                item['SecondaryClassify'] = secondary
                dynamic_dialogue_page_URL = pageDialogue.xpath('span[1]/a/@href').get()
                id_number = re.findall(r'\d+', dynamic_dialogue_page_URL)
                if len(id_number) == 0:
                    pass
                else:
                    item['id'] = id_number[0]
                    yield scrapy.Request(url=dynamic_dialogue_page_URL, callback=self.parse_dynamic_pages, meta = {'item': item})

    def parse_dynamic_pages(self, response):
        # 在下载中间件中已经登陆并展开整个页面并返回response
        # 先判断这个数据需不需要爬取
        category = response.xpath('/html/body/header[2]/section/h1/span/text()').get()
        outcome = re.search("电话|视频|电话问诊|", category)
        if outcome is None:
            # 可以继续操作
            # 首先爬取对话数据
            dialogue_data = []
            # 上一个说话人及其数据
            last_avatar = False           # True为患者,False为医生
            last_avatar_data = ''
            # 判断是否应该yield item
            error_flag = False
            # 找到存放对话数据的div
            dialogues = response.xpath('/html/body/main/section/section[2]/div[2]')
            dialogues = dialogues.xpath('div//div[@class="msg-item item-left "]|div//div[@class="msg-item item-right "]')
            # 接着对医生和病人的对话进行提取,排除小牛医助的对话
            for dialogue_info in dialogues:
                avatar = dialogue_info.xpath('div[2]/p[1]/span/text()').get()
                current_dialogue_data = dialogue_info.xpath('div[2]/p[2]/span[@class="content-him content-text content-him-patient"]/text()|div[2]/p[2]/span[@class="content-him content-text "]/text()|div[2]/p/span[@class="content-him content-privacy content-him-patient"]/text()|div[2]/p[2]/span[@class="content-audio-warpper"]/span[1]/text()|div[2]/p/span[@class="content-sysnotice"]/text()').get()
                if current_dialogue_data is None:
                    error_flag = True
                    break
                elif re.search("图片资料,仅主诊医生和患者本人可见|隐私内容,仅主诊医生和患者本人可见|我发起了电话沟通,请注意接听", current_dialogue_data) is not None:
                    error_flag = True
                    break
                else:
                    if avatar is None:     # 无内容的处理方法以及中间灰色字的处理
                        pass
                    else:
                        if avatar == '小牛医助':
                            pass
                        elif avatar == '患者':
                            if last_avatar is False:                       # 上一个讲话的是医生
                                last_avatar = True                         # 改为患者
                                last_avatar_data = '医生:' + last_avatar_data
                                dialogue_data.append(last_avatar_data)
                                last_avatar_data = current_dialogue_data
                            else:
                                last_avatar = True
                                last_avatar_data = last_avatar_data + current_dialogue_data   # 病人说的话多句拼接
                        else:
                            if last_avatar is False:                       # 上一个讲话的是医生
                                last_avatar = False
                                last_avatar_data = last_avatar_data + current_dialogue_data   # 医生说的话多句拼接
                            else:                                          # 上一个讲话的是患者
                                last_avatar = False                        # 改为医生
                                last_avatar_data = '患者:' + last_avatar_data
                                dialogue_data.append(last_avatar_data)
                                last_avatar_data = current_dialogue_data                        # 刷新对话数据缓存

            if error_flag is False:
                PatientInfo_dic = {}
                patient_info_title = response.xpath('/html/body/main/section/p//span[@class="info3-title "]|/html/body/main/section/p/span[@class="info3-title info3-row"]')
                patient_info_value = response.xpath('/html/body/main/section/p//span[@class="info3-value  newline"]|/html/body/main/section/p//span[@class="info3-value info3-point newline"]')
                already_found_flag = False
                for i in range(len(patient_info_title)):
                    if patient_info_title[i].xpath('text()').get() == '既往病史:':
                        already_found_flag = True
                        PatientInfo_dic['既往病史:'] = ','.join(response.xpath('/html/body/main/section/p//span[@class="info3-value info3-point"]/text()').extract())
                    else:
                        if already_found_flag is False:
                            PatientInfo_dic[patient_info_title[i].xpath('text()').get()] = patient_info_value[i].xpath('text()').get()
                        else:
                            PatientInfo_dic[patient_info_title[i].xpath('text()').get()] = patient_info_value[i-1].xpath('text()').get()

                source_item = response.meta['item']
                primary = source_item['PrimaryClassify']
                secondary = source_item['SecondaryClassify']
                id_ = source_item['id']
                item = DynamicItem()
                item['id'] = id_
                item['PrimaryClassify'] = primary
                item['SecondaryClassify'] = secondary
                item['PatientInfo'] = PatientInfo_dic
                item['DiagnoseAdvice'] = {"病历概要": response.xpath('/html/body/main/section/section[1]/div[2]/p/span[2]/text()').get(), "处置建议": response.xpath('/html/body/main/section/section[1]/div[3]/p/span[2]/text()').get()}
                item['Doc2PatDialogue'] = dialogue_data
                yield item
        else:
            pass  # "无法继续操作"

items.py
在上一节的items.py中加上一下代码

class DynamicItem(scrapy.Item):
    id = scrapy.Field(serializer=str)                            # id
    PrimaryClassify = scrapy.Field(serializer=str)               # 一级分类的名字
    SecondaryClassify = scrapy.Field(serializer=str)             # 二级分类的名字
    PatientInfo = scrapy.Field(serializer=dict)                  # 病例信息
    DiagnoseAdvice = scrapy.Field(serializer=dict)               # 诊断建议
    Doc2PatDialogue = scrapy.Field(serializer=list)              # 医患对话数据
    interURL = scrapy.Field(serializer=str)                      # 存储中间URL

middlewares.py

from scrapy import signals
import scrapy
import re
from selenium import webdriver
import selenium
from selenium.webdriver.common.by import By
from time import sleep
import json
from scrapy.http import HtmlResponse    


# useful for handling different item types with a single interface
# from itemadapter import is_item, ItemAdapter

class SeleniumMiddleware(object):
    @classmethod
    def process_request(cls, request, spider):
        if spider.name == 'DynamicPage' or spider.name == 'try':
            if re.search(r'\d{8}', request.url) is not None:
                user_data_dir = r'C:\Users\25439\AppData\Local\Google\Chrome\User Data'
                user_option = webdriver.ChromeOptions()
                user_option.add_argument('--headless')                      # 不出现游览器
                # user_option.add_experimental_option("detach", True)       # 不维持游览器
                user_option.add_argument(f'--user-data-dir={user_data_dir}')
                driver = webdriver.Chrome(options=user_option)
                driver.get(request.url)

                # 自动展开页面
                for i in range(16):
                    try:
                        button = driver.find_element(By.XPATH, '/html/body/main/section/section[@class="msgboard js-msgboard"]/div[3]/div[2]')
                        button.click()
                        sleep(0.5)
                    except selenium.common.exceptions.ElementNotInteractableException:
                        break
                sleep(0.5)

                html = driver.page_source
                driver.quit()

                # 构建response, 将它发送给spider引擎

                return HtmlResponse(url=request.url, body=html, request=request, encoding='utf-8')

开摆了,直接放代码。最近挺忙,以后有时间再补充。第二个爬虫去了解一下下载中间件(download middleware)就明白了。引入selenium是为了爬取动态加载的页面。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

A DOG BY MY SIDE

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值