Bob's Blog

Web开发、测试框架、自动化平台、APP开发、机器学习等

返回上页首页

Python爬虫(七)使用scrapy快速爬取全站页面



前面介绍了用请求解析页面数据、headless加载页面js等资源的方式。我们也可以关注一下现有的轮子,一些比较优秀的爬虫框架。这里介绍一下scrapy,并以快速爬取网站全站的链接为例子说明。

scrapy是非常强大和方便的工具,它还提供了一些预设好的爬取类型。关于scrapy的组件可以参考下图,不做详细叙述。

首先假设我们现在有一个需求,是爬取自己网站的所有链接,并检查是否存在404、500等状态的无效链接,来维护网站功能。

如果用requests之类的话,则需要设定非常多的规则,比如是否爬取重复链接因为有些页面的链接是一个循环,如何处理异常,解析什么样的类型的节点,等等。而scrapy已经有相应的处理了,我们只需要指定配置再设定自己的步骤就可以。

首先安装scrapy

pip install scrapy

然后启动一个项目

scrapy startproject myblog

此时已经自动生成了配置文件、中间件、爬虫的基本代码。

我们进入该目录

cd myblog

在spider文件夹中新增一个文件,这个文件则是我们具体要爬取的工作定义。

from pathlib import Path
import scrapy

class BlogSpider(scrapy.Spider):
    name = "scrapy_blog"
    allowed_domains = ["xxx.com"]

    def start_requests(self):
        urls = ["https://www.xxx.com/"]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = f'quotes-{page}.html'
        Path(filename).write_bytes(response.body)
        self.log(f'Saved file {filename}')

这是一个最简单的爬虫定义,我们指定允许的链接地址是xxx.com,开始爬行的链接定义在url这个列表中。要注意这个name的定义是命令行中需要对应上的名字。

此时运行一个简单的命令,便可看到当前目录下生成了一个文件记录了页面内容。

scrapy crawl scrapy_blog

但这肯定不够,没有满足我们的需要。于是需要改变一下。

首先我们继承的类不再是scrapy.Spider,改为CrawlSpider,这个类就是前面提到的预设了一些方便我们使用的方法,非常适合爬取全站。

与CrawlSpider合作的往往还有Rule和LinkExtractor,前者代表着爬取的行为规则定义,后者代表着我们爬取的是链接。

于是代码可以改为;

from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class BlogSpider(CrawlSpider):
    name = "scrapy_blog"
    main_url = "xxx.com"
    allowed_domains = ["xxx.com"]
    start_urls = [f"https://{main_url}/"]
    total = 0

    rules = (
        Rule(LinkExtractor(allow=(f".*{main_url}.*",)), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        self.total += 1
        res = {
            "url": response.url,
            "status": response.status,
            "refer": bytes.decode(response.request.headers.get("Referer", "")),
            "count": self.total
        }
        if response.status == 200:
            self.log(f"valid: {res['url']}: {res['status']} -- {res['refer']}")
        else:
            self.log(f"valid: {res['url']}: {res['status']} -- {res['refer']}")
        yield res

LinkExtractor(allow=(f".*{main_url}.*",))这里代表着抓取链接,但是必须符合这个正则的文本才会抓取。

callback='parse_item'这里代表着对于合适的对象采用parse_item这个方法做处理。

follow=True这里代表着递归爬取,链接可以跳转,就会随着跳转去下一个页面检查,相当于节省了我们很多功夫。(要注意的是,scrapy默认的设置是不会爬取重复的链接了,毕竟有些页面也会导向前一个页面或者主页。如果需要爬取重复的则需要设定dont_filter = False)

yield res则是将我们处理的信息返回,我们可以用来保存或者再处理。

在获取到链接进入到parse_item这个方法时,我们通过内部传递的reponse对象来取值具体的url、状态码、以及从header中获取从哪个页面跳过来的,这对于后续的记录很有用。

此时我们运行这个命令便可看到日志,如果指定-o的参数则可以将res保存到文件

scrapy crawl scrapy_blog   # 看日志
scrapy crawl scrapy_blog -o links.json   # 看到日志并且保存到links.json文件

但是在这里,会发现最终只爬取了200状态的链接,因为非200的被当做不正常状态被直接丢弃。因此我们需要在类里面加上一个配置:custom_settings = {'HTTPERROR_ALLOW_ALL': True},此时便可处理所有状态的链接。

另外如果不想用-o的参数来保存,还可以在spider文件中指定重写close方法。要么在parse_item方法中满足条件就保存文件,要么在close方法中在最后爬虫结束时保存文件。此时获取全站链接的爬虫代码便可以变成:

from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor


class BlogSpider(CrawlSpider):
    name = "scrapy_blog"
    main_url = "xxx.com"
    allowed_domains = ["xxx.com"]
    start_urls = [f"https://{main_url}/"]
    total = 0
    custom_settings = {'HTTPERROR_ALLOW_ALL': True}
    all_links = []

    rules = (
        Rule(LinkExtractor(allow=(f".*{main_url}.*",)), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        self.total += 1
        res = {
            "url": response.url,
            "status": response.status,
            "refer": bytes.decode(response.request.headers.get("Referer", "")),
            "count": self.total
        }
        self.all_liniks.append(res)
        if response.status == 200:
            self.log(f"valid: {res['url']}: {res['status']} -- {res['refer']}")
        else:
            self.log(f"valid: {res['url']}: {res['status']} -- {res['refer']}")
        if self.total % 2000 == 0:
            self.record_to_file()
        yield res

    def close(self, reason):
        self.log("crawl end")
        self.record_to_file()

    def record_to_file():
        with open("./links.csv", "w") as f:
            for u in self.all_links:
                url = u["url"]
                url_from = u["refer"]
                f.write(f"\"{url}\",{u['status']},\"{url_from}\"\n")

此时运行scrapy crawl scrapy_blog即可保存内容到文件。

如果想从其他文件来调取这个爬虫的话,则可以用到CrawlProcess,比如在根目录创建一个run.py:

from scrapy.crawler import CrawlerProcess
from myblog.myblog.spiders.blog_links import BlogSpider

if __name__ == "__main__":
    p = CrawlerProcess()
    p.crawl(BlogSpider)
    p.start()

此时运行python run.py即可。

如果考虑到全站爬取的站点是一个包含很多内容的网站,需要爬取特别久,喂了以防特殊情况造成中断,需要一个能继续上一次爬取的功能,还好scrapy提供了这个功能,https://docs.scrapy.org/en/latest/topics/jobs.html

运行时指定-s,比如

scrapy crawl scrapy_blog -s JOBDIR=crawls_blog

或者在setting.py中设定

JOBDIR = 'PROJECT_DIR'

如果要同时展示log到console并且保存到文件,可以用下面这个命令
 

scrapy crawl scrapy_blog -s JOBDIR=crawls_blog 2>&1 | tee record_console.log

 

下一篇:  python批量裁剪图片
上一篇:  ChatGPT注册和使用过程记录

共有0条评论

添加评论

暂无评论