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是为了爬取动态加载的页面。