http://home.hddly.cn:50090/media/chrome_IWlr5MZVSJ.mp4
http://home.hddly.cn:50090/media/chrome_oOCFZ6Y8IT.mp4
http://home.hddly.cn:50090/media/TiSsnUj7Xt.mp4
http://home.hddly.cn:50090/media/yC4w8sHV4d.mp4
下面我们使用 scrapy
写一个爬虫脚本,爬取蓝桥课程页面所有课程名称、简介、类型和学习人数信息,并保存为 JSON
文本。课程页面的地址是 https://www.lanqiao.cn/courses/
。
在 /home/shiyanlou/Code
下新建 shiyanlou_courses_spider.py
文件,写入 Scrapy 爬虫的基本结构:
xxxxxxxxxx
291import scrapy
2
3
4class ShiyanlouCoursesSpider(scrapy.Spider):
5 """
6 使用 scrapy 爬取页面数据需要编写一个爬虫类,该爬虫类要继承 scrapy.Spider 类
7 在爬虫类中定义要请求的网站和链接、如何从返回的网页提取数据等等
8 在 scrapy 项目中可能会有多个爬虫,name 属性用于标识每个爬虫,各个爬虫类的 name 值不能相同
9 """
10 name = 'shiyanlou-courses'
11
12 # 注意此方法的方法名字是固定的,不可更改
13 def start_requests(self):
14 """
15 此方法需要返回一个可迭代对象,迭代的元素是 scrapy.Request 对象
16 可迭代对象可以是一个列表或者迭代器,这样 scrapy 就知道有哪些网页需要爬取了
17 scrapy.Request 接受一个 url 参数和一个 callback 参数
18 url 指明要爬取的网页
19 callback 是一个回调函数,用于处理返回的网页,它的值通常是一个提取数据的 parse 方法
20 """
21
22 # 注意此方法的方法名字也是固定的,不可更改
23 def parse(self, response):
24 """
25 这个方法作为 scrapy.Request 的 callback ,在里面编写提取数据的代码
26 scrapy 中的下载器会下载 start_reqeusts 中定义的每个 Request
27 并且将结果封装为一个 response 对象传入这个方法
28 """
29 pass
分析蓝桥的课程页面可以看出,每页有 4 x 5 行共 20 个课程卡片,我们需要从中提取 20 条数据,爬取前 3 页,共计 60 条数据,点击页面底部的「下一页」按钮进入下一页,将浏览器地址栏中的地址复制出来:
这样就可以写出 start_requests
方法:
xxxxxxxxxx
81def start_requests(self):
2 # 课程列表页面 URL ,注意此列表中的地址可能有变动,需手动打开页面复制最新地址
3 url_list = ['https://www.lanqiao.cn/courses/',
4 'https://www.lanqiao.cn/courses/?page=2',
5 'https://www.lanqiao.cn/courses/?page=3']
6 # 返回一个生成器,生成 Request 对象,生成器是可迭代对象
7 for url in url_list:
8 yield scrapy.Request(url=url, callback=self.parse)
Scrapy 内部的下载器会下载每个 Request
,然后将结果封装为 response
对象传入 parse
方法,这个对象和前面 scrapy shell 练习中的对象是一样的,也就是说你可以用 response.css()
或者 response.xpath()
来提取数据了。
通过分析蓝桥课程页面的文档结构,以《Linux 入门基础(新版)》课程为例,我们需要提取的数据主要包含在下面的 div 里面:
根据这个 div 可以用提取器写出 parse 方法:
xxxxxxxxxx
161def parse(self, response):
2 # 遍历每个课程的 div.col-md-3
3 for course in response.css('div.col-md-3'):
4 # 使用 css 语法对每个 course 提取数据
5 yield {
6 # 课程名称,注意这里使用 strip 方法去掉字符串前后的空白字符
7 # 所谓空白字符,指的是空格、换行符、制表符
8 # 下面获取 name 的写法还可以省略 h6 的类属性,思考一下为什么可以省略
9 'name': course.css('h6.course-name::text').extract_first().strip(),
10 # 课程描述
11 'description': course.css('div.course-description::text').extract_first().strip(),
12 # 课程类型
13 'type': course.css('span.course-type::text').extract_first('免费').strip(),
14 # 学生人数
15 'students': course.css('span.students-count span::text').extract_first()
16 }
xxxxxxxxxx
411import scrapy
2
3
4class ShiyanlouCoursesSpider(scrapy.Spider):
5"""
6使用 scrapy 爬取页面数据需要编写一个爬虫类,该爬虫类要继承 scrapy.Spider 类
7在爬虫类中定义要请求的网站和链接、如何从返回的网页提取数据等等
8在 scrapy 项目中可能会有多个爬虫,name 属性用于标识每个爬虫,各个爬虫类的 name 值不能相同
9"""
10name = 'shiyanlou-courses'
11
12# 注意此方法的方法名字是固定的,不可更改
13def start_requests(self):
14# 课程列表页面 URL ,注意此列表中的地址可能有变动,需手动打开页面复制最新地址
15url_list = ['https://www.lanqiao.cn/courses/',
16'https://www.lanqiao.cn/courses/?page=2',
17'https://www.lanqiao.cn/courses/?page=3']
18# 返回一个生成器,生成 Request 对象,生成器是可迭代对象
19for url in url_list:
20yield scrapy.Request(url=url, callback=self.parse)
21
22
23# 注意此方法的方法名字也是固定的,不可更改
24def parse(self, response):
25# 遍历每个课程的 div.col-md-3
26for course in response.css('div.col-md-3'):
27# 使用 css 语法对每个 course 提取数据
28yield {
29# 课程名称,注意这里使用 strip 方法去掉字符串前后的空白字符
30# 所谓空白字符,指的是空格、换行符、制表符
31# 下面获取 name 的写法还可以省略 h6 的类属性,思考一下为什么可以省略
32'name': course.css('h6.course-name::text').extract_first().strip(),
33# 课程描述
34'description': course.css('div.course-description::text').extract_first().strip(),
35# 课程类型
36'type': course.css('span.course-type::text').extract_first('免费').strip(),
37# 学生人数
38'students': course.css('span.students-count span::text').extract_first()
39}
40
41
按照上一步中的格式写好 spider 后,就能使用 scrapy 的 runspider
命令来运行爬虫了。
xxxxxxxxxx
31cd /home/shiyanlou/Code
2pip install scrapy
3scrapy runspider shiyanlou_courses_spider.py -o data.json
注意这里输出得到的 data.json
文件中的中文显示成 unicode 编码的形式,所以看到感觉像是乱码,其实是正常的。
-o
参数表示打开一个文件,scrapy 默认会将结果序列化为 JSON 格式写入其中。爬虫运行完后,在当前目录打开 data.json
文件就能看到爬取到的数据了。
上一节中,我们只是基于 Scrapy 写了一个爬虫脚本,并没有使用 Scrapy 项目标准的形式。这一节我们要将脚本变成标准 Scrapy 项目的形式,并将爬取到的数据存储到 MySQL 数据库中。数据库的连接和操作使用 SQLAlchemy。
本实验会将爬取的数据存入 MySQL,需要做一些准备工作。首先需要将 MySQL
的编码格式设置为 utf8
,编辑配置文件:
xxxxxxxxxx
11sudo vim /etc/mysql/my.cnf
检查以下几个配置是否存在,有的话则可以不修改:
xxxxxxxxxx
81[client]
2default-character-set = utf8
3
4[mysqld]
5character-set-server = utf8
6
7[mysql]
8default-character-set = utf8
保存后,就可以启动 mysql 了:
xxxxxxxxxx
11sudo service mysql start
以 root 身份进入 mysql,实验环境默认是没有密码的:
xxxxxxxxxx
11mysql -uroot
创建 shiyanlou
库给本实验使用:
xxxxxxxxxx
11mysql > CREATE DATABASE shiyanlou;
完成后输入 quit
退出。
本实验使用 SQLAlchemy
这个 ORM 在爬虫程序中连接和操作 MySQL ,先安装一下sqlalchemy,还需要安装 Python3 连接 MySQL 的驱动程序 mysqlclient
:
xxxxxxxxxx
41pip install scrapy
2pip install sqlalchemy
3sudo apt-get install libmysqlclient-dev
4pip install mysqlclient
进入到 /home/shiyanlou/Code
目录,使用 Scrapy 提供的 startproject
命令创建一个 Scrapy 项目,需要提供一个项目名称,我们要爬取蓝桥的数据,所以将 shiyanlou 作为项目名:
xxxxxxxxxx
21cd /home/shiyanlou/Code
2scrapy startproject shiyanlou
进入 /home/shiyanlou/Code/shiyanlou
目录,可以看到项目结构是这样的:
xxxxxxxxxx
91shiyanlou/
2 scrapy.cfg # 部署配置文件
3 shiyanlou/ # 项目名称
4 __init__.py
5 items.py # 项目 items 定义在这里
6 pipelines.py # 项目 pipelines 定义在这里
7 settings.py # 项目配置文件
8 spiders/ # 所有爬虫写在这个目录下面
9 __init__.py
Scrapy 的 genspider
命令可以快速初始化一个爬虫模版,使用方法如下:
xxxxxxxxxx
11scrapy genspider <name> <domain>
其中 name
参数是这个爬虫的名称,domain
指定要爬取的网站。
进入第二个 shiyanlou
目录,运行下面的命令快速初始化一个爬虫模版:
xxxxxxxxxx
21cd /home/shiyanlou/Code/shiyanlou/shiyanlou
2scrapy genspider courses lanqiao.cn
Scrapy 会在 /home/shiyanlou/Code/shiyanlou/shiyanlou/spiders
目录下新建一个 courses.py
文件,并且在文件中初始化了代码结构:
xxxxxxxxxx
101import scrapy
2
3
4class CoursesSpider(scrapy.Spider):
5 name = 'courses'
6 allowed_domains = ['lanqiao.cn']
7 start_urls = ['http://lanqiao.cn/']
8
9 def parse(self, response):
10 pass
这里面有一个新的属性 allowed_domains
是在前一节中没有介绍到的。它是干嘛的呢?allow_domains
的值可以是一个列表或字符串,包含这个爬虫可以爬取的域名。假设我们要爬的页面是 https://www.example.com/1.html
,那么就把 example.com
添加到 allowed_domains。这个属性是可选的,在我们的项目中并不需要使用它,可以删除。
除此之外 start_urls
的代码和上一节相同:
xxxxxxxxxx
131# -*- coding: utf-8 -*-
2import scrapy
3
4
5class CoursesSpider(scrapy.Spider):
6 name = 'courses'
7
8
9 def start_urls(self):
10 url_list = ['https://www.lanqiao.cn/courses/',
11 'https://www.lanqiao.cn/courses/?page=2',
12 'https://www.lanqiao.cn/courses/?page=3']
13 return url_list
爬虫的主要目标是从网页中提取结构化的信息,Scrapy 爬虫可以将爬取到的数据作为一个 Python dict 返回,但由于 dict 的无序性,所以它不太适合存放结构性数据。Scrapy 推荐使用 Item
容器来存放爬取到的数据。
所有的 items 写在 items.py
(/home/shiyanlou/Code/shiyanlou/shiyanlou/items.py
) 中,下面为要爬取的课程定义一个 Item
:
xxxxxxxxxx
121import scrapy
2
3
4class CourseItem(scrapy.Item):
5 """
6 定义 Item 非常简单,只需要继承 scrapy.Item 类,将每个要爬取的数据声明为 scrapy.Field()
7 下面的代码是我们每个课程要爬取的 4 个数据
8 """
9 name = scrapy.Field()
10 description = scrapy.Field()
11 type = scrapy.Field()
12 students = scrapy.Field()
有了 CourseItem
,就可以将 parse 方法的返回包装成它:
xxxxxxxxxx
251# -*- coding: utf-8 -*-
2import scrapy
3from shiyanlou.items import CourseItem
4
5
6class CoursesSpider(scrapy.Spider):
7 name = 'courses'
8
9
10 def start_urls(self):
11 url_list = ['https://www.lanqiao.cn/courses/',
12 'https://www.lanqiao.cn/courses/?page=2',
13 'https://www.lanqiao.cn/courses/?page=3']
14 return url_list
15
16 def parse(self, response):
17 for course in response.css('div.col-md-3'):
18 # 将返回结果包装为 CourseItem ,其它地方同上一节
19 item = CourseItem({
20 'name': course.css('h6::text').extract_first().strip(),
21 'description': course.css('div.course-description::text').extract_first().strip(),
22 'type': course.css('span.course-type::text').extract_first('免费').strip(),
23 'students': course.css('span.students-count span::text').extract_first()
24 })
25 yield item
如果把 scrapy
想象成一个产品线,spider
负责从网页上爬取数据,Item
相当于一个包装盒,对爬取的数据进行标准化包装,然后把它们扔到 Pipeline
流水线中。
主要在 Pipeline
对 Item
进行这几项处理:
当创建项目时,scrapy 已经在 /home/shiyanlou/Code/shiyanlou/shiyanlou/pipelines.py
中为项目生成了一个 pipline
模版:
xxxxxxxxxx
61class ShiyanlouPipeline(object):
2 def process_item(self, item, spider):
3 """ parse 出来的 item 会被传入这里,这里编写的处理代码会
4 作用到每一个 item 上面。这个方法必须要返回一个 item 对象。
5 """
6 return item
除了 process_item
还有两个常用的 hooks 方法,open_spider
和 close_spider
:
xxxxxxxxxx
131class ShiyanlouPipeline(object):
2 def process_item(self, item, spider):
3 return item
4
5 def open_spider(self, spider):
6 """ 当爬虫被开启的时候调用
7 """
8 pass
9
10 def close_spider(self, spider):
11 """ 当爬虫被关闭的时候调用
12 """
13 pass
在 items.py
所在目录下创建 models.py
(/home/shiyanlou/Code/shiyanlou/shiyanlou/models.py
),在里面使用 SQLAlchemy
语法定义 courses
表结构:
创建文件
xxxxxxxxxx
21cd /home/shiyanlou/Code/shiyanlou/shiyanlou
2touch ./models.py
源码
xxxxxxxxxx
191from sqlalchemy import create_engine
2from sqlalchemy.ext.declarative import declarative_base
3from sqlalchemy import Column, String, Integer
4
5
6engine = create_engine('mysql://root@localhost:3306/shiyanlou?charset=utf8')
7Base = declarative_base()
8
9class Course(Base):
10 __tablename__ = 'courses'
11
12 id = Column(Integer, primary_key=True)
13 name = Column(String(64), index=True)
14 description = Column(String(1024))
15 type = Column(String(64), index=True)
16 students = Column(Integer)
17
18if __name__ == '__main__':
19 Base.metadata.create_all(engine)
使用 SQLAlchemy 创建映射类 Course 这步操作在前面的实验《关系数据库 MySQL 和 ORM》中讲到了,大家可以去查阅相关文档,也可以通过《SQLAlchemy 基础教程》 了解更多相关知识。
运行程序:
xxxxxxxxxx
11python3 models.py
如果运行正确的话,程序什么都不会输出,执行完后,进入 MySQL 客户端中检查是否已经创建了表:
xxxxxxxxxx
81use shiyanlou;
2show tables;
3desc courses;
4+---------------------+
5| Tables_in_shiyanlou |
6+---------------------+
7| courses |
8+---------------------+
如果出现类似上面的东西说明表已经创建成功了!
注意,如果遇到 MySQLdb 包没有找到的错误,有以下几种可能,依次排查下就可以了:
创建好数据表后,就可以在 pipelines.py
编写代码将爬取到的每个 item 存入数据库中。
xxxxxxxxxx
301from sqlalchemy.orm import sessionmaker
2from shiyanlou.models import Course, engine
3
4
5class ShiyanlouPipeline(object):
6
7 def process_item(self, item, spider):
8 # 提取的学习人数是字符串,把它转换成 int
9 item['students'] = int(item['students'])
10 # 根据 item 创建 Course Model 对象并添加到 session
11 # item 可以当成字典来用,所以也可以使用字典解构, 相当于
12 # Course(
13 # name=item['name'],
14 # type=item['type'],
15 # ...,
16 # )
17 self.session.add(Course(**item))
18 return item
19
20 def open_spider(self, spider):
21 """ 在爬虫被开启的时候,创建数据库 session
22 """
23 Session = sessionmaker(bind=engine)
24 self.session = Session()
25
26 def close_spider(self, spider):
27 """ 爬虫关闭后,提交 session 然后关闭 session
28 """
29 self.session.commit()
30 self.session.close()
我们编写的这个 ShiyanlouPipeline
默认是关闭的状态,要开启它,需要在 /home/shiyanlou/Code/shiyanlou/shiyanlou/settings.py
将下面的代码取消注释:
xxxxxxxxxx
41# 默认是被注释的
2ITEM_PIPELINES = {
3 'shiyanlou.pipelines.ShiyanlouPipeline': 300
4}
ITEM_PIPELINES
里面配置需要开启的 pipeline
,它是一个字典,key 表示 pipeline 的位置,值是一个数字,表示的是当开启多个 pipeline 时它的执行顺序,值小的先执行,这个值通常设在 100~1000 之间
前面使用的 runspider
命令用于启动一个独立的 scrapy 爬虫脚本,在 scrapy 项目中启动爬虫使用 crawl
命令,需要指定爬虫的 name
:
xxxxxxxxxx
21cd /home/shiyanlou/Code/shiyanlou/shiyanlou
2scrapy crawl courses
爬虫运行完后,进入 MySQL,输入下面的命令查看爬取数据的前 3 个:
xxxxxxxxxx
21mysql> use shiyanlou;
2mysql> select name, type, description, students from courses limit 3\G
因为 Scrapy 爬虫是异步执行的,所以爬取到的 course 顺序和蓝桥网站上的会不一样
在云课中完成《连接数据库的标准 Scrapy 项目》,然后截图。截图中包含cources.py脚本,items.py脚本,和item pipline脚本,运行脚本和mysql查询数据脚本
本节内容运用前两节学到的知识,爬取蓝桥的用户数据,主要是为了练习、巩固前面学习到的知识。
下面是一个用户主页的截图,箭头指的是我们要爬取的内容:
要爬取的内容和字段名称定义:
意:本节实验的操作需要使用上一节 scrapy 创建的 shiyanlou 项目的代码,代码目录为 /home/shiyanlou/Code/shiyanlou
环境准备
xxxxxxxxxx
51pip install scrapy
2cd /home/shiyanlou/Code
3scrapy startproject shiyanlou
4cd /home/shiyanlou/Code/shiyanlou/shiyanlou/
5touch ./models.py
决定好了要爬取的内容,就可以使用 SQLAlchemy 定义数据模型了,在上一节实验中创建的 /home/shiyanlou/Code/shiyanlou/shiyanlou/models.py
中的 Course
后面定义 User
模型:
使用桌面文件管理工具,找到/home/shiyanlou/Code/shiyanlou/shiyanlou/models.py
,右击,使用vscode打开文件,使用右侧剪切板工具,粘贴如下内容到models.py文件中:
xxxxxxxxxx
361from sqlalchemy import create_engine
2from sqlalchemy.ext.declarative import declarative_base
3from sqlalchemy import Column, String, Integer
4# User 表用到新类型要引入
5from sqlalchemy import Date, Boolean
6
7engine = create_engine('mysql://root@localhost:3306/shiyanlou?charset=utf8')
8Base = declarative_base()
9
10class Course(Base):
11 __tablename__ = 'courses'
12
13 id = Column(Integer, primary_key=True)
14 name = Column(String(64), index=True)
15 description = Column(String(1024))
16 type = Column(String(64), index=True)
17 students = Column(Integer)
18
19
20class User(Base):
21 __tablename__ = 'users'
22
23 id = Column(Integer, primary_key=True)
24 name = Column(String(64), index=True)
25 # 用户类型有普通用户和会员用户两种,我们用布尔值字段来存储
26 # 如果是会员用户,该字段的值为 True ,否则为 False
27 # 这里需要设置字段的默认值为 False
28 is_vip = Column(Boolean, default=False)
29 status = Column(String(64), index=True)
30 school_job = Column(String(64))
31 level = Column(Integer, index=True)
32 join_date = Column(Date)
33 learn_courses_num = Column(Integer)
34
35if __name__ == '__main__':
36 Base.metadata.create_all(engine)
使用终端工具,运行脚本在数据库中创建 users
表了:
创建数据库
以 root 身份进入 mysql,实验环境默认是没有密码的:
xxxxxxxxxx
21sudo service mysql start
2mysql -uroot
创建 shiyanlou
库给本实验使用:
xxxxxxxxxx
21CREATE DATABASE shiyanlou;
2exit;
安装依赖包
xxxxxxxxxx
61pip install scrapy
2pip install sqlalchemy
3pip install mysqlclient
4sudo apt-get install -y libmysqlclient-dev
5cd /home/shiyanlou/Code/shiyanlou/shiyanlou/
6python3 ./models.py
SQLAlchemy
默认不会重新创建已经存在的表,所以不用担心 create_all
会重新创建 course
表造成数据丢失;
运行完成后验证表是否创建
xxxxxxxxxx
51mysql -uroot
2show databases;
3use shiyanlou;
4show tables;
5exit;
结果如下,如果有courses和users表,说明创建表的脚本执行成功:
在 /home/shiyanlou/Code/shiyanlou/shiyanlou/items.py
中添加 UserItem
,为每个要爬取的字段声明一个 Field
:
使用桌面浏览器,使用vscode打开items.py进行修改
在文件末尾添加UserItem类,文件结果如下:
xxxxxxxxxx
211import scrapy
2
3
4class CourseItem(scrapy.Item):
5 """
6 定义 Item 非常简单,只需要继承 scrapy.Item 类,将每个要爬取的数据声明为 scrapy.Field()
7 下面的代码是我们每个课程要爬取的 4 个数据
8 """
9 name = scrapy.Field()
10 description = scrapy.Field()
11 type = scrapy.Field()
12 students = scrapy.Field()
13
14class UserItem(scrapy.Item):
15 name = scrapy.Field()
16 is_vip = scrapy.Field()
17 status = scrapy.Field()
18 school_job = scrapy.Field()
19 level = scrapy.Field()
20 join_date = scrapy.Field()
21 learn_courses_num = scrapy.Field()
如图示:
使用 genspider
命令创建 users
爬虫:
xxxxxxxxxx
21cd /home/shiyanlou/Code/shiyanlou/shiyanlou/
2scrapy genspider users lanqiao.cn
如图示:
使用vscode打开/home/shiyanlou/Code/shiyanlou/shiyanlou/目录下的:users.py
Scrapy 为我们在 spiders
下面创建 users.py
爬虫,将它修改如下:
xxxxxxxxxx
141import scrapy
2
3class UsersSpider(scrapy.Spider):
4 name = 'users'
5
6
7 def start_urls(self):
8 """
9 蓝桥注册的用户数目前大约六十几万,为了爬虫的效率,
10 取 id 在 524,800~525,000 之间的新用户,
11 每间隔 10 取一个,最后大概爬取 20 个用户的数据
12 """
13 url_tmp = 'https://www.lanqiao.cn/users/{}/'
14 return (url_tmp.format(i) for i in range(525000, 524800, -10))
解析数据主要是编写 parse
函数。在实际编写前,最好是用 scrapy shell
对某一个用户进行测试,将正确的提取代码复制到 parse 函数中。
下面的几个例子是 “需要提取的某用户数据在页面源码中的结构” 和对应的提取器(提取器有很多种写法,可以用自己的方式去写)
xxxxxxxxxx
381# 以下为用户页面部分源码
2
3<div class="user-meta" data-v-22a7bf90>
4 <span data-v-22a7bf90> 幺幺哒 </span>
5 <span data-v-22a7bf90> L2282 </span>
6 <!---->
7 <!---->
8</div>
9<div data-v-1b7bcd86 data-v-22a7bf90>
10 <div class="user-status" data-v-1b7bcd86>
11 <span data-v-1b7bcd86> 学生 </span>
12 <span data-v-1b7bcd86> 格瑞魔法学校 </span>
13 </div>
14</div>
15<span class="user-join-date" data-v-22a7bf90> 2016-11-10 加入蓝桥 </span>
16# name
17# 提取结果为字符串,strip 方法去掉前后的空白字符
18# 空白字符包括空格和换行符
19response.css('div.user-meta span::text').extract()[0].strip()
20
21# level
22# 注意提取结果的第一个字符是 L
23response.css('div.user-meta span::text').extract()[1].strip()
24
25# status
26# 该字段为用户个人信息,如果用户未设置,提取不到数据
27# 这种情况将其设为“无”
28response.css('div.user-status span::text').extract_first(default='无').strip()
29
30# school_job
31# 该字段同上,这里需要使用 xpath 提取器
32response.xpath('//div[@class="user-status"]/span[2]/text()').extract_first(default='无').strip()
33
34# learn_courses_num
35# 使用正则表达式来提取数字部分
36# \D+ 匹配多个非数字字符,\d+ 匹配多个数字字符
37# 大家可以尝试使用同样的方法修改 level 提取器的写法
38response.css('span.tab-item::text').re_first('\D+(\d+)\D+')
如下图所示,会员用户头像右下角有一个会员标志,它是一张图片。在 div 标签下,非会员用户只有一个 img 标签在 a 标签内部,会员用户有两个 img 标签,我们可以根据 img 的数量来判断用户类型:
xxxxxxxxxx
21if len(response.css('div.user-avatar img').extract()) == 2:
2 item['is_vip'] = True
依次在 scrapy shell 中测试每个要爬取的数据,最后将代码整合进 users.py
中如下:
xxxxxxxxxx
261import scrapy
2from ..items import UserItem
3
4
5class UsersSpider(scrapy.Spider):
6 name = 'users'
7 allowed_domains = ['lanqiao.cn']
8
9
10 def start_urls(self):
11 url_temp = 'https://www.lanqiao.cn/users/{}'
12 return (url_temp.format(i) for i in range(525000, 524800, -10))
13
14 def parse(self, response):
15 item = UserItem(
16 name = response.css('div.user-meta span::text').extract()[0].strip(),
17 level = response.css('div.user-meta span::text').extract()[1].strip(),
18 status = response.css('div.user-status span::text').extract_first(default='无').strip(),
19 school_job = response.xpath('//div[@class="user-status"]/span[2]/text()').extract_first(default='无').strip(),
20 join_date = response.css('span.user-join-date::text').extract_first().strip(),
21 learn_courses_num = response.css('span.tab-item::text').re_first('\D+(\d+)\D+')
22 )
23 if len(response.css('div.user-avatar img').extract()) == 2:
24 item['is_vip'] = True
25
26 yield item
因为 pipeline 会作用在每个 item 上,当和课程爬虫共存时,需要根据 item 类型使用不同的处理函数。
最终代码文件 /home/shiyanlou/Code/shiyanlou/shiyanlou/pipelines.py
:
xxxxxxxxxx
381from datetime import datetime
2from sqlalchemy.orm import sessionmaker
3from shiyanlou.models import Course, User, engine
4from shiyanlou.items import CourseItem, UserItem
5
6
7class ShiyanlouPipeline(object):
8
9 def process_item(self, item, spider):
10 """ 对不同的 item 使用不同的处理函数
11 """
12 if isinstance(item, CourseItem):
13 self._process_course_item(item)
14 else:
15 self._process_user_item(item)
16 return item
17
18 def _process_course_item(self, item):
19 item['students'] = int(item['students'])
20 self.session.add(Course(**item))
21
22 def _process_user_item(self, item):
23 # 抓取到的数据类似 'L100',需要去掉 'L' 然后转化为 int
24 item['level'] = int(item['level'][1:])
25 # 抓取到的数据类似 '2017-01-01 加入蓝桥',把其中的日期字符串转换为 date 对象
26 item['join_date'] = datetime.strptime(item['join_date'].split()[0], '%Y-%m-%d')
27 # 学习课程数目转化为 int
28 item['learn_courses_num'] = int(item['learn_courses_num'])
29 # 添加到 session
30 self.session.add(User(**item))
31
32 def open_spider(self, spider):
33 Session = sessionmaker(bind=engine)
34 self.session = Session()
35
36 def close_spider(self, spider):
37 self.session.commit()
38 self.session.close()
使用 crawl
命令启动爬虫:
xxxxxxxxxx
21cd /home/shiyanlou/Code/shiyanlou/shiyanlou/
2scrapy crawl users
实验设计了一个新的实例,爬取蓝桥的用户页面,在这个页面中首先需要分析页面中的各种元素,从而设计爬虫中数据提取的方式。然后把需要的数据内容通过 Scrapy 项目中的代码获取得到并解析出来,存储到数据库中。
本节实验包含以下的知识点:
本节实验的操作在笔记本上,数据库使用远程数据库,暂使用内网地址:10.0.10.158,端口:3306
xxxxxxxxxx
71pip install scrapy
2d:
3mkdir d:\\myname
4cd d:\\myname
5scrapy startproject shiyanlou
6cd d:\\myname\shiyanlou\shiyanlou\
7echo '' > models.py
使用pycharm打开目录d:\myname\shiyanlou\
修改D:\myname\shiyanlou\shiyanlou\models.py 文件,内容如下:
xxxxxxxxxx
421from sqlalchemy import create_engine
2from sqlalchemy.ext.declarative import declarative_base
3from sqlalchemy import Column, String, Integer
4# User 表用到新类型要引入
5from sqlalchemy import Date, Boolean
6
7engine = create_engine('mysql://test:test@home.hddly.cn:53306/test?charset=utf8')
8Base = declarative_base()
9
10
11class Course(Base):
12 __tablename__ = 'courses'
13
14 id = Column(Integer, primary_key=True)
15 name = Column(String(64), index=True)
16 description = Column(String(1024))
17 type = Column(String(64), index=True)
18 students = Column(Integer)
19 collector = Column(String(64), index=True)
20 coll_time = Column(Date)
21
22class User(Base):
23 __tablename__ = 'users'
24
25 id = Column(Integer, primary_key=True)
26 name = Column(String(640), index=True)
27 # 用户类型有普通用户和会员用户两种,我们用布尔值字段来存储
28 # 如果是会员用户,该字段的值为 True ,否则为 False
29 # 这里需要设置字段的默认值为 False
30 is_vip = Column(Boolean, default=False)
31 status = Column(String(64), index=True)
32 school_job = Column(String(64))
33 level = Column(Integer, index=True)
34 join_date = Column(Date)
35 learn_courses_num = Column(Integer)
36 stucode = Column(String(64), index=True)
37 collector = Column(String(64), index=True)
38 coll_time = Column(Date)
39
40
41if __name__ == '__main__':
42 Base.metadata.create_all(engine)
安装依赖包
xxxxxxxxxx
51pip install scrapy
2pip install sqlalchemy
3pip install mysqlclient
4pip install fake-useragent
5
items.py中添加
UserItem,每个Item需要包含stucode,collector,coll_time信息,文件结果如下:
xxxxxxxxxx
281import scrapy
2
3
4class CourseItem(scrapy.Item):
5 """
6 定义 Item 非常简单,只需要继承 scrapy.Item 类,将每个要爬取的数据声明为 scrapy.Field()
7 下面的代码是我们每个课程要爬取的 4 个数据
8 """
9 name = scrapy.Field()
10 description = scrapy.Field()
11 type = scrapy.Field()
12 students = scrapy.Field()
13 stucode = scrapy.Field()
14 collector = scrapy.Field()
15 coll_time = scrapy.Field()
16
17
18class UserItem(scrapy.Item):
19 name = scrapy.Field()
20 is_vip = scrapy.Field()
21 status = scrapy.Field()
22 school_job = scrapy.Field()
23 level = scrapy.Field()
24 join_date = scrapy.Field()
25 learn_courses_num = scrapy.Field()
26 stucode = scrapy.Field()
27 collector = scrapy.Field()
28 coll_time = scrapy.Field()
使用 genspider
命令创建 users
爬虫:
xxxxxxxxxx
31d:
2cd D:\myname\shiyanlou\shiyanlou\
3scrapy genspider users lanqiao.cn
修改 users.py
中如下:
xxxxxxxxxx
491import scrapy
2from fake_useragent import UserAgent
3from scrapy import Request
4
5from ..items import UserItem
6
7
8class UsersSpider(scrapy.Spider):
9 name = 'users'
10 allowed_domains = ['lanqiao.cn']
11
12 # @property
13 # def start_urls(self):
14 # url_temp = 'https://www.lanqiao.cn/users/{}'
15 # return (url_temp.format(i) for i in range(525000, 523800, -1))
16
17 def start_requests(self):
18 ua = UserAgent()
19 url_temp = 'https://www.lanqiao.cn/users/{}'
20 for i in range(525000, 524800, -1):
21 headers = {
22 "User-Agent": ua.random,
23 "Cookie" :'lqtoken=9992d82d0b28839fa52f2138f0c5ef83; _ga=GA1.2.148394195.1713187596; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%222432765%22%2C%22first_id%22%3A%2218edcdcb4911297-05af5f4e7352aa-26001a51-1395396-18edcdcb4922006%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMThlZGNkY2I0OTExMjk3LTA1YWY1ZjRlNzM1MmFhLTI2MDAxYTUxLTEzOTUzOTYtMThlZGNkY2I0OTIyMDA2IiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiMjQzMjc2NSJ9%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%222432765%22%7D%2C%22%24device_id%22%3A%2218edcdcb4911297-05af5f4e7352aa-26001a51-1395396-18edcdcb4922006%22%7D; _gid=GA1.2.56784145.1715653295; acw_tc=0b63bb3217156904608981869e599b3089309bec154217375bc6f9732da63b; Hm_lvt_56f68d0377761a87e16266ec3560ae56=1715306206,1715408219,1715652781,1715690462; platform=LANQIAO-FE; Hm_lvt_39c7d7a756ef8d66180dc198408d5bde=1715306207,1715408221,1715652782,1715690463; Hm_lpvt_39c7d7a756ef8d66180dc198408d5bde=1715690636; _ga_XY08NHY75L=GS1.2.1715690463.39.1.1715690641.0.0.0; tfstk=fU-iwEME8F76kbexsHjsYLgmPR3d15sfaIEAMiCq865Q65E93xjDtKvtCGpViEfeElLOk1d3YtXzQRSVbRTBwLr9XVsYmGsf0bh-20p9Cisqzt2XQ5_FnTmYgO7ZIgT6obh-2DphX96-wEhZlc_hh6WN3NWZKyWPIiPV0I5F8TW8_iS2gpkhHTEabtraYDWAIbz4J1QR-HkJ9OhJu_idtObGgoC3mttL2N5Pw6q46HXg__JNtorV1_WA_pvtsu9df3dleCnUjQv2FE7HmWllGHvebEJQsjWBuCIF6guaKGK6QU724ja2ZGAczh733uJ6Q6shL3kYP6Kh6I-G8YElkM-RzG8KRfQAjTA2fC200QJpeh_BmbVFGFB5YtxjUrfcug738zPipo6EHHzblNWCK_MaStc_TLmeNvD3zGbNdOyqKv4blNWCK_HnKz8G796a3; Hm_lpvt_56f68d0377761a87e16266ec3560ae56=1715692043'
24 }
25 url =url_temp.format(i)
26 yield Request(url=url, headers=headers)
27
28 def parse(self, response):
29 item = UserItem(
30 name = response.css('div.user-basic-info div span::text').extract()[0].strip(),
31 # #__layout > div > div.body.no-padding > div.page-box > div > div.user-info-wrap.mb-20px > div.left > div.user-basic-info > div > div > span.name
32 # #__layout > div > div.body.no-padding > div.page-box > div > div.user-info-wrap.mb-20px > div.left > div.user-basic-info > div > div > span.name
33 level = response.css('div.user-basic-info div span::text').extract()[1].strip(),
34 # #__layout > div > div.body.no-padding > div.page-box > div > div.user-info-wrap.mb-20px > div.left > div.user-basic-info > div > div > span.level
35 status ='0',
36
37 # school_job = response.xpath('//div[@class="user-status"]/span[2]/text()').extract_first(default='无').strip(),
38 school_job = '',
39 # join_date = response.css('span.user-join-date::text').extract_first().strip(),
40 join_date = '1900-01-01',
41 learn_courses_num = response.css('div.statistic-item.trial-number div.data span.value::text').extract_first().strip()
42 # #__layout > div > div.body.no-padding > div.page-box > div > div.bottom-content > div.left >
43 # div.card-panel.mb-20px > div > div.statistic-wrap > div.statistic-item.trial-number > div.data > span
44 )
45 if len(response.css('div.auth-content a div img').extract()) == 2:
46 # #__layout > div > div.universal-header > div > div > div > div > div.auth-content > a > div > img
47 item['is_vip'] = True
48
49 yield item
因为 pipeline 会作用在每个 item 上,当和课程爬虫共存时,需要根据 item 类型使用不同的处理函数。
最终代码文件 pipelines.py:
需要修改open_spider方法中的stucode和collector值,请换成本人学号和姓名
xxxxxxxxxx
491from datetime import datetime
2
3from sqlalchemy.orm import sessionmaker
4
5from .items import CourseItem
6from .models import Course, User, engine
7
8
9class ShiyanlouPipeline(object):
10
11 def process_item(self, item, spider):
12 """ 对不同的 item 使用不同的处理函数
13 """
14 item['stucode'] =self.stucode
15 item['collector'] = self.collector
16 item['coll_time'] = datetime.now()
17 if isinstance(item, CourseItem):
18 self._process_course_item(item)
19 else:
20 self._process_user_item(item)
21 return item
22
23 def _process_course_item(self, item):
24 item['students'] = int(item['students'])
25 self.session.add(Course(**item))
26
27 def _process_user_item(self, item):
28 # 抓取到的数据类似 'L100',需要去掉 'L' 然后转化为 int
29 # if not str(item['name']).startswith('L'):
30 # item['name']= str(str(item['name']).encode('utf-8'))
31
32 item['level'] = int(item['level'][1:])
33 # 抓取到的数据类似 '2017-01-01 加入蓝桥',把其中的日期字符串转换为 date 对象
34 # item['join_date'] = datetime.strptime(item['join_date'].split()[0], '%Y-%m-%d')
35 # 学习课程数目转化为 int
36 item['learn_courses_num'] = int(item['learn_courses_num'])
37 # 添加到 session
38 self.session.add(User(**item))
39 self.session.commit()
40
41 def open_spider(self, spider):
42 self.stucode = 'yuxm' # 请改为自已的学号,如:22273401111
43 self.collector = '俞老师' # 请改为自已的名字
44 Session = sessionmaker(bind=engine)
45 self.session = Session()
46
47 def close_spider(self, spider):
48 self.session.commit()
49 self.session.close()
修改settings文件开启ITEM_PIPELINES,在文件末尾添加如下:
xxxxxxxxxx
71ITEM_PIPELINES = {
2"shiyanlou.pipelines.ShiyanlouPipeline": 300,
3}
4
5DOWNLOAD_DELAY = 2 # 基础延迟时间
6RANDOMIZE_DOWNLOAD_DELAY = True # 启用随机延迟
7
使用 crawl
命令启动爬虫:
xxxxxxxxxx
31d:
2cd D:\myname\shiyanlou\shiyanlou\
3scrapy crawl users
修改D:\myname\shiyanlou\shiyanlou__init__.py,内容如下,然后可右击该文件运行或调试
xxxxxxxxxx
51from scrapy import cmdline
2
3if __name__ == '__main__':
4cmdline.execute("scrapy crawl users".split())
5
练习内容:
在本地完成Scrapy 爬取蓝桥用户数据抓取 ,步骤包括环境准备、创建scrapy项目、修改items.py、创建爬虫users.py、创建爬虫users.py、修改settings.py、运行爬虫等,完成以上内容并截图
截图要求:
本节内容主要介绍使用 Scrapy 进阶的知识和技巧,包括页面追随、图片下载、组成 item 的数据在多个页面和模拟登录。
在前面实现课程爬虫和用户爬虫中,因为蓝桥的课程和用户 URL 都是通过 id 来构造的,所以可以轻松构造一批顺序的 URLS 给爬虫。但是在很多网站中,URL 并不是轻松可以构造的,更常用的方法是从一个或者多个链接(start_urls)爬取页面后,再从页面中解析需要的链接继续爬取,不断循环。
下面是一个简单的例子,在蓝桥课程编号为 63 的课程主页,从页面底部相关课程推荐来获取下一批要爬取的课程的 URL ,我们的任务是爬取该课程和所有推荐课程的名字和作者。
结合前面所学的知识,你可能会写出类似这样的代码:
xxxxxxxxxx
181import scrapy
2
3
4class CoursesFollowSpider(scrapy.Spider):
5 name = 'courses_follow'
6 start_urls = ['https://lanqiao.cn/courses/63']
7
8 def parse(self, response):
9 yield {
10 'name': response.css('h1.course-title::text').extract_first().strip(),
11 'author': response.css('p.teacher-info span::text').extract_first()
12 }
13 # 从返回的 response 中提取 “推荐课程” 的链接
14 # 依次构造请求,再将本函数指定为回调函数,类似递归
15 for url in response.css('div.course-item-box a::attr(href)').extract():
16 # 解析出的 url 是相对 url,可以手动将它构造为全 url
17 # 或者使用 response.urljoin() 方法
18 yield scrapy.Request(url=response.urljoin(url), callback=self.parse)
完成页面跟随的核心就是最后 for 循环的代码。使用 response.follow
方法可以对 for 循环代码做进一步简化:
xxxxxxxxxx
161import scrapy
2
3
4class CoursesFollowSpider(scrapy.Spider):
5 name = 'courses_follow'
6 start_urls = ['https://www.lanqiao.cn/courses/63']
7
8 def parse(self, response):
9 yield {
10 'name': response.css('h1.course-title::text').extract_first().strip(),
11 'author': response.css('p.teacher-info span::text').extract_first()
12 }
13 # 不需要 extract 了
14 for url in response.css('div.course-item-box a::attr(href)'):
15 # 不需要构造全 url 了
16 yield response.follow(url, callback=self.parse)
scrapy 内部内置了下载图片的 pipeline。下面以下载蓝桥课程首页每个课程的封面图片为例展示怎么使用它。注意项目的路径需要放置在 /home/shiyanlou/Code/
下,命名为 shiyanlou。
可以新创建一个项目,也可以继续使用上一节实验的 scrapy 项目代码。
首先需要在 /home/shiyanlou/Code/shiyanlou/shiyanlou/items.py
中定义一个 item ,它包含两个必要的字段:
xxxxxxxxxx
51class CourseImageItem(scrapy.Item):
2 # 要下载的图片 url 列表
3 image_urls = scrapy.Field()
4 # 下载的图片会先放在这里
5 images = scrapy.Field()
运行 scrapy genspider courses_image lanqiao.cn/courses
生成一个爬虫,爬虫的核心工作就是解析所有图片的链接到 CourseImageItem
的 image_urls 中。 将以下代码写入 /home/shiyanlou/Code/shiyanlou/shiyanlou/spiders/courses_image.py
文件中:
xxxxxxxxxx
141import scrapy
2
3from shiyanlou.items import CourseImageItem
4
5
6class CoursesImageSpider(scrapy.Spider):
7 name = 'courses_image'
8 start_urls = ['https://www.lanqiao.cn/courses/']
9
10 def parse(self, response):
11 item = CourseImageItem()
12 #解析图片链接到 item
13 item['image_urls'] = response.xpath('//img[@class="cover-image"]/@src').extract()
14 yield item
代码完成后需要在 settings.py
中启动 scrapy 内置的图片下载 pipeline,因为 ITEM_PIPELINES
里的 pipelines 会按顺序作用在每个 item 上,而我们不需要 ShiyanlouPipeline
作用在图片 item 上,所以要把它注释掉:
xxxxxxxxxx
41ITEM_PIPELINES = {
2 'scrapy.pipelines.images.ImagesPipeline': 100,
3 # 'shiyanlou.pipelines.ShiyanlouPipeline': 300
4}
还需要配置图片存储的目录:
xxxxxxxxxx
11IMAGES_STORE = 'images'
运行程序:
xxxxxxxxxx
51# 安装需要的 PIL 包,pillow 是前者的一个比较好的实现版本
2$ pip3 install pillow
3
4# 执行图片下载爬虫
5$ scrapy crawl courses_image
scrapy 会将图片下载到 images/full
下面,保存的文件名是对原文件进行的 hash。为什么会有一个 full 目录呢?full 目录用于存储原尺寸的图片,因为 scrapy 可以配置改变下载图片的尺寸,比如在 settings 中给你添加下面的配置生成小图片:
xxxxxxxxxx
31IMAGES_THUMBS = {
2 'small': (50, 50)
3}
在前面几节实现的爬虫中,组成 item 的数据全部都是在一个页面中获取的。但是在实际的爬虫项目中,经常需要从不同的页面抓取数据组成一个 item。下面通过一个例子展示如何处理这种情况。
有一个需求,爬取蓝桥课程首页所有课程的名称、封面图片链接和课程作者。课程名称和封面图片链接在课程主页 https://www.shiyanlou.cn/courses/
就能爬到,课程作者只有点击课程,进入课程详情页面才能看到,怎么办呢?
scrapy 的解决方案是多级 request 与 parse 。简单地说就是先请求课程首页,在回调函数 parse 中解析出课程名称和课程图片链接,然后在 parse 函数中再构造一个请求到课程详情页面,在处理课程详情页的回调函数中解析出课程作者。
首先在 items.py 中创建相应的 Item 类:
xxxxxxxxxx
41class MultipageCourseItem(scrapy.Item):
2 name = scrapy.Field()
3 image = scrapy.Field()
4 author = scrapy.Field()
终端执行如下命令生成一个爬虫脚本:
xxxxxxxxxx
11$ scrapy genspider multipage www.lanqiao.cn
操作截图:
如上图所示,打开 multipage.py
文件并修改代码如下:
xxxxxxxxxx
331import scrapy
2
3from shiyanlou.items import MultipageCourseItem
4
5
6class MultipageSpider(scrapy.Spider):
7 name = 'multipage'
8 start_urls = ['https://www.lanqiao.cn/courses/']
9
10 def parse(self, response):
11 for course in response.css('div.col-md-3'):
12 item = MultipageCourseItem(
13 # 解析课程名称
14 name=course.css('h6.course-name::text').extract_first().strip(),
15 # 解析课程图片
16 image=course.css('img.cover-image::attr(src)').extract_first()
17 )
18 # 构造课程详情页面的链接,爬取到的链接是相对链接,调用 urljoin 方法构造全链接
19 course_url = course.css('a::attr(href)').extract_first()
20 full_course_url = response.urljoin(course_url)
21 # 构造到课程详情页的请求,指定回调函数
22 request = scrapy.Request(full_course_url, self.parse_author)
23 # 将未完成的 item 通过 meta 传入 parse_author
24 request.meta['item'] = item
25 yield request
26
27 def parse_author(self, response):
28 # 获取未完成的 item
29 item = response.meta['item']
30 # 解析课程作者
31 item['author'] = response.css('p.teacher-info span::text').extract_first()
32 # item 构造完成,生成
33 yield item
关闭所有的 pipeline,运行爬虫,保存结果到文件中:
xxxxxxxxxx
11$ scrapy crawl multipage -o /home/shiyanlou/Code/shiyanlou/shiyanlou/data.json
这部分的知识点不容易理解,可以参考下先前同学的一个提问来理解 https://www.lanqiao.cn/questions/50921/
有些网页需要登录后才能访问,例如任何网站的用户个人主页。有些网页中的部分内容需要登录后才能看到,例如 GitHub 中的私有仓库。
如果想要爬取登录后才能看到的内容就需要 Scrapy 模拟出登录的状态再去抓取页面,解析数据。这个实验就是要模拟登录自己的主页。
通常网站都会有一个 login 页面,GitHub 的 login 页面网址是:https://github.com/login
。在浏览器的地址栏输入这个地址敲回车:
鼠标右键选择“检查”,打开开发者工具栏,选择 Network
:
在页面的登录表单中输入错误的用户名和密码,点击“登录”按钮。如下图所示,右侧出现 session 请求,点击此文件,出现请求和响应信息:
点击登录按钮后,会有一个 POST 请求发送给服务器。在 Form Data 中可以看到本次提交的内容。
其中红色框中的 token 字段是关键信息,为了防止跨域伪造攻击,网站通常会在表单中添加一个隐藏域来放置这个 token 字段。当浏览器发送携带表单的 POST 请求时,服务器收到请求后会比对表单中的 token 字段与 Cookies 中的信息以判断请求来源的可靠性。
所以我们在发送 POST 登录请求时,就要携带这个 token 字段。
如何获取这个字段呢?很简单,在登录页的源码上就会有。在开发者工具栏中打开页面源码,搜索 "token" 关键字即可:
也就是 form 标签下的第一个 input 标签的 value 属性值。
要获取 token 值,就要发送一次请求。将如下代码写入 /home/shiyanlou/github_login.py
文件中:
xxxxxxxxxx
121import scrapy
2
3
4class GithubSpider(scrapy.Spider):
5 name = 'github_login'
6 start_urls = ['https://github.com/login']
7
8 def parse(self, response):
9 token = response.xpath('//form/input[1]/@value').extract_first()
10 print('==================================')
11 print('TOKEN:', token)
12 print('==================================')
以上代码会向 GitHub 发送一次登录请求,我们可以通过响应对象获取隐藏在表单中的 token 字段。
针对本次模拟登录的学习,我们要安装 Scrapy 2.1 版本:
xxxxxxxxxx
11$ sudo pip install scrapy==2.1
在执行 scrapy runspider
命令时,可以使用 -L
选项设置打印信息的级别。为了避免多余的普通信息出现在屏幕上,我们设置打印级别为 ERROR ,也就是只打印错误信息。
现在在终端执行如下命令:
xxxxxxxxxx
11$ scrapy runspider -L ERROR github_login.py
此命令执行顺利的话,会打印 token 字段到屏幕上。操作截图如下:
有了 token 字段,就可以构造 POST 登录请求了。这次请求与前一次的地址相同,请求方法不同。
这种情况,我们可以使用 scrapy.FormRequest.from_response 方法构造 POST 请求。
修改 github_login.py
文件如下:
xxxxxxxxxx
291import scrapy
2
3
4class GithubSpider(scrapy.Spider):
5 name = 'github_login'
6 start_urls = ['https://github.com/login']
7
8 def parse(self, response):
9 # 获取 token 字段
10 token = response.xpath('//form/input[1]/@value').extract_first()
11 print('==================================')
12 print('TOKEN:', token)
13 print('==================================')
14 # 构造 POST 登录请求
15 return scrapy.FormRequest.from_response(
16 # 第一个参数为上次请求的响应对象,这是固定写法
17 response,
18 # 将 token 字段和用户信息作为表单数据
19 formdata = {
20 'authenticity_token': token,
21 'login': '用户名',
22 'password': '密码'
23 },
24 callback = self.after_parse,
25 )
26
27 def after_parse(self, response):
28 print('STATUS:', response.status)
29 print('==================================')
再次执行程序:
这样就登录成功了。现在可以爬取你自己的私有仓库和个人主页。
*注:由于原网页随时会发生变化,所以爬虫代码往往是即时代码,有效期由每个网站决定,主要看思路和框架
本节内容主要通过蓝桥用户爬虫的代码实例介绍如何使用 Scrapy 进阶的知识和技巧,包括页面追随,图片下载, 组成 item 的数据在多个页面,模拟登录等。
本节实验中涉及到的知识点:
完成本周的学习之后,我们再次根据脑图的知识点进行回顾,让动手实践过程中学习到的知识点建立更加清晰的体系。
请点击以下链接回顾本周的 Scrapy 爬虫框架的学习:
请注意实验只会包含常用的知识点,对于未涉及到的知识点,如果在脑图中看到可以查看 Scrapy 官方文档获得详细说明,也非常欢迎在讨论组里与同学和助教进行讨论,技术交流也是学习成长的必经之路