items.py
文件中可以看到scrapy startproject fang
cd fang
命令两次,再输入cd spiders
命令,进入爬虫文件夹scrapy genspider sfw fang.com
创建爬虫文件settings.py
文件ROBOTSTXT_OBEY = False
DOWNLOAD_DELAY =
3middlewares.py
中书写(下面有)DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en'
}
下载器中间件和管道文件后期代码书写完成之后再打开
items.py
文件此文件中写要存储的数据
# 新房信息
class NewHouseItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
# 省份
province = scrapy.Field()
# 城市
city = scrapy.Field()
# 小区的名字
name = scrapy.Field()
# 价格
price = scrapy.Field()
# 几居, 这是个列表
rooms = scrapy.Field()
# 面积
area = scrapy.Field()
# 地址
address = scrapy.Field()
# 行政区
district = scrapy.Field()
# 是否在售
sale = scrapy.Field()
# 房天下详情页面的url
origin_url = scrapy.Field()
# 二手房信息
class ESFHouseItem(scrapy.Item):
# 省份
province = scrapy.Field()
# 城市
city = scrapy.Field()
# 小区名字
name = scrapy.Field()
# # 几室几厅
# rooms = scrapy.Field()
# # 层
# floor = scrapy.Field()
# # 朝向
# toward = scrapy.Field()
# # 年代
# year = scrapy.Field()
# 地址
address = scrapy.Field()
# # 建筑面积
# # area = scrapy.Field()
# 总价
price = scrapy.Field()
# 单价
unit = scrapy.Field()
# 原始的url
origin_url = scrapy.Field()
# 信息
infos = scrapy.Field()
sfw.py
文件(爬虫文件)主要的爬虫代码写在此文件中
import scrapy
import re
from fang.items import NewHouseItem, ESFHouseItem
class SfwSpider(scrapy.Spider):
name = 'sfw'
allowed_domains = ['fang.com']
start_urls = ['https://www.fang.com/SoufunFamily.htm']
def parse(self, response):
# 所有城市标签
trs = response.xpath("//div[@class = 'outCont']//tr")
province = None
# 遍历得到每一行的数据
for tr in trs:
# 获取省份和对应城市的两个td标签
tds = tr.xpath(".//td[not(@class)]")
# 省份名称
province_text = tds[0]
# 省份对应的城市名称及链接
city_info = tds[1]
# 提取省份名称
province_text = province_text.xpath(".//text()").get()
province_text = re.sub(r"\s", "", province_text)
if province_text:
province = province_text
# 不爬取海外房产
if province == "其它":
continue
# 提取城市名称及链接
city_links = city_info.xpath(".//a")
for city_link in city_links:
# 获取城市
city = city_link.xpath(".//text()").get()
# 获取城市链接
city_url = city_link.xpath(".//@href").get()
# 构建新房链接
url_split = city_url.split("fang")
url_former = url_split[0]
url_backer = url_split[1]
newhouse_url = url_former + "newhouse.fang.com/house/s/"
# 构建二手房链接
esf_url = url_former + "esf.fang.com/"
# print("++" * 20)
# print("省份:", province)
# print("城市:", city)
# print("新房链接:", newhouse_url)
# print("二手房链接:", esf_url)
# print("++" * 20)
# 返回新房信息再解析
yield scrapy.Request(url=newhouse_url, callback=self.parse_newhouse, meta={"info": (province, city)})
# 返回二手房信息再解析
yield scrapy.Request(url=esf_url, callback=self.parse_esf, meta = {"info": (province, city)})
# 新房页面解析
def parse_newhouse(self, response):
province, city = response.meta.get("info")
lis = response.xpath("//div[contains(@class, 'nl_con')]/ul/li[not(@style)]")
for li in lis:
# 获取房产名字
name = li.xpath(".//div[@class='nlcd_name']/a/text()").get().strip()
# 获取几居室
rooms = li.xpath(".//div[contains(@class, 'house_type')]/a//text()").getall()
# 获取面积
area = li.xpath(".//div[contains(@class, 'house_type')]/text()").getall()
area = "".join(area).strip()
area = re.sub(r"/|-|/s| |\n", "", area)
# 获取地址
address = li.xpath(".//div[@class = 'address']/a/@title").get()
# 获取是哪个区的房子
district = li.xpath(".//div[@class = 'address']/a//text()").getall()
district = "".join(district)
district = re.search(r".*\[(.+)\].*", district).group(1)
# 获取是否在售
sale = li.xpath(".//div[contains(@class, 'fangyuan')]/span/text()").get()
# 获取价格
price = li.xpath(".//div[@class = 'nhouse_price']//text()").getall()
price = "".join(price).strip()
# 获取详情页url
origin_url = li.xpath(".//div[@class = 'nlcd_name']/a/@href").get()
# 构建item返回
item = NewHouseItem(province = province, city = city, name = name, rooms = rooms, area = area, address = address, district = district, sale = sale, price = price, origin_url = origin_url)
yield item
# 爬取下一页数据
next_url = response.xpath("//div[@class = 'page']//a[@class = 'next']/@href").get()
if next_url:
yield scrapy.Request(url=response.urljoin(next_url), callback=self.parse_newhouse, meta={"info": (province, city)})
# 二手房页面解析
def parse_esf(self, response):
province, city = response.meta.get("info")
dls = response.xpath("//div[contains(@class, 'shop_list')]/dl[@dataflag = 'bg']")
for dl in dls:
item = ESFHouseItem(province = province, city = city)
# 房子名字
name = dl.xpath(".//p[@class = 'add_shop']/a/@title").get()
item["name"] = name
# 信息(几室几厅(rooms),面积(area), 层(floor), 朝向(toward), 年代(year))
infos = dl.xpath(".//p[@class = 'tel_shop']/text()").getall()
infos = "".join(infos).strip()
infos = re.sub(r"'|\|\r|\n|/s| ", "", infos)
item['infos'] = infos
# 地址
address = dl.xpath(".//p[@class = 'add_shop']/span/text()").get()
item['address'] = address
# 价格
price = dl.xpath(".//dd[@class = 'price_right']/span[1]//text()").getall()
price = "".join(price)
item['price'] = price
# 均价
unit = dl.xpath(".//dd[@class = 'price_right']/span[2]/text()").get()
item['unit'] = unit
# 原始url
origin_url = dl.xpath(".//h4[@class = 'clearfix']/a/@href").getall()
origin_url = "".join(origin_url)
origin_url = response.urljoin(origin_url)
item['origin_url'] = origin_url
yield item
# 下一页url
next_url = response.xpath("//div[@class = 'page_al']/p[last()-1]/a/@href").get()
if next_url:
yield scrapy.Request(url=response.urljoin(next_url), callback=self.parse_esf, meta={"info": (province, city)})
if __name__ == '__main__':
from scrapy import cmdline
args = "scrapy crawl sfw".split()
cmdline.execute(args)
爬虫文件最后四行main代码是设置的快捷运行爬虫代码,不用再使用命令行输入命令启动爬虫,直接右键运行即可。
pipelines.py
)爬虫文件中有两个函数,最后分别将爬取到的新房和二手房数据通过yield
一条一条返回到pipelines.py
中,pipelines.py
是数据存储文件,将数据存储为Json文件、存储进MySQL数据库、存储进MongoDB数据库均是在这个文件中。
from scrapy.exporters import JsonLinesItemExporter
from fang.items import NewHouseItem, ESFHouseItem
class FangePipeline(object):
def __init__(self):
self.newhouse_fp = open("newhouse.json", "wb")
self.esfhouse_fp = open("esfhouse.json", "wb")
self.newhouse_exporter = JsonLinesItemExporter(self.newhouse_fp, ensure_ascii = False)
self.esfhouse_exporter = JsonLinesItemExporter(self.esfhouse_fp, ensure_ascii=False)
def process_item(self, item, spider):
# 判断返回的item和items.py文件中定义的item类型是否一致
if isinstance(item, NewHouseItem):
self.newhouse_exporter.export_item(item)
else:
self.esfhouse_exporter.export_item(item)
return item
def close_spider(self, spider):
self.newhouse_fp.close()
self.esfhouse_fp.close()
当代码写完后,在设置中把管道注释打开即可,如下所示:
ITEM_PIPELINES = {
# 存储为Json文件管道
'fang.pipelines.FangePipeline': 300,
# 存储为MySQL同步操作管道
# 'fang.pipelines.MysqlSavePipline_1': 300,
# 存储为MySQL异步操作管道
# 'fang.pipelines.MysqlSavePipline_2': 300,
# 存入到MongoDB
# 'fang.pipelines.MongodbPipline': 300,
}
切换到爬虫文件,右键运行即可获取到Json数据。
—————————————————————————————————————————————
附加内容:
将数据存入Json文件常用的有两种方法:
一种是本文件中使用的JsonLinesItemExporter
;
还有一种是JsonItemExporter
。
前一种方法是将数据逐条写入文件中,后一种方法是将数据一起写入文件中,当数据量大的时候,后一种方法十分吃内存,但是后一种方法是标准的Json格式,各有优缺点。
pipelines.py
)数据存入MySQL分为同步操作和异步操作两种。
# 存入MySQL数据库(同步操作)
import pymysql
class MysqlSavePipline_1(object):
def __init__(self):
# 建立连接 后面参数分别为:主机, MySQL用户名, MySQL密码, 哪一个数据库
self.conn = pymysql.connect("localhost", "root", "123456", "fang")
# 创建游标
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
# 判断返回的item和items.py文件中定义的item类型是否一致
if isinstance(item, NewHouseItem):
# 新房sql语句
insert_sql = """
insert into newhouse(province, city, name, price, rooms, area, address, district, sale, origin_url) values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
# 执行插入数据库的操作
self.cursor.execute(insert_sql, (item['province'], item['city'], item['name'], item['price'], item['rooms'], item['area'], item['address'], item['district'], item['sale'], item['origin_url']))
if isinstance(item, ESFHouseItem):
# 二手房sql语句
insert_sql = """
insert into esfhouse(province, city, name, address, price, unit, origin_url, infos) values (%s, %s, %s, %s, %s, %s, %s, %s)
"""
# 执行s插入数据库操作
self.cursor.execute(insert_sql, (item['province'], item['city'], item['name'], item['address'], item['price'], item['unit'], item['origin_url'], item['infos']))
# 提交,不进行提交保存不到数据库
self.conn.commit()
def close_spider(self, spider):
# 关闭游标和连接
self.cursor.close()
self.conn.close()
写完代码后,在设置文件中将管道注释打开即可,如下所示:
ITEM_PIPELINES = {
# 存储为Json文件管道
# 'fang.pipelines.FangePipeline': 300,
# 存储为MySQL同步操作管道
'fang.pipelines.MysqlSavePipline_1': 300,
# 存储为MySQL异步操作管道
# 'fang.pipelines.MysqlSavePipline_2': 300,
# 存入到MongoDB
# 'fang.pipelines.MongodbPipline': 300,
}
切换到爬虫文件,右键运行后即可以同步方式存储数据。
# 存入MySQL数据库(异步操作)
import pymysql
from twisted.enterprise import adbapi
class MysqlSavePipline_2(object):
def __init__(self, dbpool):
self.dbpool = dbpool
@classmethod
# 函数名固定,会被scrapy调用,直接可用settings的值
def from_settings(cls, settings):
"""
数据库建立连接
:param settings:配置参数
:return:实例化参数
"""
adbparams = dict(
host = settings['MYSQL_HOST'],
db = settings['MYSQL_DBNAME'],
user = settings['MYSQL_USER'],
password = settings['MYSQL_PASSWORD'],
# 指定cursor类型
cursorclass = pymysql.cursors.DictCursor
)
# 连接数据池ConnectionPool,使用pymysql连接
dbpool = adbapi.ConnectionPool('pymysql', **adbparams)
# 返回实例化参数
return cls(dbpool)
def process_item(self, item, spider):
"""
使用twisted将MySQL插入变成异步执行。通过连接池执行具体的sql操作,返回一个对象
:param item:
:param spider:
:return:
"""
# 指定操作方法和操作数据
query = self.dbpool.runInteraction(self.do_insert, item)
# 添加异常处理
query.addCallback(self.handle_error)
def do_insert(self, cursor, item):
# 对数据库执行插入操作,并不需要commit,twisted会自动commit
# 首先判断应该插入哪一张表
if isinstance(item, NewHouseItem):
insert_sql = """
insert into newhouse(province, city, name, price, rooms, area, address, district, sale, origin_url) values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_sql, (item['province'], item['city'], item['name'], item['price'], item['rooms'], item['area'], item['address'], item['district'], item['sale'], item['origin_url']))
if isinstance(item, ESFHouseItem):
insert_sql = """
insert into esfhouse(province, city, name, address, price, unit, origin_url, infos) values (%s, %s, %s, %s, %s, %s, %s, %s)
"""
# 执行s插入数据库操作
cursor.execute(insert_sql, (item['province'], item['city'], item['name'], item['address'], item['price'], item['unit'], item['origin_url'], item['infos']))
def handle_error(self,failure):
if failure:
# 打印错误信息
print(failure)
代码书写完成后,在设置文件中打开管道注释,如下所示:
ITEM_PIPELINES = {
# 存储为Json文件管道
# 'fang.pipelines.FangePipeline': 300,
# 存储为MySQL同步操作管道
# 'fang.pipelines.MysqlSavePipline_1': 300,
# 存储为MySQL异步操作管道
'fang.pipelines.MysqlSavePipline_2': 300,
# 存入到MongoDB
# 'fang.pipelines.MongodbPipline': 300,
}
切换到爬虫文件,右键运行后即可以异步方式存储数据。
pipelines.py
)首先在settings.py
文件中添加数据库配置信息:
# 数据库配置信息
MYSQL_HOST = 'localhost'
MYSQL_DBNAME = 'fang'
MYSQL_USER = 'root'
MYSQL_PASSWORD = '123456'
然后在piplines.py
文件中书写存储代码:
# 存入MongoDB数据库
import pymongo
class MongodbPipline(object):
def __init__(self):
# 建立数据库连接
client = pymongo.MongoClient('127.0.0.1', 27017)
# 连接所需数据库, fang为数据库名字
db = client['fang']
# 连接所用集合,也就是通常所说的表,newhouse为表名
self.post_newhouse = db['newhouse'] # 新房
self.post_esfhouse = db['esfhouse'] # 二手房
def process_item(self, item, spider):
if isinstance(item, NewHouseItem):
# 把item转化为字典形式
postItem = dict(item)
# 向数据库插入一条记录
self.post_newhouse.insert(postItem)
if isinstance(item, ESFHouseItem):
# 把item转化为字典形式
postItem = dict(item)
# 向数据库插入一条记录
self.post_esfhouse.insert(postItem)
代码书写完成后,在设置文件中打开管道注释,如下所示:
ITEM_PIPELINES = {
# 存储为Json文件管道
# 'fang.pipelines.FangePipeline': 300,
# 存储为MySQL同步操作管道
# 'fang.pipelines.MysqlSavePipline_1': 300,
# 存储为MySQL异步操作管道
# 'fang.pipelines.MysqlSavePipline_2': 300,
# 存入到MongoDB
'fang.pipelines.MongodbPipline': 300,
}
切换到爬虫文件,右键运行后即可将数据存储到MongoDB数据库。
middlewares.py
)# 设置随机请求头
class UserAgentDownloadMiddleware(object):
# User-Agent中的请求头
User_Agents = [
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0",
"Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; InfoPath.3; rv:11.0) like Gecko",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
"Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
"Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11",
"Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
"Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
"Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
"Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
"Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22; CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Opera/9.80 (Android 2.3.4; Linux; Opera Mobi/build-1107180945; U; en-GB) Presto/2.8.149 Version/11.10",
"Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13",
"Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, like Gecko) Version/6.0.0.337 Mobile Safari/534.1+",
"Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.70 Safari/534.6 TouchPad/1.0",
"Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/20.0.019; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.18124",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; HTC; Titan)",
"UCWEB7.0.2.37/28/999",
"NOKIA5700/ UCWEB7.0.2.37/28/999",
"Openwave/ UCWEB7.0.2.37/28/999",
"Mozilla/4.0 (compatible; MSIE 6.0; ) Opera/UCWEB7.0.2.37/28/999"
]
# 定义函数随机获取一个请求头
def process_request(self, request, spider):
user_agent = random.choice(self.User_Agents)
request.headers['User-Agent'] = user_agent
# 设置ip代理
class IPProxyDownloadMiddleware(object):
# 在代理平台购买代理,填写信息后会返回一个链接,把链接中的ip代理复制过来
proxies = ["178.44.170.152:8000", "110.44.113.182:8080", "209.126.124.73:8888", "84.42.79.243:8080","42.104.84.106:8080","117.64.234.7:808", "103.76.199.166:8080"]
# 定义函数一次选择一个ip
def process_request(self, request, spider):
proxy = random.choice(self.proxies)
request.meta["proxy"] = proxy
写完代码后记得在设置中添加中间件。
import base64
# 设置独享代理ip
class DuXiangIPProxyDownloadMiddleware(object):
def process_request(self, request, spider):
# 购买独享代理的ip地址以及端口号
proxy = "独享代理ip地址 : 端口号"
# 购买独享代理的账号和密码
user_password = "账号 : 密码"
request.meta["proxy"] = proxy
# 把密码设置进去,先进行base64转换. b64_user_password需要是bytes数据类型,而user_password是Unicode(str)类型,所以需要先编码。
b64_user_password = base64.b64encode(user_password.encode('utf-8'))
# 设置在请求头中. Basic是str数据类型,b64_user_password是bytes数据类型,所以首先解码
request.headers["Proxy-Authorization"] = "Basic " + b64_user_password.decode('utf-8')
写完代码后记得在设置中添加中间件。
Redis
地址
:https://github.com/MicrosoftArchive/redis/releases
Redis
cmd
命令窗口Redis
文件夹目录redis-server.exe redis.windows.conf
即可打开。(此条命令分别是Redis
目录下的两个文件)cmd
,输入命令redis-cli
即可连接Redis
Redis
文件中的这个文件redis.windows.conf
中的bind
后面的ip
改为Redis
所在服务器的ip
Redis
的主机),输入代码连接redis-cli -h [Redis所在服务器的ip] -p 6379
Scrapy
单机爬虫中有一个本地爬取队列Queue
,这个队列是利用deque
模块实现的。如果新的Request
生成就会放到队列里面,随后Request
被Scheduler
调度。之后,Request
交给Downloader
执行爬取,简单的调度架构如下图所示。
如果两个Scheduler
同时从队列里面取Request
,每个Scheduler
都有其对应的Downloader
,那么在带宽足够、正常爬取且不考虑队列存取压力的情况下,爬取效率会有什么变化?没错,爬取效率会翻倍。
这样,Scheduler
可以扩展多个,Downloader
也可以扩展多个。而爬取队列Queue
必须始终为一个,也就是所谓的共享爬取队列。这样才能保证Scheduer
从队列里调度某个Request
之后,其他Scheduler
不会重复调度此Request
,就可以做到多个Schduler
同步爬取。这就是分布式爬虫的基本雏形,简单调度架构如下图所示。
我们需要做的就是在多台主机上同时运行爬虫任务协同爬取,而协同爬取的前提就是共享爬取队列。这样各台主机就不需要各自维护爬取队列,而是从共享爬取队列存取Request
。但是各台主机还是有各自的Scheduler
和Downloader
,所以调度和下载功能分别完成。如果不考虑队列存取性能消耗,爬取效率还是会成倍提高。
那么这个队列用什么来维护?首先需要考虑的就是性能问题。我们自然想到的是基于内存存储的Redis,它支持多种数据结构,例如列表(List)、集合(Set)、有序集合(Sorted Set)等,存取的操作也非常简单。
Redis
支持的这几种数据结构存储各有优点。
lpush()
、lpop()
、rpush()
、rpop()
方法,我们可以用它来实现先进先出式爬取队列,也可以实现先进后出栈式爬取队列。Scrapy
的Request
也有优先级的控制,我们可以用它来实现带优先级调度的队列。我们需要根据具体爬虫的需求来灵活选择不同的队列。
Scrapy
有自动去重,它的去重使用了Python
中的集合。这个集合记录了Scrapy
中每个Request
的指纹,这个指纹实际上就是Request
的散列值。
Scrapy
源代码中的request_fingerprint()
就是计算Request指纹的方法,其方法内部使用的是hashlib
的sha1()
方法。计算的字段包括Request的Method、URL、Body、Headers这几部分内容,这里只要有一点不同,那么计算的结果就不同。计算得到的结果是加密后的字符串,也就是指纹。每个Request都有独有的指纹,指纹就是一个字符串,判定字符串是否重复比判定Request对象是否重复容易得多,所以指纹可以作为判定Request是否重复的依据。
在Scrapy
中,爬虫运行时的Request队列放在内存中。爬虫运行中断后,这个队列的空间就被释放,此队列就被销毁了。所以一旦爬虫运行中断,爬虫再次运行就相当于全新的爬取过程。
要做到中断后继续爬取,我们可以将队列中的Request
保存起来,下次爬取直接读取保存数据即可获取上次爬取的队列。我们在Scrapy
中指定一个爬取队列的存储路径即可,这个路径使用JOB_DIR
变量来标识,我们可以用如下命令来实现:
scrapy crawl spider -s JOB_DIR=crawls/spider
更加详细的使用方法可以参见官方文档,链接为:https://doc.scrapy.org/en/latest/topics/jobs.html
在Scrapy
中,我们实际是把爬取队列保存到本地,第二次爬取直接读取并恢复队列即可。那么在分布式架构中我们还用担心这个问题吗?不需要。因为爬取队列本身就是用数据库保存的,如果爬虫中断了,数据库中的Request依然是存在的,下次启动就会接着上次中断的地方继续爬取。
所以,当Redis
的队列为空时,爬虫会重新爬取;当Redis
的队列不为空时,爬虫便会接着上次中断之处继续爬取。
Centos8
下Python3
安装配置先升级
su - # 切换到root用户,设置必须在root下完成*
gcc --version # 查看有没安装gcc*
yum install -y update # 升级
2. 再安装 `gcc`和`make`插件
yum install gcc gcc-c++
yum -y install gcc automake autoconf libtool make
yum groupinstall -y 'Development Tools'
yum install -y gcc openssl-devel bzip2-devel libffi-devel
python3.8.2
(Centos8
自带有Python3.6
,可视情况安装)
python3.8.2
wget https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tgz
解压缩:
tar -zxvf Python-3.8.1.tgz
2. 用脚本检验整个编译环境
./configure prefix=/usr/local/python3 --enable-optimizations
3. 用make命令编译安装
make && make install
4. 修改环境变量
export PATH=$PATH:/usr/local/python3/bin/
5. 安装PIP
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3.8 get-pip.py
打开电脑cmd
命令行,进入到项目目录,输入如下命令,会在项目中生成一个txt
文件,里面有项目所有需要的包。
pip freeze > requirments.txt
Centos8
安装Python
虚拟环境pip3 install virtualenv -i https://mirrors.aliyun.com/pypi/simple/
2. 安装`virtualenvwrapper`工具管理虚拟环境
pip3 install virtualenvwrapper -i https://mirrors.aliyun.com/pypi/simple/
3. 创建目录用来存放虚拟环境
mkdir $HOME/.virtualenvs
4. 进入`root`用户,修改`.bashrc` 文件
su - # 进入root用户
work空格+两个Tab键 # 进入目录
# vi ~/.bashrc # 编辑.bashrc文件
5. 在`.bashrc`文件中进入编辑模式,再添加如下代码(不同系统记得改路径)
export WORKON_HOME=$HOME/.virtualenvs # 虚拟环境文件位置
source /usr/local/bin/virtualenvwrapper.sh # 虚拟环境管理工具的位置
VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3.6 # 指定使用的解释器目录
6. 运行
source ~/.bashrc
mkvirtualenv [虚拟环境名称]
workon [虚拟环境名称]
退出虚拟环境:
deactivate
删除虚拟环境(慎用):
rmvirtualenv
[虚拟环境名称]
注意:在虚拟环境下安装包
进入虚拟环境后,输入如下命令(记得一定要带上 -r):
pip install -r requirment.txt
如果提示找不到pywin32
这个包,就打开文件删除这个包名字之后,再重新安装,因为这个包是Windows
独有的,Linux
系统不用安装。
Scrapy-Redis
步骤scrapy-redis
包:pip install scrapy-redis
from scrapy_redis.spiders import RedisSpider
scrapy.Spider
变成RedisSpider
;或者是从scrapy.CrawlSpider
变成RedisCrawlSpider
。start_urls
删掉或注释,增加一个redis_key = "xxx"
。这个redis_key
是为了以后在redis
中控制爬虫启动的,爬虫的第一个url
,就是在redis
中通过这个发送出去的。例如:redis_key = "fang:start_url"
settings.py
配置文件中增加以下配置:# Scrapy-Redis相关配置
# 确保request存储到redis中
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 确保所有的爬虫共享相同的去重指纹
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 设置redis为item pipeline(数据保存在了redis服务器上)
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 300
}
# 在redis中保持scrapy-redis用到的队列,不会清理redis中的队列,从而可以实现暂停和恢复的功能
SCHEDULER_PERSIST = True
# 设置连接Redis信息
REDIS_HOST = '192.168.x.x' # Redis服务器的主机地址(安装Redis的电脑的ip)
REDIS_PORT = 6379
上面代码写入scrapy
的settings.py
文件后,记得把原来的ITEM_PIPELINES
给注释掉。
1. 把项目在自己电脑压缩,使用命令rz命令上传到服务器(上传到虚拟环境)
2. 上传后在服务器中进行解压:unzip fang.zip
3. 进入项目中爬虫所在文件夹:使用cd命令
4. 运行爬虫
在爬虫服务器上,进入爬虫文件所在的路径,然后输入命令:scrapy runspider [爬虫名字]
在Redis
服务器上,推入一个开始的url
链接:redis-cli > lpush [redis_key] start_url
开始爬取