Scrapy ---- Python开发的一个快速、高层次的屏幕抓取和web抓取框架,用于抓取web站点并从页面中提取结构化的数据。Scrapy吸引人的地方在于它是一个框架,任何人都可以根据需求方便的修改。
创建项目:scrapy startproject xxx(项目名)
进入项目:cd xxx #进入某个文件夹下
创建爬虫:scrapy genspider xxx(爬虫名) xxx.com (爬取域)
生成文件:scrapy crawl xxx -o xxx.json (生成某种类型的文件)
运行爬虫:scrapy crawl XXX
列出所有爬虫:scrapy list
获得配置信息:scrapy settings [options]
Selenium是一个涵盖了一系列工具和库的开源框架,这些工具和库支持Web浏览器的自动化测试。
Selenium框架由多个工具组成,包括: Selenium IDE,Selenium RC,Selenium WebDriver和Selenium Grid。
参考链接:https://blog.csdn.net/wanglian2017/article/details/72843984
# 配置
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless') # 使用headless无界面浏览器模式
# browser = webdriver.Firefox()
browser = webdriver.Chrome()
# browser = webdriver.Chrome(options=chrome_options)
# 打开百度首页
browser.get('http://www.baidu.com/')
# 搜索框输入python,并按下确定键
browser.find_element_by_xpath('//input[@id="kw"]').send_keys("python")
time.sleep(2)
browser.find_element_by_xpath('//input[@type="submit"]').click()
time.sleep(2)
# 截图
browser.save_screenshot("baidu_python.png")
# 关闭浏览器
browser.close()
selenium框架也就是模拟浏览器,相当于您用程序调动浏览器让浏览器打开您需要爬取的网站。
优点:
缺点:
那有没有办法使通过WebDriver驱动的浏览器像正常浏览器一样呢?
在搜索了很久之后,发现了一个办法:通过模拟浏览器驱动控制已经打开的正常浏览器
我们可以利用Chrome DevTools协议。它允许客户检查和调试Chrome浏览器,指定端口和配置文件目录:
chrome.exe --remote-debugging-port=9222 --user-data-dir="C:\selenum\AutomationProfile"
# 对于-remote-debugging-port值,可以指定任何打开的端口。
# 对于-user-data-dir标记,指定创建新Chrome配置文件的目录。为了确保在单独的配置文件中启动chrome,不会污染默认配置文件。
回顾Selenium WebDriver的原理,发现是通过WebDriver来打开浏览器然后绑定到特定端口,那么可以手动绑定我们已经指定好端口(9222)的浏览器的端口:
chrome_options = webdriver.ChromeOptions()
# chrome_options.add_argument('--headless')
chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222") # 绑定端口
browser = webdriver.Chrome(options=chrome_options)
browser.get("https://bot.sannysoft.com/")
time.sleep(10)
# 各种操作
browser.close()
打开网页会发现跟正常浏览器无异,也就可以大胆操作了,当然还是要控制频率或者结合使用动态代理。
关键:在DOWNLOADER_MIDDLEWARES实现自定义请求
编写my_spider.py
class MySpider(scrapy.Spider):
name = "my_spider"
def firefox_init(self):
firefox_options = webdriver.FirefoxOptions()
#firefox_options.add_argument('--headless')
return webdriver.Firefox(options=firefox_options)
def chrome_init(self):
os.system('fuser -k -n tcp %d &' % self.port)
time.sleep(2)
os.system('google-chrome --remote-debugging-port=%d --user-data-dir="/python/spider/data/" --headless &' % (self.port)) #
chrome_options = Options()
chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:%d" % self.port)
return webdriver.Chrome(chrome_options=chrome_options)
def __init__(self):
self.browsers = []
self.port = 0
def start_requests(self):
start_urls = ['http://www.baidu.com']
# 使用空闲的端口,例如9221
self.port = 9221
# 使用chrome浏览器
self.browsers.append(self.chrome_init())
# self.browsers.append(self.firefox_init())
yield Request(start_urls[0], callback=self.parse, meta={'spider_name':'my_spider'}) #
def parse(self, response):
print(response.meta['spider_name'])
# 具体的解析内容
pass
# 重写爬虫close函数
def close(self, spider, reason):
# 关闭浏览器
if self.browsers:
# firefox
# for browser in self.browsers:
# browser.quit()
# chrome
os.system('fuser -k -n tcp %d &' % self.port)
closed = getattr(spider, 'closed', None)
if callable(closed):
return closed(reason)
编写middlewares.py
class SeleniumMiddleware(object):
def process_request(self, request, spider):
# 获取浏览器实例
browser = spider.browsers[0]
# 其他模拟操作,例如 点击 等
page_source = browser.page_source
return HtmlResponse(url=request.url, body=page_source, encoding='utf-8', request=request)
编写settings.py
DOWNLOADER_MIDDLEWARES = {
'teyecrawler.middlewares.SeleniumMiddleware': 543,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': 544,
}
生产者和消费者模式是多线程开发中经常见到的一种模式。生产者的线程专门用来生产一些数据,然后存放到一个中间的变量中。消费者再从这个中间的变量中取出数据进行消费。但是因为要使用中间变量,中间变量经常是一些全局变量,因此需要使用锁来保证数据完整性。
import threading
import random
import time
gMoney = 1000
gLock = threading.Lock()
# 记录生产者生产的次数,达到10次就不再生产
gTimes = 0
class Producer(threading.Thread):
def run(self):
global gMoney
global gLock
global gTimes
while True:
money = random.randint(100, 1000)
gLock.acquire()
# 如果已经达到10次了,就不再生产了
if gTimes >= 10:
gLock.release()
break
gMoney += money
print('%s当前存入%s元钱,剩余%s元钱' % (threading.current_thread(), money, gMoney))
gTimes += 1
time.sleep(0.5)
gLock.release()
class Consumer(threading.Thread):
def run(self):
global gMoney
global gLock
global gTimes
while True:
money = random.randint(100, 500)
gLock.acquire()
if gMoney > money:
gMoney -= money
print('%s当前取出%s元钱,剩余%s元钱' % (threading.current_thread(), money, gMoney))
time.sleep(0.5)
else:
# 如果钱不够了,有可能是已经超过了次数,这时候就判断一下
if gTimes >= 10:
gLock.release()
break
print("%s当前想取%s元钱,剩余%s元钱,不足!" % (threading.current_thread(),money,gMoney))
gLock.release()
def main():
for x in range(5):
Consumer(name='消费者线程%d'%x).start()
for x in range(5):
Producer(name='生产者线程%d'%x).start()
if __name__ == '__main__':
main()
Lock版本的生产者与消费者模式可以正常的运行。但是存在一个不足,在消费者中,总是通过while True死循环并且上锁的方式去判断钱够不够。上锁是一个很耗费CPU资源的行为。因此这种方式不是最好的。
还有一种更好的方式便是使用threading.Condition来实现。
threading.Condition可以在没有数据的时候处于阻塞等待状态。一旦有合适的数据了,还可以使用notify相关的函数来通知其他处于等待状态的线程。这样就可以不用做一些无用的上锁和解锁的操作。可以提高程序的性能。
首先对threading.Condition相关的函数做个介绍,threading.Condition类似threading.Lock,可以在修改全局数据的时候进行上锁,也可以在修改完毕后进行解锁。
import threading
import time
import random
gCondition = threading.Condition()
gMoney = 1000
gTotalTime = 10 # 总生产次数
gCount = 0
class Producer(threading.Thread):
def run(self) -> None:
global gMoney
global gCount
while True:
money = random.randint(100,1000)
gCondition.acquire()
if gCount >= gTotalTime:
gCondition.release()
break
gMoney += money
gCondition.notify_all()
print("%s生产了%d钱,剩余%d钱" % (threading.current_thread(), money, gMoney))
gCount += 1
gCondition.notify_all()
gCondition.release()
time.sleep(1)
class Consumer(threading.Thread):
def run(self) -> None:
global gMoney
global gCount
while True:
money = random.randint(100,1000)
gCondition.acquire()
# 这里要给个while循环判断,因为等轮到这个线程的时候,条件有可能又不满足了
# 我的理解:假如有多个(2个)线程同时判断 gMoney
# 然而第一个执行完后,第二个执行时其实钱不够了,所有要加while,如果是if两个线程都会进入
while gMoney < money:
if gCount >= gTotalTime:
gCondition.release()
return
print("%s要消费%d钱,但只剩余%d钱,不足!" % (threading.current_thread(), money, gMoney))
gCondition.wait()
gMoney -= money
print("%s消费了%d钱,剩余%d钱" % (threading.current_thread(), money, gMoney))
gCondition.release()
time.sleep(1)
def main():
for i in range(3):
t = Consumer(name='消费者线程%d' % i)
t.start()
for i in range(2):
t = Producer(name='生产者线程%d' % i)
t.start()
if __name__ == '__main__':
main()
在线程中,访问一些全局变量,加锁是一个经常的过程。如果你是想把一些数据存储到某个队列中,那么Python内置了一个线程安全的模块叫做queue模块。Python中的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列Queue,LIFO(后入先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。相关的函数如下:
import requests
from lxml import etree
import threading
from queue import Queue
import csv
class BSSpider(threading.Thread):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
}
def __init__(self,page_queue,joke_queue,*args,**kwargs):
super(BSSpider, self).__init__(*args,**kwargs)
self.base_domain = 'http://www.budejie.com'
self.page_queue = page_queue
self.joke_queue = joke_queue
def run(self):
while True:
if self.page_queue.empty():
break
url = self.page_queue.get()
response = requests.get(url, headers=self.headers)
text = response.text
html = etree.HTML(text)
descs = html.xpath("//div[@class='j-r-list-c-desc']")
for desc in descs:
jokes = desc.xpath(".//text()")
joke = "\n".join(jokes).strip()
link = self.base_domain+desc.xpath(".//a/@href")[0]
self.joke_queue.put((joke,link))
print('='*30+"第%s页下载完成!"%url.split('/')[-1]+"="*30)
class BSWriter(threading.Thread):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
}
def __init__(self, joke_queue, writer,gLock, *args, **kwargs):
super(BSWriter, self).__init__(*args, **kwargs)
self.joke_queue = joke_queue
self.writer = writer
self.lock = gLock
def run(self):
while True:
try:
joke_info = self.joke_queue.get(timeout=40)
joke,link = joke_info
self.lock.acquire()
self.writer.writerow((joke,link))
self.lock.release()
print('保存一条')
except:
break
def main():
page_queue = Queue(10)
joke_queue = Queue(500)
gLock = threading.Lock()
fp = open('bsbdj.csv', 'a',newline='', encoding='utf-8')
writer = csv.writer(fp)
writer.writerow(('content', 'link'))
for x in range(1,11):
url = 'http://www.budejie.com/text/%d' % x
page_queue.put(url)
for x in range(5):
t = BSSpider(page_queue,joke_queue)
t.start()
for x in range(5):
t = BSWriter(joke_queue,writer,gLock)
t.start()
if __name__ == '__main__':
main()
scrapy基于twisted异步IO框架,downloader是多线程的。但是,由于python使用GIL(全局解释器锁,保证同时只有一个线程在使用解释器),这极大限制了并行性,在处理运算密集型程序的时候,Python的多线程效果很差,而如果开多个线程进行耗时的IO操作时,Python的多线程才能发挥出更大的作用。(因为Python在进行长时IO操作时会释放GIL)
scrapy中settings.py
# 最大并发数(默认16个)
CONCURRENT_REQUESTS = 32
# Configure a delay for requests for the same website (default: 0)
# See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 3
# The download delay setting will honor only one of:
#CONCURRENT_REQUESTS_PER_DOMAIN = 16 # 针对域名
#CONCURRENT_REQUESTS_PER_IP = 16 # 针对ip
若设置最大并发数并且将 DOWNLOAD_DELAY 设为0,则就是最大同时发出32个请求;若设置了DOWNLOAD_DELAY 并发请求就会受到影响,DOWNLOAD_DELAY 会影响 CONCURRENT_REQUESTS,不能使并发显现出来。
当有CONCURRENT_REQUESTS,没有DOWNLOAD_DELAY 时,服务器会在同一时间收到大量的请求。
当有CONCURRENT_REQUESTS,有DOWNLOAD_DELAY 时,服务器不会在同一时间收到大量的请求。
虽然说downloader就是多线程的,但还是可以尝试在代码中使用多线程
import threading
import time
from concurrent.futures.thread import ThreadPoolExecutor
import requests
from queue import Queue
from lxml import etree
from scrapy import Request
import scrapy
import re
class ProductDetails:
def __init__(self, href, company_brand, market_name):
self.href = href
self.company_brand = company_brand
self.market_name = market_name
class TestSpider(scrapy.Spider):
name = "test_spider"
base_domain = 'https://www.alditalk.de'
start_urls = ['https://www.alditalk.de/smartphones?page=0']
pool = ThreadPoolExecutor(max_workers=3) # 使用线程池
start_time = time.time()
headers = {
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)'
}
def get_request(self, product_details: ProductDetails):
#print(threading.current_thread().name, ': get_request')
res = requests.get(product_details.href,headers=self.headers)
return res.text
def parse_details(self, future):
#print(threading.current_thread().name, ': parse_details')
product_details = future.result()
# brand = product_details.company_brand
# name = product_details.market_name
text = etree.HTML(product_details).xpath('//ul[@class="product-detail-teaser__list"]//li/text()')
# print(text)
time.sleep(2)
#print('brand: ', brand, ', name: ', name)
def parse_details_by_spider(self, response):
self.sleep()
def sleep(self):
time.sleep(2)
def parse(self, response):
products = response.xpath('//div[contains(@class, "product-detail-teaser js-product-container")]')
for product in products:
price = product.xpath('.//span[@class="current-price"]//text()').get()
price = price.replace(',', '.').replace('-', '00').strip().split()[0]
if float(price) > 800 or float(price) < 30:
continue
href = self.base_domain + product.xpath('.//a[@class="product__list--name"]/@href').get()
title = product.xpath('.//a[@class="product__list--name"]/h3/text()').get()
company_brand = title.split()[0]
market_name = re.sub(',.*|Smartphone|\d{1,3}.?GB.*', '', title).replace(' ', ' ').strip()
print(href, company_brand, market_name)
#future = self.pool.submit(self.get_request, ProductDetails(href, company_brand, market_name))
#future.add_done_callback(self.parse_details) # 线程数量3: 30,线程数越多,越快
yield Request(href,callback=self.parse_details_by_spider) # 63s
# next_url = response.xpath('//a[@class="util-icon--after util-icon--arrow-slide-right"]/@href').get()
# if not next_url:
# return
# else:
# yield Request(self.base_domain + '/smartphones' + next_url, callback=self.parse)
def close(self, spider, reason):
self.pool.shutdown(True)
print(time.time() - self.start_time)
参考链接:
Scrapy框架之分布式操作
基于Scrapy分布式爬虫的开发与设计
分布式爬虫概念:多台机器上执行同一个爬虫程序,实现网站数据的分布爬取
1、原生的Scrapy无法实现分布式爬虫的原因?
2、scrapy-redis组件
scrapy-redis是专门为scrapy框架开发的一套组件。该组件可以解决上述两个问题,让Scrapy实现分布式。
pip install scarpy-redis
关键: