继续创作,加快生长!这是我参与「日新计划 10 月更文挑战」的第23天,点击查看活动概况


Spider实战

本文将解说怎么运用scrapy框架完结北京公交信息的获取。

方针网址为beijing.8684.cn/。

在前文的爬虫实战中,现已解说了怎么运用requests和bs4爬取公交站点的信息,感兴趣的话能够先阅览一下「Python」爬虫实战系列-北京公交线路信息爬取(requests+bs4) – (),在回来阅览这篇文章。

关于爬虫系列文章,这儿浅浅的罗列一下,欢迎阅览‍️‍️‍️:

「Python」爬虫-1.入门知识简介 – ()

「Python」爬虫-2.xpath解析和cookie,session – ()

「Python」爬虫-3.防盗链处理 – ()

「Python」爬虫-4.selenium的运用 – ()

「Python」爬虫-5.m3u8(视频)文件的处理 – ()

「Python」爬虫-6.爬虫功率的进步 – ()

「Python」爬虫-7.验证码的识别 – ()

「Python」爬虫-8.断点调试-网易云谈论爬取 – ()


1.相关技术介绍

scrapy是一个为了爬取网站数据,提取结构性数据而编写的运用框架,能够运用在包括数据挖掘,信息处理或存储历史数据等一列的程序中。

scrapy的操作流程如下:

1.挑选网站->2.创立一个scrapy项目->3.创立一个spider->4.界说item ->5.编写spider->6.提取item->7.存取爬取的数据->8.履行项目。

下面将顺次按照此流程来解说scrapy框架的运用:

1.挑选网站

本文选取的方针url为beijing.8684.cn/,经过之前的实验实战文章,信任读者对该网站现已有了开始的知道。

这儿相同咱们所需求爬取的信息为公交线路称号、公交的运营规模、运转时刻、参阅票价、公交所属的公司以及服务热线、公交来回线路的途径站点。

2.创立一个scrapy项目

在开始爬取之前,需求创立一个新的scrapy项目,切换到该项目途径的目录下,并履行scrapy startproject work,

(其间work为项意图姓名,能够自拟,叫啥都可

观察work目录下的结构如下所示:

└─work
    │  scrapy.cfg  # 项目配置文件
    │
    └─work   # 该项意图python模块
        │  items.py  # 界说爬取的数据结构
        │  middlewares.py  # 界说爬取时的中心件
        │  pipelines.py  # 数据管道,将数据存入本地文件或存入数据库
        │  settings.py  # 项意图设置文件
        │  __init__.py
        │
        └─spiders  # 放置spider代码的目录
                __init__.py

3.创立一个spider。

work目录下,运用genspider句子,创立一个spider。格局如下:

scrapy genspider spider_name url

「Python」爬虫-9.Scrapy框架的初识-公交信息爬取

spider_name为自己程序的姓名,自己为自己程序取名,不过分吧?

url为自己爬取的方针网址,本文为beijing.8684.cn/。

4.界说item

item是保存爬取到的数据容器,运用办法和python字典相似,并供给了额定保护机制来防止拼写过错导致的未界说字段过错。

首要,依据从方针网站获取到的数据对item 进行建模,并在item中界说相应的字段,编辑work目录中的items.py文件。比方:

import scrapy
class MyItem(scrapy.Item):
    title = scrapy.Field()
    desc = scrapy.Field()

经过界说item,能够很方面的运用scrapy的其他办法,而这些办法需求知道item 的界说。

5.编写spider

spider是用户编写用于从单个网站爬取数据的类,其间包括了一个用于下载的初始url,以及怎么跟进网页中的链接和分析页面中的内容,提取生成item的办法。

为了创立一个spider,有必要继承scrapy.Spider类,而且有必要界说以下三个特点:

  • name:用于区别Spider,该姓名有必要仅有,不能够为不同的spider设定相同的姓名
  • start_urls: 包括了Spider在启动时进行爬取的url列表,因此,第一个被获取到的页面将是其间之一,后续的url则从初始的url获取到的数据中获取。
  • parse(): 是spider的一个办法,被调用时,每个初始url完结下载后生成的Response目标将会作为仅有的参数传递给该函数,该办法负责解析回来的数据(response data),提取数据(生成item)以及生成需求进一步处理的url的Request目标

6.提取item

从网页中提取数据的办法有许多,正如前面实验所用到的requests或许selenium等,scrapy运用了一种依据xpath和css表达式机制:scrapy selectors,这儿给出常用xpath表达式对应的含义:

/html/head/title:挑选html文档中<head>标签内的<title>元素。
/html/head/title/text():挑选上面提到的<title>元素的文字。
//td:挑选一切的<td>元素
//div[@class="mine"]:挑选一切具有class="mine"特点的div元素。

为了配合xpath,scrapy除了供给selector之外,还供给了其他办法来防止每次从response中提取数据时生成selector的费事。

selector有四个根本的办法;

  • xpath():传入xpath表达式,回来该表示对应一切节点的selector list列表
  • css():传入css表达式,回来该表达式对应的一切节点的selector list列表
  • extract():序列化该节点为unicode字符串并回来list列表
  • re():依据传入的正则表达式对数据进行提取,回来unicode字符串list列表

7.存取爬取的数据

scrapy还集成了存储数据的办法,运用指令如下:

scrapy crawl work -o items.json

该指令将选用json格局对爬取的数据进行序列化,并生成对应的items.json文件。对于小规模的项目,这种存储方式较为灵敏,假如对爬取到的item做更多更为杂乱的操作就需求编写pipelines.py

8.履行项目

到当前项意图根目录,履行以下指令即可启动spider:

scrapy crawl work

scrapy为spider的start_urls特点中的每一个url创立了scrapy.Request目标,并将parse办法作为回调函数callback赋值给了Request.

Request目标经过调度,履行生成scrapy.HTTP.Response目标并送回给spider.parse()办法。

cd 到 /work/work/目录下,履行scrapy crawl BusCrawl也能够启动spider。


2.全体爬取进程分析

2.1 settings.py

依据上述相关技术介绍,新建一个work项目,并履行

genspider BusCrawl https://beijing.8684.cn/

经过genspider之后,就会发现项意图BusCrawl.py中主动为咱们现已增加了一些代码:

import scrapy
class BuscrawlSpider(scrapy.Spider):
    name = 'BusCrawl'
    allowed_domains = ['beijing.8684.cn']
    start_urls = ['http://beijing.8684.cn']
    def __init__(self, name=None, **kwargs):
        super().__init__(name=None, **kwargs)
    def start_requests(self):
        pass
    def parse(self, response):
        pass

这儿由于咱们的公交线路衔接需求拼接list?,所以在本文中start_urls直接修正为字符串的方式。

settings.py默许内容如下:

BOT_NAME = 'work'
SPIDER_MODULES = ['work.spiders']
NEWSPIDER_MODULE = 'work.spiders'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = 'work (+http://www.yourdomain.com)'
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
# LOG_LEVEL = 'WARNING'

items.py默许内容如下:

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
# useful for handling different item types with a single interface
from itemadapter import ItemAdapter
class BusCrawlPipeline:
    def process_item(self, item, spider):
        return item

然后修正settings.pyrobots协议为False以及粘贴自己的headers

robots协议本来是爬虫应该遵循的,可是假如遵循的话,信任大部分信息都设置为不行爬取,那么,就爬不到啥东西了

本文的详细设置为如下:

ROBOTSTXT_OBEY = False
DEFAULT_REQUEST_HEADERS = {
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'Accept-Language': 'en',
  "User-Agent": "xxx"
}

毕竟大部分的网站信息要是遵循robots协议如同都爬取不了。 user-agent直接自己去仿制就好了,办法这儿不再赘述。

middlewares.py本文不触及,这儿省掉介绍。

2.2 BusCrawl.py

接下来需求写BusCrawl.py,能够看到scrapy现已为咱们写好了部分函数,咱们需求依据自己的需求进一步完善相关的代码文件。

2.2.1 start_requests(self)

先将start_urls切换为字符串的方式,由于咱们爬取的详细信息url需求进一步结构。依据之前的分析,以每个数字或许字母开头的页面是直接在https://beijing.8684.cn/后加上list?。所以start_requests()函数如下:

def start_requests(self):
    for page in range(3):
        url = '{url}/list{page}'.format(url=self.start_urls, page=page + 1)
        yield FormRequest(url, callback=self.parse_index)

其间yield函数就是函数履行到这就结束了,并调用了FormRequest()函数,这个需求引进from scrapy import FormRequest,然后将对页面进行详细分析的函数名传递给callback,这样就能够调用parse_index函数了。(parse_index其实就是解析函数

留意这儿的parse_index姓名不是固定的,这儿仅仅依据callback来调用详细的函数。

运转程序之后是能够看到在控制台输出了咱们的list链接信息。

「Python」爬虫-9.Scrapy框架的初识-公交信息爬取

2.2.2 parse_index(self, response)

然后依据https://beijing.8684.cn/list?规划perse_index提取到每条详细线路的href中的概况页面。parse_index函数规划如下:

def parse_index(self, response):
    hrefs = response.xpath('//div[@class="list clearfix"]/a/@href').extract()
    for href in hrefs:
        detail_url = urljoin(self.start_urls, href)
        yield Request(detail_url, callback=self.parse_detail)

这儿需求留意:提取每个list里边的线路详细链接的时分直接仿制的xpath代码用不了,需求依据class名才干提取到,经过extract()即可提取到href的信息。

response.xpath('//div[@class="list clearfix"]/a/@href').extract()

extract():这个办法回来的是一个数组list

extract_first():这个办法回来的是一个string字符串,是list数组里边的第一个字符串。

yield Request(...)之后就能够看到概况界面的链接了,说明咱们的函数写的十分正确!

「Python」爬虫-9.Scrapy框架的初识-公交信息爬取

运用yield之后,会默许在控制台打印信息,而不需求咱们自己手动print打印了。

2.2.3 parse_detail(self, response)

提取到每条公交线路的概况页面的url之后,就能够开始对概况页面进行信息提取了。这儿 的分析办法和之前的bs4或许xpath解析差不多,稍微改一改就能够直接拿过来用了。

这儿遇到的问题和之前的相似,在提取公交的站点信息的时分,仍然会呈现中心路线呈现了终点站或开始站的状况,这儿的解决办法和之前相似->遍历中心的站点,然后删去开始站点。

未处理前爬取的站点信息图如下:

「Python」爬虫-9.Scrapy框架的初识-公交信息爬取

详细的解析页面的函数规划如下:

def parse_detail(self, response):
    title = response.xpath('//h1[@class="title"]/span/text()').extract_first()  # 线路称号
    category = response.xpath('//a[@class="category"]/text()').extract_first()  # 线路类别
    time = response.xpath('//ul[@class="bus-desc"]/li[1]/text()').extract_first()  # 开车时刻
    price = response.xpath('//ul[@class="bus-desc"]/li[2]/text()').extract_first()  # 参阅票价
    company = response.xpath('//ul[@class="bus-desc"]/li[3]/a/text()').extract()  # 所属公司
    trip = response.xpath('//div[@class="trip"]/text()').extract()  # 开车方向
    path_go, path_back = None, None
    begin_station, end_station = trip[0].split('—')
    if len(trip) > 1:
        path_go = response.xpath('//div[@class="service-area"]/div[2]/ol/li/a/text()').extract()
        path_back = response.xpath('//div[@class="service-area"]/div[4]/ol/li/a/text()').extract()
        go_list = [begin_station] + [station for station in path_go[1:-1] if station != end_station] + [end_station]
        back_list = [end_station] + [station for station in path_back[1:-1] if station != begin_station] + [
            begin_station]
        path_go = {trip[0]: '->'.join(go_list)}
        path_back = {trip[1]: '->'.join(back_list)}
    else:
        path_go = response.xpath('//div[@class="bus-lzlist mb15"]/ol/li/a/text()').extract()
        go_list = [begin_station] + [station for station in path_go[1:-1] if station != end_station] + [end_station]
        path_go = {trip[0]: '->'.join(go_list)}
    item = {'title': title,
            'category': category,
            'time': time,
            'price': price,
            'company': company,
            'trip': trip,
            'path_go': path_go,
            'path_back': path_back,
            }
    bus_info = WorkItem()  # 实例化item
    for field in bus_info.fields:
        bus_info[field] = eval(field)
    yield bus_info

需求留意的是,并不是一切的公交线路的开始站点和终点站都不相同。在处理信息的进程中呈现了开始站和终点站相同的状况,如下图的五间楼-五间楼

这儿需求对其进行进一步的处理,这儿直接增加的if判别,详细完成见上面的代码。。

「Python」爬虫-9.Scrapy框架的初识-公交信息爬取

bus_info = WorkItem()  # 实例化item
    for field in bus_info.fields:
        bus_info[field] = eval(field)
    yield bus_info

这一段代码能够笼统为一个模板,将自己需求提取的信息放到bus_info里边即可。然后yield bus_info。前面也提到过item的运用办法和字典相似。

2.3 items.py

接下来写`items.py:

items.py中的信息称号需求和parse_detail中的实例化的item名对应起来。

本文详细函数规划如下:

class WorkItem(scrapy.Item):
    title = scrapy.Field()
    category = scrapy.Field()
    time = scrapy.Field()
    price = scrapy.Field()
    company = scrapy.Field()
    trip = scrapy.Field()
    path_go = scrapy.Field()
    path_back = scrapy.Field()

tips:

scrapy.Field(),相似于字典。在spider中实例化,即parse_detail()bus_info = WorkItem()实例化,然后取值的方式相似于字典,即代码中的bus_info[field] = eval(field)

2.4 pipelines.py

接下来需求规划写入到数据库的pipelines.py。本文以写入本地的MongoDB数据库为例,MongoDB数据库的版本为6.x。

首要在settings.py中增加

# 启用pipeline
ITEM_PIPELINES = {
   'work.pipelines.MyMongoPipeline': 300,
}
# 数据库相关设置
DB_HOST = 'localhost'
DB_NAME = 'bus_spider'
DB_COLLECTION_NAME = 'ipad'

ITEM_PIPELINES中的300是分配给每个类的整型值,确定了他们运转的次序,item按数字从低到高的次序,经过pipeline,通常将这些数字界说在0-1000规模内(0-1000随意设置,数值越低,组件的优先级越高)。

item pipiline组件是一个独立的Python类,其间process_item()办法有必要完成,item pipeline一般运用在:

  • 验证爬取的数据(查看item包括某些字段,比方说name字段)
  • 查重(并丢弃)
  • 将爬取结果保存到文件或许数据库中

这儿规划的pipelines.py是将数据存储到本地的MongoDB中,py衔接MongoDB就不必详细说了,详细规划如下:

from pymongo import MongoClient
from .settings import *
class MyMongoPipeline:
    def __init__(self):
        self.client = MongoClient(host=DB_HOST)
        self.db = self.client.get_database(DB_NAME)
        self.collection = self.db[DB_COLLECTION_NAME]
    def process_item(self, item, spider):
        # 这个办法有必要回来一个 Item 目标,被丢弃的item将不会被之后的pipeline组件所处理
        self.collection.insert(dict(item))
        return item
    def close_spider(self, spider):
        self.client.close()

假如想将数据存为json的格局,那么能够参阅以下代码:

import json
class ItcastJsonPipeline(object):
    def __init__(self):
        self.file = open('xxx.json', 'wb')
    def process_item(self, item, spider):
        content = json.dumps(dict(item), ensure_ascii=False) + "\n"  # 这儿先对item进行了转型-变为dict
        self.file.write(content)
        return item
    def close_spider(self, spider):
        self.file.close()

3.代码部分

3.1 运转界面

爬取数据成功界面

「Python」爬虫-9.Scrapy框架的初识-公交信息爬取

mongodb数据查询界面

「Python」爬虫-9.Scrapy框架的初识-公交信息爬取

3.2 详细代码

# Buscrawl.py
import scrapy
from scrapy import FormRequest, Request
import logging
from urllib.parse import urljoin
from ..items import WorkItem
logging.getLogger("filelock").setLevel(logging.INFO)
class BuscrawlSpider(scrapy.Spider):
    name = 'BusCrawl'
    allowed_domains = ['beijing.8684.cn']
    start_urls = 'http://beijing.8684.cn'
    def __init__(self, name=None, **kwargs):
        super().__init__(name=None, **kwargs)
    def start_requests(self):
        for page in range(3):
            url = '{url}/list{page}'.format(url=self.start_urls, page=page + 1)
            yield FormRequest(url, callback=self.parse_index)
    def parse_index(self, response):
        hrefs = response.xpath('//div[@class="list clearfix"]/a/@href').extract()
        for href in hrefs:
            detail_url = urljoin(self.start_urls, href)
            yield Request(detail_url, callback=self.parse_detail)
    def parse_detail(self, response):
        title = response.xpath('//h1[@class="title"]/span/text()').extract_first()  # 线路称号
        category = response.xpath('//a[@class="category"]/text()').extract_first()  # 线路类别
        time = response.xpath('//ul[@class="bus-desc"]/li[1]/text()').extract_first()  # 开车时刻
        price = response.xpath('//ul[@class="bus-desc"]/li[2]/text()').extract_first()  # 参阅票价
        company = response.xpath('//ul[@class="bus-desc"]/li[3]/a/text()').extract()  # 所属公司
        trip = response.xpath('//div[@class="trip"]/text()').extract()  # 开车方向
        path_go, path_back = None, None
        begin_station, end_station = trip[0].split('—')
        if len(trip) > 1:
            path_go = response.xpath('//div[@class="service-area"]/div[2]/ol/li/a/text()').extract()
            path_back = response.xpath('//div[@class="service-area"]/div[4]/ol/li/a/text()').extract()
            go_list = [begin_station] + [station for station in path_go[1:-1] if station != end_station] + [end_station]
            back_list = [end_station] + [station for station in path_back[1:-1] if station != begin_station] + [
                begin_station]
            path_go = {trip[0]: '->'.join(go_list)}
            path_back = {trip[1]: '->'.join(back_list)}
        else:
            path_go = response.xpath('//div[@class="bus-lzlist mb15"]/ol/li/a/text()').extract()
            go_list = [begin_station] + [station for station in path_go[1:-1] if station != end_station] + [end_station]
            path_go = {trip[0]: '->'.join(go_list)}
        item = {'title': title,
                'category': category,
                'time': time,
                'price': price,
                'company': company,
                'trip': trip,
                'path_go': path_go,
                'path_back': path_back,
                }
        bus_info = WorkItem()
        for field in bus_info.fields:
            bus_info[field] = eval(field)
        yield bus_info
# items.py
import scrapy
class WorkItem(scrapy.Item):
    title = scrapy.Field()
    category = scrapy.Field()
    time = scrapy.Field()
    price = scrapy.Field()
    company = scrapy.Field()
    trip = scrapy.Field()
    path_go = scrapy.Field()
    path_back = scrapy.Field()
# pipelines.py
from pymongo import MongoClient
from .settings import *
class MyMongoPipeline:
    def __init__(self):
        self.client = MongoClient(host=DB_HOST)
        self.db = self.client.get_database(DB_NAME)
        self.collection = self.db[DB_COLLECTION_NAME]
    def process_item(self, item, spider):
        self.collection.insert(dict(item))
        return item
    def close_spider(self, spider):
        self.client.close()
# settings.py
BOT_NAME = 'work'
SPIDER_MODULES = ['work.spiders']
NEWSPIDER_MODULE = 'work.spiders'
ROBOTSTXT_OBEY = False
DEFAULT_REQUEST_HEADERS = {
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'Accept-Language': 'en',
  "User-Agent": "xxx"
}
ITEM_PIPELINES = {
   'work.pipelines.MyMongoPipeline': 300,
}
DB_HOST = 'localhost'
DB_NAME = 'bus_spider'
DB_COLLECTION_NAME = 'ipad'

4.可能呈现的报错:

1.找不到WorkPipeline

原因:自己修正了pipelines.py里边的类名,可是没有修正settings.py里边的pipelines称号。

解决办法settings.py中的ITEM_PIPELINES里边的.MyMongoPipeline一定要和pipelines.py里边的类名对上。

ITEM_PIPELINES = {
   'work.pipelines.MyMongoPipeline': 300,
}

2. [filelock] DEBUG: Attempting to acquire lock 4469884432 on …… __a22fb8__tldextract-3.3.1/

原因:其实不是报错,假如不想在控制台的看到的话,直接设置一下logging的打印等级即可。

解决办法

import logging
logging.getLogger("filelock").setLevel(logging.INFO)

参阅:github.com/scrapy/scra…

3..raise KeyError(f”{self.class.name} does not support field: {key}”)

原因:KeyError: ‘WorkItem does not support field: _id’

解决办法:将item转为dict类型即可,即在刺进数据时运用:

self.collection.insert(dict(item))

参阅链接

1.blog.csdn.net/m0_59483606…

2.blog.csdn.net/songrenqing…