速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

基于 Scrapy 的爬虫解决方案

  • 2021-06-23
  • 本文字数:4948 字

    阅读完需:约 16 分钟

基于Scrapy的爬虫解决方案

一、背景介绍


笔者在业务中遇到了爬虫需求,由于之前没做过相关的活儿,所以从网上调研了很多内容。但是互联网上的信息比较杂乱,且真真假假,特别不方便,所以完成业务后就想写一篇对初学者友好且较为完整的文章,希望能对阅读者有所帮助。


由于笔者最近 Python 用得比较熟练,所以就想用 Python 语言来完成这个任务。经过一番调研,发现 Scrapy 框架使用者比较多,文档也比较全,所以选择了使用该框架。(其实 Scrapy 只做了非常简单的封装,对于普通的爬虫任务,使用 requests 库和 bs4 库中的 BeautifulSoup 类就完全能解决了)。


首先简单介绍一下爬虫是什么。爬虫就是从一个或多个 URL 链接开始,使用某种方法(例如 requests 库中的函数)获取到该 URL 对应的网页的内容(一般是 HTML 格式),然后从该网页的内容中提取出需要记录下来的信息和需要继续爬取的 URL 链接(例如使用上文中提到的 BeautifulSoup 类)。之后,再对爬取到的 URL 链接进行上述同样的操作,直到所有 URL 链接都被爬取完,爬虫程序结束。


Scrapy 的官网【1】,英文版官方文档【2】,第三方的汉化文档(较为简陋和过时)【3】提供如下,感兴趣的读者也可以自行查阅。由于本文重点不在这里,就不在此处对 Scrapy 进行介绍了。


【1】:https://scrapy.org/

【2】:https://docs.scrapy.org/en/latest/

【3】:https://scrapy-chs.readthedocs.io/zh_CN/0.24/index.html


二、Scrapy 使用方法


安装 Scrapy 库


pip install scrapy
复制代码


新建一个爬虫项目


scrapy startproject your_project_name
复制代码


输入该命令后,会在当前目录下新建一个名为 your_project_name 的文件夹,该文件夹下的文件层级关系如下:


your_project_name|    scrapy.cfg|----your_project_name|    |    __init__.py|    |    items.py|    |    middlewares.py|    |    pipelines.py|    |    settings.py|    |----spiders|    |    |    __init__.py
复制代码


其中,scrapy.cfg 是整个项目的配置文件,spiders 目录下存放爬虫的逻辑代码,因为该项目刚建立,还没有写具体的爬虫代码,所以该目录下为空。


生成一个爬虫


在刚刚新建的项目目录下输入命令:


scrapy genspider example www.qq.com
复制代码


其中 example 是爬虫的名字,www.qq.com 是该爬虫的第一个要爬取的 URL 链接。


执行该命令后,Scrapy 会在 spiders 目录下生成一个叫 example.py 的文件,该文件是一个非常基础的爬虫模板。之后要做的事情就是在该 py 文件里填入具体的爬虫逻辑代码,然后再执行该爬虫脚本就可以了。example.py 文件内的代码如下:


import scrapy

class ExampleSpider(scrapy.Spider): name = 'example' allowed_domains = ['qq.com'] start_urls = ['http://qq.com/']
def parse(self, response): pass
复制代码


代码中的 ExampleSpider 就是刚才生成的爬虫类。其中,name 是爬虫的名字,allowed_domains 是对域名的限制(即该爬虫只会爬取该限制下的 URL 域名),start_urls 是爬虫的初始 URL 链接,这里面的值是刚才创建爬虫时输入的 URL 链接,parse 函数是默认的解析函数。


运行爬虫


在项目目录下执行命令:


scrapy crawl example


其中 example 是要运行的爬虫名字。执行该命令后,该框架就会用 example 爬虫里定义的初始 URL 链接和解析函数去爬取网页了。


调试爬虫


在写代码的过程中,由于不同网页的源码的组织方式不同,所以需要用一种交互式的方式来访问网页,以此来修改代码。虽然在很多情况下可以通过 Chrome 浏览器 F12 的审查模式来查看网页的 HTML 源码,但是在有些情况下代码中获得的源码和浏览器中看到的却是不一样的,所以交互式访问网页就必不可少了。(也可以通过运行完整爬虫的方式来调试代码,但是效率就有点低下了)。


要想交互式访问网页,需要在项目目录下执行命令:


scrapy shell www.qq.com


使用体验类似于直接在命令行输入 python 进入 Python 的交互式界面。


完善解析函数


解析函数的完善是爬虫的核心步骤。解析函数的初始化如下:


def parse(self, response):    pass
复制代码


其中只有 response 一个实参,该实参就是访问某个 URL 链接的返回结果,里面含有该 URL 链接的 HTML 源码(该 response 是对 requests.Response 类的封装,所以用法类似,但是包含的成员函数更多)。而解析函数 parse 的作用就是从 response 中杂乱的 HTML 源码提取出有价值的信息。


在 Scrapy 框架中,有两种解析 HTML 源码的函数,分别是 css 和 xpath。其中 css 是 Scrapy 专有的函数,具体用法只能在 Scrapy 文档中查找,不建议使用;而 xpath 是一种通用的语言(例如 BeautifulSoup 类中也能使用),它的一些语法的定义在网上资料更多。xpath 的具体用法要讲的话就太多了,所以这里不多做介绍,如果有需要,可以直接去搜索引擎查找相关资料。


如果需要在解析过程中遇到了需要解析的 URL 链接,则可以直接调用:


yield scrapy.Request(url_str, callback=self.parse)
复制代码


其中,url_str 是需要解析的 URL 链接的字符串,self.parse 是解析函数,这里我使用的是默认的解析函数,当然这里也能使用自定义的解析函数(自定义解析函数的入参出参类型需要和默认解析函数相同)。


值得注意的是:scrapy.Request 除了以上俩必须的参数外,还能通过 meta 字段来传递参数,而参数的获取能通过 response.meta 来实现。


小建议


默认情况下,Scrapy 会遵守被爬取网站的 robots.txt 规则(该文件规定了哪些能爬,哪些不能爬),但往往我们想要爬取的内容都被规定为不能爬取的内容。可以将 settings.py 文件中的 ROBOTSTXT_OBEY = True 改为 ROBOTSTXT_OBEY = False 来避免这种情况的发生。


三、常见问题


动态网页不能正确解析


上述的简单操作只能解析静态网页,需要动态加载的网页(例如含有 Javascript 代码的网页)则无法正常解析,因为 response 里的 HTML 源码是动态加载之前的页面的源码,而我们需要的大多是动态加载之后的页面。


可以通过在 Python 中调用 Chrome 浏览器的方式来处理这个问题。除此之外,还能使用 Chrome 浏览器的 headless 模式。使用了该模式之后,Chrome 浏览器并不会真的被调用,但是 Python 中能获取到和浏览器相同的返回结果,而浏览器中返回的结果就是动态加载之后的页面。


不过,要使用这个方法,必须在机器上安装 Chrome 浏览器和对应版本的 Chrome 驱动程序。安装完成之后,在 middlewares.py 文件中加入以下代码:


from selenium import webdriverfrom scrapy.http import HtmlResponse

class JavaScriptMiddleware: def process_request(self, request, spider): option = webdriver.ChromeOptions() option.add_argument('--headless') option.add_argument('--no-sandbox') option.add_argument('--disable-gpu') driver = webdriver.Chrome(options=option, executable_path=chrome_driver_path_str) driver.get(request.url) js = 'var q=document.documentElement.scrollTop=10000' driver.execute_script(js) body = driver.page_source return HtmlResponse(driver.current_url, body=body, encoding='utf-8', request=request)
复制代码


除此之外,还要在 settings.py 文件中加入以下代码:



DOWNLOADER_MIDDLEWARES = { 'your_project_name.middlewares.JavaScriptMiddleware': 543,}
复制代码


经过这两处修改之后,爬虫脚本里的所有 request 请求都会通过 Chrome headless 浏览器包装后再发向要爬取的 URL 链接。


防爬虫之修改 header


很多网站都有各自的反爬虫机制,但是最基础的一种方式是检查请求的 HTTP 包里面的 header 是否正常。其中经常检查的一个字段是 User-Agent,User-Agent 字段指的是浏览器的型号。反爬虫机制会检查该字段是否为普通的浏览器,而普通的爬虫程序是不会修饰该字段的。如果不显式将该字段设为某种浏览器型号,就容易触发反爬虫,从而不能正常地获得数据。


要想修改 Scrapy 里的 user-agent 字段,可以在 settings.py 文件里添加以下代码:


USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36'
复制代码


添加完该代码后,Scrapy 在发起 request 请求时就会将上面的值替换到 header 中的 User-Agent 中。


反爬虫之 IP 池


在很多时候,爬取网站时一开始是能正常获得数据的,但是爬着爬着,就不能正常地获得数据了。一个很大的可能是 IP 被该网站封禁了。每个网站封 IP 的策略都不一样,但是总体来说其实就是该 IP 访问该网站的频率太高,网站害怕该访问是恶意攻击或者担心服务器承受不了大量的访问从而直接封禁该 IP。


应对方式也非常粗暴,那就是用代理 IP 去爬虫。网站封一个 IP,我就用另外的 IP 去访问,只要我 IP 足够多,就总能获取到我想要的所有数据。而正好互联网上就有服务商提供这种 IP 服务。网上大致分为免费和付费两种服务,其中免费提供商提供的 IP 质量非常低,有不小的概率是直接不能用的,所以这里不推荐使用免费服务。至于付费服务商网上有很多家都挺靠谱的,本文里使用的名为“快代理”的服务商,下面提供的代码也是只针对该特定厂家的。不同服务商使用 IP 池的方式都不一样,具体使用方法还是以各自的官方文档为主。


在“快代理”上购买 IP 套餐后,在 middleware.py 文件中添加一下代码:


from w3lib.http import basic_auth_headerimport requests

class ProxyDownloaderMiddleware: username = 'your_username' password = 'your_password' api_url = 'https://dps.kdlapi.com/api/getdps/?orderid=your_orderid&num=1&pt=1&dedup=1&sep=1' proxy_ip_list = [] list_max_len = 20
def update_ip(self): if len(self.proxy_ip_list) != self.list_max_len: ip_str = requests.get('https://dps.kdlapi.com/api/getdps/?orderid=your_orderid&num={}&pt=1&dedup=1&sep=3'.format(self.list_max_len)).text self.proxy_ip_list = ip_str.split(' ') while True: try: proxy_ip = self.proxy_ip_list.pop(0) proxies = { 'http': 'http://{}:{}@{}'.format(self.username, self.password, proxy_ip), 'https': 'http://{}:{}@{}'.format(self.username, self.password, proxy_ip) } requests.get('http://www.baidu.com', proxies=proxies, timeout=3.05) self.proxy_ip_list.append(proxy_ip) return except Exception as e: self.proxy_ip_list.append(requests.get(self.api_url).text)
def process_request(self, request, spider): self.update_ip() request.meta['proxy'] = 'http://{}'.format(self.proxy_ip_list[-1]) # 用户名密码认证 request.headers['Proxy-Authorization'] = basic_auth_header(self.username, self.password) return None
复制代码


其中 username,password,order id 都是“快代理”中使用 IP 所要用的参数。上面的代码维护了一个大小为 20 的 IP 池,每次要使用时就提取第一个 IP 并先要检查该 IP 是否已经失效,如果失效了就丢弃并补充新的 IP。Scrapy 每次发起 request 请求时,会经过该 proxy 层的封装,但要想正常使用,还得在 settings.py 文件中添加以下代码:


DOWNLOADER_MIDDLEWARES = {    'your_project_name.middlewares.ProxyDownloaderMiddleware': 100,}
复制代码


在上文爬取动态页面的相关内容中也修改了这个 DOWNLOADER_MIDDLEWARES 这个字典。该字典中的 key 和 value 分别是在 middlewares.py 文件中添加的类和封装 request 包的顺序。如果要同时使用动态页面爬取和 IP 池,那么 settings.py 文件的该参数应该如下所示:


DOWNLOADER_MIDDLEWARES = {    'your_project_name.middlewares.JavaScriptMiddleware': 543,    'your_project_name.middlewares.ProxyDownloaderMiddleware': 100,}
复制代码


其中 100 < 543,代表 request 请求要先经过代理封装再经过动态加载封装,最后才发送给目标 URL 链接。



头图:Unsplash

作者:赵宇航

原文:https://mp.weixin.qq.com/s/-jCxnhzo-G9fzZNT-Azp7g

原文:基于 Scrapy 的爬虫解决方案

来源:云加社区 - 微信公众号 [ID:QcloudCommunity]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021-06-23 08:002200

评论

发布
暂无评论
发现更多内容
基于Scrapy的爬虫解决方案_文化 & 方法_云加社区_InfoQ精选文章