关于python:scrapy学习之爬虫练习平台4

30次阅读

共计 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 等,所以在遇到反爬时先找数据是怎么被渲染进去的,剩下的问题就是解决数据,依据数据的起源进行针对性的解决。

正文完
 0