Python Scrapy 爬虫(二):scrapy 初试
接上篇,之前我们搭建好了运行环境,相当于我们搭好了炮台,现在就差猎物和武器了。
一、选取猎物
此处选择爬取西刺代理 IP 作为示例项目,原因有如下两点:
- 西刺代理数据规范,爬取简单,作为演示项目比较合适
- 代理 IP 在我们的爬虫中也许还能派上用场(虽然可用率低了点,但如果你不是走量的,平时自己用一下还是不错的)
猎物 URL
http://www.xicidaili.com/nn
注:虽然西刺声称提供了全网唯一的免费代理 IP 接口,但似乎并没有什么用,因为根本不返回数据...我们自己做点小工作还是可以的。
二、我们的目标是
scrapy 初试计划实现的效果是:
- 从西刺代理网站上爬取免费的国内高匿代理 IP
- 将爬取的代理 IP存入 MySQL 数据库中
- 通过循环爬取的方式获取最新 IP,并通过设立数据库唯一键的方式进行简易版去重
三、目标分析
正所谓知己知彼,至于胜多胜少,先不纠结。我们先打开网站(使用 Chrome 打开),看见的大概是下面的这个东西。
西刺代理网页结构分析
- 大致浏览我们的目标网站,选取我们需要的数据。从网页上我们可以看到西刺代理国内高匿 IP 展示了国家、IP、端口、服务器地址、是否匿名、类型、速度、连接时间、存活时间、验证时间这些信息。
- 在网页的数据展示区的字段名称(蓝色)区域点击右键 -> 检查,我们发现该网页的数据是由 <table></table> 进行渲染布局的
- 把网页拖动到底部,发现网站的数据进行分布展示,我们点击下一页翻页一观察就发现很有规律的是页码参数就在 URL 的后面,当前第几页就传数字几,如:
http://www.xicidaili.com/nn/3
- 我们看到国家是显示的国旗,速度与连接时间是显示的两个颜色块,似乎不太好拿这三个信息?
此时,我们将鼠标移至网页中某一面小国旗的位置处点击右键 -> 检查,我们发现这是一个 img 标签,其中 alt 属性有国家代码。明了了吧,这个国家信息我们能拿到。
我们再把鼠标移至速度的色块处点击右键 -> 检查,我们可以发现有个 div 上有个 title 显示类似 0.876秒这种数据,而连接时间也是这个套路,于是基本确定我们的数据项都能拿到,且能通过翻页拿取更多 IP。
四、准备武器弹药
4.1 准备武器
我们的武器当然是 scrapy
4.2 准备弹药
弹药包括
- pymsql (Python 操作 MySQL 的库)
- fake-useragent (隐藏你的身份)
- pywin32
五、开火
5.1 创建虚拟环境
C:\Users\jiang>workon
Pass a name to activate one of the following virtualenvs:
==============================================================================
test
C:\Users\jiang>mkvirtualenv proxy_ip
Using base prefix 'd:\\program files\\python36'
New python executable in D:\ProgramData\workspace\python\env\proxy_ip\Scripts\python.exe
Installing setuptools, pip, wheel...done.
5.2 安装第三方库
注意:下文的命令中,只要命令前方有 "(proxy_ip)" 标识,即表示该命令是在上面创建的 proxy_ip 的 python 虚拟环境下执行的。
1 安装 scrapy
(proxy_ip) C:\Users\jiang>pip install scrapy -i https://pypi.douban.com/simple
如果安装 scrapy 时出现如下错误:
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
可使用如下方式解决:
- 手动下载 twisted 的 whl 包,下载地址如下
https://www.lfd.uci.edu/~gohlke/pythonlibs/
打开上方的 URL,搜索 Twisted,下载最新的符合 python 版本与 windows 版本的 whl 文件,如
twisted如上图所示,Twisted‑18.4.0‑cp36‑cp36m‑win_amd64.whl 表示匹配 Python 3.6 的 Win 64 文件。这是与我环境匹配的。因此,下载该文件,然后找到文件下载的位置,把其拖动到 CMD 命令行窗口进行安装,如下示例:
(proxy_ip) C:\Users\jiang>pip install D:\ProgramData\Download\Twisted-18.4.0-cp36-cp36m-win_amd64.whl
离线安装 twisted
再执行如下命令安装 scrapy 即可成功安装
(proxy_ip) C:\Users\jiang>pip install scrapy -i https://pypi.douban.com/simple
2 安装 pymysql
(proxy_ip) C:\Users\jiang>pip install pymysql -i https://pypi.douban.com/simple
3 安装 fake-useragent
(proxy_ip) C:\Users\jiang>pip install fake-useragent -i https://pypi.douban.com/simple
4 安装 pywin32
(proxy_ip) C:\Users\jiang>pip install pywin32 -i https://pypi.douban.com/simple
5.3 创建 scrapy 项目
1 创建一个工作目录(可选)
我们可以创建一个专门的目录用于存放 python 的项目文件,例如:
我在用户目录下创建了一个 python_projects,也可以创建任何名称的目录或者选用一个自己知道位置的目录。
(proxy_ip) C:\Users\jiang>mkdir python_projects
2 创建 scrapy 项目
进入工作目录,执行命令创建一个 scrapy 的项目
(proxy_ip) C:\Users\jiang>cd python_projects
(proxy_ip) C:\Users\jiang\python_projects>scrapy startproject proxy_ip
New Scrapy project 'proxy_ip', using template directory 'd:\\programdata\\workspace\\python\\env\\proxy_ip\\lib\\site-packages\\scrapy\\templates\\project', created in:
C:\Users\jiang\python_projects\proxy_ip
You can start your first spider with:
cd proxy_ip
scrapy genspider example example.com
至此,已经完成了创建一个 scrapy 项目的工作。接下来,开始我们的狩猎计划吧...
5.4 关键配置编码
1 打开项目
使用 PyCharm 打开我们刚刚创建好的 scrapy 项目,点击 "Open in new window" 打开项目
打开项目 scrapy 项目初始结构2 配置项目环境
File -> Settings -> Project: proxy_ip -> Project Interpreter -> 齿轮按钮 -> add ...
设置项目环境选择 "Existing environment" -> "..." 按钮
选择虚拟环境位置找到之前创建的 proxy_ip 的虚拟环境的 Scripts/python.exe,选中并确定
选择 proxy_ip 虚拟环境的 python.exe虚拟环境 proxy_ip 的位置默认位于 C:\Users\username\envs,此处我的虚拟机位置已经通过修改 WORK_ON 环境变量更改。
3 配置 items.py
items 中定义了我们爬取的字段以及对各字段的处理,可以简单地类似理解为这是一个 Excel 的模板,我们定义了模块的表头字段及字段的属性等等,然后我们按照这个模块往表格里填数。
class ProxyIpItem(scrapy.Item):
country = scrapy.Field()
ip = scrapy.Field()
port = scrapy.Field( )
server_location = scrapy.Field()
is_anonymous = scrapy.Field()
protocol_type = scrapy.Field()
speed = scrapy.Field()
connect_time = scrapy.Field()
survival_time = scrapy.Field()
validate_time = scrapy.Field()
source = scrapy.Field()
create_time = scrapy.Field()
def get_insert_sql(self):
insert_sql = """
insert into proxy_ip(
country, ip, port, server_location,
is_anonymous, protocol_type, speed, connect_time,
survival_time, validate_time, source, create_time
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
params = (
self["country"], self["ip"], self["port"], self["server_location"],
self["is_anonymous"], self["protocol_type"], self["speed"], self["speed"],
self["survival_time"], self["validate_time"], self["source"], self["create_time"]
)
return insert_sql, params
4 编写 pipelines.py
pipeline 直译为管道,而在 scrapy 中的 pipelines 的功能与管道也非常相似,我们可以在 piplelines.py 中定义多个管道。你可以这么来理解,比如:有一座水库,我们要从这个水库来取水到不同的地方,比如自来水厂、工厂、农田...
于是我们建了三条管道1、2、3,分别连接到自来水厂,工厂、农田。当管道建立好之后,只要我们需要水的时候,打开开关(阀门),水库里的水就能源源不断的流向不同的目的地。
而此处的场景与上面的水库取水有很多相似性。比如,我们要爬取的网站就相当于这个水库,而我们在 pipelines 中建立的管道就相当于建立的取水管道,只不过 pipelines 中定义的管道不是用来取水,而是用来存取我们爬取的数据,比如,你可以在 pipelines 中定义一个流向文件的管道,也可以建立一个流向数据库的管道(比如MySQL/Mongodb/ElasticSearch 等等),而这些管道的阀门就位于 settings.py 文件中,当然,你也可以多个阀门同时打开,你甚至还可以定义各管道的优先级。
如果您能看到这儿,您是否会觉得很有意思。原来,那些程序界的大佬们,他们在设计这个程序框架的时候,其实参照了很多现实生活中的实例。在此,我虽然不敢肯定编写出 scrapy 这样优秀框架的前辈们是不是参考的现实生活中的水库的例子在设计整个框架,但我能确定的是他们一定参考了和水库模型类似的场景。
在此,我也向这些前辈致敬,他们设计的框架非常优秀而且简单好用,感谢!
前面说了这么多,都是我的一些个人理解以及感触,下面给出 pipelines.py 中的示例代码:
# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
import pymysql
from twisted.enterprise import adbapi
class ProxyIpPipeline(object):
"""
xicidaili
"""
def __init__(self, dbpool):
self.dbpool = dbpool
@classmethod
def from_settings(cls, settings):
dbparms = dict(
host=settings["MYSQL_HOST"],
db=settings["MYSQL_DBNAME"],
user=settings["MYSQL_USER"],
passwd=settings["MYSQL_PASSWORD"],
charset='utf8',
cursorclass=pymysql.cursors.DictCursor,
use_unicode=True,
)
dbpool = adbapi.ConnectionPool("pymysql", **dbparms)
# 实例化一个对象
return cls(dbpool)
def process_item(self, item, spider):
# 使用twisted将mysql插入变成异步执行
query = self.dbpool.runInteraction(self.do_insert, item)
query.addErrback(self.handle_error, item, spider) # 处理异常
def handle_error(self, failure, item, spider):
# 处理异步插入的异常
print(failure)
def do_insert(self, cursor, item):
# 执行具体的插入
# 根据不同的item 构建不同的sql语句并插入到mysql中
insert_sql, params = item.get_insert_sql()
print(insert_sql, params)
try:
cursor.execute(insert_sql, params)
except Exception as e:
print(e)
注:
- 以上代码定义的一个管道,是一个注入 MySQL 的管道,其中的写法模式完全固定,只有其中的 dbpool = adbapi.ConnectionPool("pymysql", **dbparms) 此处需要与你使用的连接 mysql 的第三方库一致,比如此处我使用的是 pymysql,就设置 pymysql,如果使用的是其他第三方库,如 MySQLdb,更改为 MySQLdb 即可...
- 其中的 dbparams 中的 mysql 信息,是从 settings 文件中读取的,我们只需要配置 settings 文件中的 MySQL 信息即可
配置 settings.py 中的 pipeline 设置
由于我们的 pipelines.py 中定义的这唯一一条管道的管道名称是创建 scrapy 项目时默认的。因此,settings.py 文件中已经默认了会使用该管道。
pipelines 默认设置在此为了突出演示效果,我在下方列举了默认的 pipeline 与 其他自定义的 pipeline 的设置
pipeline 自定义设置上面表示对于我们设置了两个 pipeline,其中一个是写入 MySQL,而另一个是写入 MongoDB,其中后面的 300,400 表示 pipeline 的执行优先级,其中数值越大优先级越小。
由于 pipelines 中用到的 MySQL 信息在 settings 中配置,在此也列举出,在 settings.py 文件的末尾添加如下信息即可
MYSQL_HOST = "localhost"
MYSQL_DBNAME = "crawler"
MYSQL_USER = "root"
MYSQL_PASSWORD = "root123"
settings 中的 MySQL 设置
5 编写 spider
接着使用水库放水的场景作为示例,水库放水给下游,但并不是水库里的所有东西都需要,比如水库里有杂草,有泥石等等,这些东西如果不需要,那么就要把它过滤掉。而过滤的过程就类似于我们的 spider 的处理过程。
前面的配置都是辅助型的,spider 里面才是我们爬取数据的逻辑,在 spiders 目录下创建一个 xicidaili.py 的文件,在里面编写我们需要的 spider 逻辑。
由于本文篇幅已经过于冗长,此处我打算省略 spider 中的详细介绍,可以从官网获取到 spider 相关信息,其中官方首页就有一个简单的 spider 文件的标准模板,而我们要做的是,只需要按照模板,在其中编写我们提取数据的规则即可。
# -*- coding: utf-8 -*-
import scrapy
from scrapy.http import Request
from proxy_ip.items import ProxyIpItem
from proxy_ip.util import DatetimeUtil
class ProxyIp(scrapy.Spider):
name = 'proxy_ip'
allowed_domains = ['www.xicidaili.com']
# start_urls = ['http://www.xicidaili.com/nn/1']
def start_requests(self):
start_url = 'http://www.xicidaili.com/nn/'
for i in range(1, 6):
url = start_url + str(i)
yield Request(url=url, callback=self.parse)
def parse(self, response):
ip_table = response.xpath('//table[@id="ip_list"]/tr')
proxy_ip = ProxyIpItem()
for tr in ip_table[1:]:
# 提取内容列表
country = tr.xpath('td[1]/img/@alt')
ip = tr.xpath('td[2]/text()')
port = tr.xpath('td[3]/text()')
server_location = tr.xpath('td[4]/a/text()')
is_anonymous = tr.xpath('td[5]/text()')
protocol_type = tr.xpath('td[6]/text()')
speed = tr.xpath('td[7]/div[1]/@title')
connect_time = tr.xpath('td[8]/div[1]/@title')
survival_time = tr.xpath('td[9]/text()')
validate_time = tr.xpath('td[10]/text()')
# 提取目标内容
proxy_ip['country'] = country.extract()[0].upper() if country else ''
proxy_ip['ip'] = ip.extract()[0] if ip else ''
proxy_ip['port'] = port.extract()[0] if port else ''
proxy_ip['server_location'] = server_location.extract()[0] if server_location else ''
proxy_ip['is_anonymous'] = is_anonymous.extract()[0] if is_anonymous else ''
proxy_ip['protocol_type'] = protocol_type.extract()[0] if type else ''
proxy_ip['speed'] = speed.extract()[0] if speed else ''
proxy_ip['connect_time'] = connect_time.extract()[0] if connect_time else ''
proxy_ip['survival_time'] = survival_time.extract()[0] if survival_time else ''
proxy_ip['validate_time'] = '20' + validate_time.extract()[0] + ':00' if validate_time else ''
proxy_ip['source'] = 'www.xicidaili.com'
proxy_ip['create_time'] = DatetimeUtil.get_current_localtime()
yield proxy_ip
6 middlewares.py
很多情况下,我们只需要编写或配置 items,pipeline,spidder,settings 这四个部分即可完整运行一个完整的爬虫项目,但 middlewares 在少数情况下会有用到。
再用水库放水的场景为例,默认情况下,水库放水的流程大概是,自来水厂需要用水,于是他们发起一个请求给水库,水库收到请求后把阀门打开,按照过滤后的要求把水放给下游。但如果自来水厂有特殊要求,比如说自来水厂他可以只想要每天 00:00 - 7:00 这段时间放水,这就属于自定义情况了。
而 middlewares.py 中就是定义的这些信息,它包括默认的请求与响应处理,比如默认全天放水... 而如果我们有特殊需求,在 middlewares.py 定义即可...
以下附本项目中使用 fake-useragent 来随机切换请求的 user-agent 的代码:
class RandomUserAgentMiddleware(object):
"""
随机更换 user-agent
"""
def __init__(self, crawler):
super(RandomUserAgentMiddleware, self).__init__()
self.ua = UserAgent()
self.ua_type = crawler.settings.get("RANDOM_UA_TYPE", "random")
@classmethod
def from_crawler(cls, crawler):
return cls(crawler)
def process_request(self, request, spider):
def get_ua():
return getattr(self.ua, self.ua_type)
random_ua = get_ua()
print("current using user-agent: " + random_ua)
request.headers.setdefault("User-Agent", random_ua)
settings.py 中配置 middleware 信息
# Crawl responsibly by identifying yourself (and your website) on the user-agent
RANDOM_UA_TYPE = "random" # 可以配置 {'ie', 'chrome', 'firefox', 'random'...}
# Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'proxy_ip.middlewares.RandomUserAgentMiddleware': 100,
}
7 settings.py
settings.py 中配置了项目的很多信息,用于统一管理配置,下方给出示例:
# -*- coding: utf-8 -*-
import os
import sys
# Scrapy settings for proxy_ip project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
# http://doc.scrapy.org/en/latest/topics/settings.html
# http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
# http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
BOT_NAME = 'proxy_ip'
SPIDER_MODULES = ['proxy_ip.spiders']
NEWSPIDER_MODULE = 'proxy_ip.spiders'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
RANDOM_UA_TYPE = "random" # 可以配置 {'ie', 'chrome', 'firefox', 'random'...}
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
# Configure maximum concurrent requests performed by Scrapy (default: 16)
#CONCURRENT_REQUESTS = 32
# Configure a delay for requests for the same website (default: 0)
# See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 10
# The download delay setting will honor only one of:
#CONCURRENT_REQUESTS_PER_DOMAIN = 16
#CONCURRENT_REQUESTS_PER_IP = 16
# Disable cookies (enabled by default)
COOKIES_ENABLED = False
# Disable Telnet Console (enabled by default)
#TELNETCONSOLE_ENABLED = False
# Override the default request headers:
#DEFAULT_REQUEST_HEADERS = {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
#}
# Enable or disable spider middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
# 'proxy_ip.middlewares.ProxyIpSpiderMiddleware': 543,
#}
# Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'proxy_ip.middlewares.RandomUserAgentMiddleware': 100,
}
# Enable or disable extensions
# See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
#EXTENSIONS = {
# 'scrapy.extensions.telnet.TelnetConsole': None,
#}
# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'proxy_ip.pipelines.ProxyIpPipeline': 300,
}
BASE_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
sys.path.insert(0, os.path.join(BASE_DIR, 'proxy_ip'))
# Enable and configure the AutoThrottle extension (disabled by default)
# See http://doc.scrapy.org/en/latest/topics/autothrottle.html
AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
AUTOTHROTTLE_DEBUG = True
# Enable and configure HTTP caching (disabled by default)
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
MYSQL_HOST = "localhost"
MYSQL_DBNAME = "crawler"
MYSQL_USER = "root"
MYSQL_PASSWORD = "root123"
六、运行测试
在 proxy_ip 项目的根目录下,创建一个 main.py 作为项目运行的入口文件,其中代码如下:
# -*- coding:utf-8 -*-
__author__ = 'jiangzhuolin'
import sys
import os
import time
while True:
os.system("scrapy crawl proxy_ip") # scrapy spider 的启动方法 scrapy crawl spider_name
print("程序开始休眠...")
time.sleep(3600) # 休眠 1 小时后继续爬取
右键 "run main" 查看运行效果
程序运行效果七、总结
我的原来打算是写一篇 scrapy 简单项目的详细介绍,把里面各种细节都通过个人的理解分享出来。但非常遗憾,由于经验不足,导致越写越觉得篇幅会过于冗长。因此里面有大量信息被我简化或者直接没有写出来,本文可能不适合完全小白的新手,如果你写过简单的 scrapy 项目,但对其中的框架不甚理解,我希望能通过本文有所改善。
本项目代码已提交到我个人的 github 与 码云上,如果访问 github 较慢,可以访问码云获取完整代码,其中,包括了创建 MySQL 数据库表的 SQL 代码
github 代码地址:https://github.com/jiangzhuolin/proxy_ip