共计 12157 个字符,预计需要花费 31 分钟才能阅读完成。
前言
上一篇文章讲了如何应用 scrapy 和 selenium 搭配来爬取数据,这篇文章来写一下如何用 selenium 来爬取应用 Ajax 加载数据的网站并且过掉反爬。
环境配置
本篇文章中所用到的环境都曾经在上篇文章中配置好了,不晓得如何应用的小伙伴能够移步上一篇文章。
开始爬取
antispider1
antispider1 阐明如下:
对接 WebDriver 反爬,检测到应用 WebDriver 就不显示页面,适宜用作 WebDriver 反爬练习。
WebDriver 反爬,阐明应用 selenium 会被检测到。
先应用上篇文章中提到的办法来尝试下。
import scrapy
from scrapy_selenium import SeleniumRequest
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
class AntiSpider(scrapy.Spider):
name = 'antispider1'
def start_requests(self):
urls = ['https://antispider1.scrape.center/']
for a in urls:
yield SeleniumRequest(url=a, callback=self.parse, wait_time=8, wait_until=EC.presence_of_element_located((By.CLASS_NAME, 'm-b-sm')))
def parse(self, response, **kwargs):
print(response.text)
input()
运行代码,selenium 会抛出一个超时异样,因为在指定的工夫内未搜寻到指定的标签,所以报了超时谬误。
selenium.common.exceptions.TimeoutException: Message:
同时查看页面,很显著被检测到了,页面内容都被 JS 删掉了,接下来查找检测点,过掉反爬。
先删除 selenium 期待元素的代码,避免抛异样导致浏览器退出,让程序有限期待在 input 函数上,爬虫不会退出,浏览器也不会被关掉,不便调试。
因为这种反爬检测没有比拟好的动手点,所以间接关上浏览器控制台,全局搜寻字符串 Webdriver Forbidden
,只找到了一处。
看样子是一个三元运算符,通过判断 window.navigator.webdriver
的值来确定是显示反爬界面还是失常加载数据。
执行 window.navigator.webdriver
能够看到它的值为 true
,有两个办法能够批改它的返回值:
- 通过
window.navigator.webdriver = undefined
Object.defineProperties(navigator, {webdriver:{get:()=>undefined}});
在最新版的 Chrome 上测试过这两种办法都曾经生效了,间接赋值的办法尽管执行胜利然而并不能批改返回值,通过批改属性的形式尽管能够批改返回值,然而在新建页面或者拜访一个新的 URL 时 window.navigator.webdriver
会主动变回 true
,须要在每个页面加载前执行才能够,所以问题就变成了如何在页面加载之前执行自定义的命令。
在 selenium 中能够应用 CDP(即 Chrome Devtools-Protocol)Chrome 开发工具协定能够解决这个问题,CDP 命令能够在每个页面加载前加载自定义的代码,在 CDP 中这个命令叫做 Page.addScriptToEvaluateOnNewDocument
。
通过 execute_cdp_cmd
函数执行 CDP 命令,代码为:
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """Object.defineProperty(navigator,'webdriver', {get: () => undefined
})"""
})
这样只须要执行一次,Chrome 就会在每次加载页背后主动执行提前定义好的指令了。
办法有了,如何集成到现有的框架中呢?因为当初应用了第三方包集成 selenium,不能间接批改第三方包的代码,而 driver 对象又主持在第三方包中,咱们可能拿不到这个对象,怎么能力执行 CDP 命令呢?
这时候就须要去翻阅第三方包的代码,看看作者将 driver 对象保留到了哪里,如何能力获取到它。
先看第一个文件(scrapy_selenium/http.py)
class SeleniumRequest(Request):
"""Scrapy ``Request`` subclass providing additional arguments"""
def __init__(self, wait_time=None, wait_until=None, screenshot=False, script=None, *args, **kwargs):
# 为了便于便于查看删除了正文
self.wait_time = wait_time
self.wait_until = wait_until
self.screenshot = screenshot
self.script = script
super().__init__(*args, **kwargs)
只是继承了 scrapy 的 Request 类,是为了不便传递四个参数给到 driver 对象,再来看另一个文件(scrapy_selenium/middlewares.py)
class SeleniumMiddleware:
"""Scrapy middleware handling the requests using selenium"""
def __init__(self, driver_name, driver_executable_path, driver_arguments,
browser_executable_path):
# 为了便于便于查看删除了正文
webdriver_base_path = f'selenium.webdriver.{driver_name}'
driver_klass_module = import_module(f'{webdriver_base_path}.webdriver')
driver_klass = getattr(driver_klass_module, 'WebDriver')
driver_options_module = import_module(f'{webdriver_base_path}.options')
driver_options_klass = getattr(driver_options_module, 'Options')
driver_options = driver_options_klass()
if browser_executable_path:
driver_options.binary_location = browser_executable_path
for argument in driver_arguments:
driver_options.add_argument(argument)
driver_kwargs = {
'executable_path': driver_executable_path,
f'{driver_name}_options': driver_options
}
self.driver = driver_klass(**driver_kwargs)
@classmethod
def from_crawler(cls, crawler):
"""Initialize the middleware with the crawler settings"""
driver_name = crawler.settings.get('SELENIUM_DRIVER_NAME')
driver_executable_path = crawler.settings.get('SELENIUM_DRIVER_EXECUTABLE_PATH')
browser_executable_path = crawler.settings.get('SELENIUM_BROWSER_EXECUTABLE_PATH')
driver_arguments = crawler.settings.get('SELENIUM_DRIVER_ARGUMENTS')
if not driver_name or not driver_executable_path:
raise NotConfigured('SELENIUM_DRIVER_NAME and SELENIUM_DRIVER_EXECUTABLE_PATH must be set')
middleware = cls(
driver_name=driver_name,
driver_executable_path=driver_executable_path,
driver_arguments=driver_arguments,
browser_executable_path=browser_executable_path
)
crawler.signals.connect(middleware.spider_closed, signals.spider_closed)
return middleware
def process_request(self, request, spider):
"""Process a request using the selenium driver if applicable"""
if not isinstance(request, SeleniumRequest):
return None
self.driver.get(request.url)
for cookie_name, cookie_value in request.cookies.items():
self.driver.add_cookie(
{
'name': cookie_name,
'value': cookie_value
}
)
if request.wait_until:
WebDriverWait(self.driver, request.wait_time).until(request.wait_until)
if request.screenshot:
request.meta['screenshot'] = self.driver.get_screenshot_as_png()
if request.script:
self.driver.execute_script(request.script)
body = str.encode(self.driver.page_source)
# Expose the driver via the "meta" attribute
request.meta.update({'driver': self.driver})
return HtmlResponse(
self.driver.current_url,
body=body,
encoding='utf-8',
request=request
)
def spider_closed(self):
"""Shutdown the driver when spider is closed"""
self.driver.quit()
是一个下载中间件的类,代码比拟长,一块一块的看。
先来看 from_crawler 办法,通过配置文件获取到定义好的配置,而后创立以后类的对象,将爬虫敞开的信号连贯到 spider_closed
办法上,在爬虫敞开时及时执行 quit
办法敞开浏览器。
再来看初始化办法,承受四个参数,通过接管到的参数应用 import_module
办法来导入类,最初增加一些参数创立 driver 对象赋值给 self.driver
,到这里就找到了 driver,能够想方法执行 CDP 办法了。
最初就是 process_request
办法,应用 get 办法来获取网页源代码,将 request
对象的 cookie 都增加到 driver 对象中,依据参数值的不同执行不同的动作,期待、截图、执行代码等等,通过 meta 属性公开了 driver 对象,不便在申请完页面数据后应用其余中间件来进行点击、滑动、翻页等等动作,最初返回一个 THML 响应对象。
尽管最初通过 meta 属性公开了 driver 对象,但这是在获取到网页源代码之后了,咱们须要在网页加载前执行对应 CDP 命令才能够。
为了在页面加载之前执行命令,所以咱们须要自定义一个本人的下载中间件,继承 SeleniumMiddleware
类,批改父类初始化办法。
在 middlewares.py
文件中增加以下代码,别忘了导入 SeleniumMiddleware
。
class MyDownloadMiddleware(SeleniumMiddleware):
def __init__(self, driver_name, driver_executable_path, driver_arguments,
browser_executable_path):
super(MyDownloadMiddleware, self).__init__(driver_name, driver_executable_path, driver_arguments,
browser_executable_path)
self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """Object.defineProperty(navigator,'webdriver', {get: () => undefined
})"""
})
先执行 super 办法初始化父类,再应用父类创立好的 driver 对象执行 CDP 命令。别忘了去 settings.py 中批改下载中间件的值为:
DOWNLOADER_MIDDLEWARES = {'learnscrapy.middlewares.MyDownloadMiddleware': 800 # 这里的数值要大一些,因为中间件返回响应后对象后就不会调用后续的下载中间件了}
从新运行爬虫,应该能够看到页面失常加载了,并且网页源代码也能够失常获取到了,之后再补充上具体的解析代码即可。
class AntiSpider(scrapy.Spider):
name = 'antispider1'
def start_requests(self):
urls = ['https://antispider1.scrape.center/']
for a in urls:
yield SeleniumRequest(url=a, callback=self.parse, wait_time=3, wait_until=EC.presence_of_element_located((By.CLASS_NAME, 'm-b-sm')))
def parse(self, response, **kwargs):
result = response.xpath('//div[@class="el-card item m-t is-hover-shadow"]')
for a in result:
item = SSR1ScrapyItem()
item['title'] = a.xpath('.//h2[@class="m-b-sm"]/text()').get()
item['fraction'] = a.xpath('.//p[@class="score m-t-md m-b-n-sm"]/text()').get().strip()
item['country'] = a.xpath('.//div[@class="m-v-sm info"]/span[1]/text()').get()
item['time'] = a.xpath('.//div[@class="m-v-sm info"]/span[3]/text()').get()
item['date'] = a.xpath('.//div[@class="m-v-sm info"][2]/span/text()').get()
url = a.xpath('.//a[@class="name"]/@href').get()
print(response.urljoin(url))
yield SeleniumRequest(url=response.urljoin(url), callback=self.parse_person, meta={'item': item},
wait_time=3, wait_until=EC.presence_of_element_located((By.CLASS_NAME, 'm-b-sm')))
def parse_person(self, response):
item = response.meta['item']
item['director'] = response.xpath('//div[@class="directors el-row"]//p[@class="name text-center m-b-none m-t-xs"]/text()').get()
yield item
残缺代码详见 https://github.com/libra146/learnscrapy/tree/antispider1
其实在页面加载之前执行自定义的 JS 代码还有另外一种办法,那就是 Chrome 拓展,能够应用相似于油猴插件的拓展来实现,限于篇幅问题这里就不演示了。
antispider2
antispider2 阐明如下:
对接 User-Agent 反爬,检测到常见爬虫 User-Agent 就会回绝响应,适宜用作 User-Agent 反爬练习。
既然是 User-Agent 反爬,那么就应用失常的 User-Agent 就能够了,临时不须要用到 selenium。
原本是想用 fake-useragent 的,起初看了下我的项目两年多没更新了,而且不是随机生成,只是从网上下载一些 UA,而后随机选取而已,这样的话没必要引入一个依赖了,本人将 UA 爬下来而后随机取就好了。
在下载中间件中增加以下代码:
class Antispider2DownloaderMiddleware(LearnscrapyDownloaderMiddleware):
def __init__(self):
super(Antispider2DownloaderMiddleware, self).__init__()
with open('ua.json', 'r') as f:
self.ua = json.load(f)
def process_request(self, request, spider):
request.headers.update({'User-Agent': random.choice(self.ua)})
读取本地文件,而后在 process_request
函数中每次随机去一个 UA 更新默认的 UA 即可。
残缺代码详见 https://github.com/libra146/learnscrapy/tree/antispider2
antispider3
antispider3 阐明如下:
对接文字偏移反爬,所见程序并不一定和源码程序统一,适宜用作文字偏移反爬练习。
网站应用到了文字偏移反爬,猜想应该应用了 CSS 管制网页文字的地位来达到反爬的目标。
看了下渲染后的网页源代码,确实是通过扭转 style 的值来使文字产生偏移的,解决办法就是将文字和 style 属性一起获取,而后依照 style 升序排列就能够失去正确的后果: 思维扭转生存
。往下看了看有的文字有偏移有的文字没有偏移,须要在代码里进行判断。
开始写代码,解析 HTML,获取数据,顺便获取对应的 style
,解决后失去程序正确的数据。
class AntiSpider(scrapy.Spider):
name = 'antispider3'
def start_requests(self):
urls = ['https://antispider3.scrape.center/']
for a in urls:
yield SeleniumRequest(url=a, callback=self.parse, wait_time=3, wait_until=EC.presence_of_element_located((By.CLASS_NAME, 'm-b-sm')))
def parse(self, response, **kwargs):
result = response.xpath('//div[@class="el-card__body"]')
for a in result:
item = Antispider3ScrapyItem()
chars = {}
# 有反爬
if r := a.xpath('.//h3[@class="m-b-sm name"]//span'):
for b in r:
chars[b.xpath('.//@style').re(r'\d\d?')[0]] = b.xpath('.//text()').get().strip()
# 先用 sorted 函数来排序,应用 lambda 指定索引值为 0 的值,也就是依据 key 值来排序,排序后应用 zip 函数来将所有的字符串放到
# 同一个元组中,list 函数用来将生成器转成列表,之后应用索引值抉择 title 所在的元组,应用 join 函数连贯所有的字符串即为题目字符串
item['title'] = ''.join(list(zip(*sorted(chars.items(), key=lambda i: i[0])))[1])
else:
# 没有反爬
item['title'] = a.xpath('.//h3[@class="name whole"]/text()').get()
item['author'] = a.xpath('.//p[@class="authors"]/text()').get().strip()
url = a.xpath('.//a/@href').get()
print(response.urljoin(url))
yield SeleniumRequest(url=response.urljoin(url), callback=self.parse_person, meta={'item': item},
wait_time=3, wait_until=EC.presence_of_element_located((By.CLASS_NAME, 'm-b-sm')))
def parse_person(self, response):
item = response.meta['item']
item['price'] = response.xpath('//p[@class="price"]/span/text()').get()
item['time'] = response.xpath('//p[@class="published-at"]/text()').get()
item['press'] = response.xpath('//p[@class="publisher"]/text()').get()
item['page'] = response.xpath('//p[@class="page-number"]/text()').get()
item['isbm'] = response.xpath('//p[@class="isbn"]/text()').get()
yield item
通过判断对应元素是否存在的形式来判断 title 是否被反爬,在判断分支中进行不同的解决。
因为工夫关系代码中只爬取了一页数据,证实办法可行就能够。
有个插曲,其实这个网站的数据也是通过 Ajax 申请的,也就是说间接从接口申请就能够获取到数据,不必解决反爬措施,这里是为了学习文字偏移反爬才从 HTML 中获取数据。
残缺代码详见 https://github.com/libra146/learnscrapy/tree/antispider3
antispider4
antispider4 阐明如下:
对接字体文件反爬,显示的内容并不在 HTML 内,而是暗藏在字体文件,设置了文字映射表,适宜用作字体反爬练习。
字体反爬,这种状况下须要先找到字体映射表,并且解析字体映射表中的文字和代码的对应关系才能够失常爬取。
然而我在看到这里之后我发现这如同不是字体反爬????(尽管这个网站确实有一个独自的字体文件),数字内容被放在了 CSS 样式表文件中,尽管我是第一次见到这种反爬措施,然而我认为叫它 CSS 反爬如同更正当一些。
不晓得是不是作者搞错了的起因,这里暂且当作 CSS 反爬来解决吧。
这种反爬措施须要将 HTML 源码中对应数字的 class 的值都抓进去,而后将 CSS 文件中对应的 value 替换就能够了,所以首先须要解决的是 CSS 文件,而不是 HTML。
查了下,这种应用形式叫隐式 Style–CSS.
CSS 中,::before 创立一个伪元素,其将成为匹配选中的元素的第一个子元素。常通过 content 属性来为一个元素增加润饰性的内容。
class AntiSpider(scrapy.Spider):
name = 'antispider4'
css = {}
def start_requests(self):
urls = ['https://antispider4.scrape.center/css/app.654ba59e.css']
for a in urls:
# 解析 css
yield Request(url=a, callback=self.parse_css)
def parse_css(self, response):
# 依据法则应用正则找到所有须要用到的属性,因为这里只反爬了分数,所以只须要匹配大量的数字和点即可。result = re.findall(r'\.(icon-\d*?):before{content:"(.*?)"}', response.text)
for key, value in result:
self.css[key] = value
print(self.css)
# 拜访主页
yield SeleniumRequest(url='https://antispider4.scrape.center/', callback=self.parse_data,
wait_time=3, wait_until=EC.presence_of_element_located((By.CLASS_NAME, 'm-b-sm')))
def parse_data(self, response):
result = response.xpath('//div[@class="el-card item m-t is-hover-shadow"]')
for a in result:
item = Antispider4ScrapyItem()
item['title'] = a.xpath('.//h2[@class="m-b-sm"]/text()').get()
if r := a.xpath('.//p[@class="score m-t-md m-b-n-sm"]//i'):
item['fraction'] = ''.join([self.css.get(b.xpath('.//@class').get()[5:],'') for b in r])
item['country'] = a.xpath('.//div[@class="m-v-sm info"]/span[1]/text()').get()
item['time'] = a.xpath('.//div[@class="m-v-sm info"]/span[3]/text()').get()
item['date'] = a.xpath('.//div[@class="m-v-sm info"][2]/span/text()').get()
url = a.xpath('.//a[@class="name"]/@href').get()
print(response.urljoin(url))
yield SeleniumRequest(url=response.urljoin(url), callback=self.parse, meta={'item': item},
wait_time=3, wait_until=EC.presence_of_element_located((By.CLASS_NAME, 'm-b-sm')))
def parse(self, response, **kwargs):
item = response.meta['item']
item['director'] = response.xpath('//div[@class="directors el-row"]//p[@class="name text-center m-b-none m-t-xs"]/text()').get()
yield item
解决形式就是先将 CSS 文件中所须要用到的内容应用正则匹配进去,在须要替换的中央间接替换就能够失去正确的分数数据了。
残缺代码详见 https://github.com/libra146/learnscrapy/tree/antispider4
总结
本篇文章只写针对 selenium 呈现的各种反爬措施,针对 IP 地址或者账号进行的反爬的内容下篇文章来写。
网页获取数据的形式无外乎就那几种,HTML,JS,CSS,Ajax 等,所以在遇到反爬时先找数据是怎么被渲染进去的,剩下的问题就是解决数据,依据数据的起源进行针对性的解决。