DMOZ爬取实验报告

一. 实验目的及要求

  1. 熟悉scrapy爬虫框架
  2. 掌握使用Chrome开发者工具找到HTML节点的XPATH路径或者CSS路径
  3. 掌握scrapy shell工具,用它验证XPATH路径或者CSS路径是否能爬取想要的节点内容
  4. 掌握xshell、xftp的使用方法
  5. 熟悉一种Python IDE
  6. 掌握powershell、CentOS的基本命令
  7. 掌握screen的用法
  8. 爬取Dmoz,Arts大类下的所有内容
  9. 提取Arts下的目录结构

二. 实验环境要求

  1. 操作系统:
  • win10(装有powershell)、CentOS7.2
  1. 软件:
  • win10: Python3.6.5, scrapy1.5.0, vscode1.23.1, xshell, xftp
  • CentOS:Python3.6.5, scrapy1.5.0, screen4.01.00

三. 实验内容与步骤

1. 确定爬取内容

通过观察Arts下的网页,我们可以发现每个网页分为了Subcategories, Related categories, Sites 和 Other languages四个节。


DMOZ爬取实验报告_第1张图片
dmoz网页布局

我们需要爬取的内容存在于Subcategories和Sites两个节中,同时观察Subcategories下的每一项中的a标签,我们可以发现下一级目录从/Arts开始的相对路径。所以可以确定每个网页需要爬取的内容为:

  • 当前网页的所属目录(即URL去掉根域名)
  • 当前网页包括的子目录(每个目录项的名称和相对路径)
  • 当前网页收藏的网页(每个网页项的标题、链接、摘要)

所以可以确定items.py文件为:

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html

import scrapy


class DmozprjItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    category = scrapy.Field() # 当前网页的所属目录(即URL去掉根域名)
    subcategories = scrapy.Field() # 当前网页包括的子目录(每个目录项的名称和相对路径)
    sites = scrapy.Field() # 当前网页收藏的网页(每个网页项的标题、链接、摘要)

2. 确定爬取内容的Xpath路径

用Chrome打开网页后,点击F12使用开发者工具。点击Ctrl+Shift+C打开元素选取工具,选定元素后,在界面右侧用鼠标右键点击相应的节点,Copy->Copy Xpath。

DMOZ爬取实验报告_第2张图片
使用Chrome开发者工具确定Xpath路径

然后将路径复制到文档中,使用scrapy shell工具验证路径是否正确。
右键点击开始图标->选择Windows PowerShell(管理员)-> 输入scrapy shell " https://curlie.org/Recreation/Antiques/" -> 然后验证相应的Xpath路径。
经过验证后,我们得到的爬取内容的Xpath路径如下:

# 获取当前网页所在目录
response.url.lstrip('https://curlie.org')

# 获取目录块
//section[@class="children"]//div[@class="cat-item"]
a/div/text() # 获取目录块下的目录标题
a/@href      # 获取目录块下的目录相对地址

# 获取网页块
//div[@class="site-item "]/div[3]
a/div/text() #获取网页标题
a/@href      #获取网页链接
div/text()   #获取网页摘要

3. 确定spider的爬取规则

将Arts目录下的所有内容爬取下来的规则很简单,提取Arts目录下所有子目录的链接,然后从每个链接下的网页提取相应的内容。
通过查看scrapy官方文档,我们发现spider继承CrawlSpider后,跟踪链接将更为方便。

This is the most commonly used spider for crawling regular websites, as it provides a convenient mechanism for following links by defining a set of rules. It may not be the best suited for your particular web sites or project, but it’s generic enough for several cases, so you can start from it and override it as needed for more custom functionality, or just implement your own spider.

它通过定义Rule对start_urls中起始网页里追踪到的链接进行筛选,然后通过设置Rule中的callback参数,设置处理符合条件的网页的parse函数。(这里定义的parse函数不能重载默认的parse函数,因为CrawlSpider类机制需要使用parse函数,定义的时候需将提取网页内容的函数命名为类似于parse_item)
在爬取Subcategories与Sites时,分别建立两个for循环。每一个项为一个dict,包含项的详细信息。然后for循环结束后,得到两个list,然后赋值给item相应的元素。
项目的爬虫文件curlie.py如下:

# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from dmozprj.items import DmozprjItem

class CurlieSpider(CrawlSpider):
    name = 'curlie'
    allowed_domains = ['curlie.org']
    start_urls = ['https://curlie.org/Arts/']
    rules = (
        Rule(
            LinkExtractor(  # 限定爬取子目录各项中的链接
                restrict_xpaths=('//section[@class="children"]//div[@class="cat-item"]'), 
                allow_domains=('curlie.org')
                ), 
            callback='parse_item', 
            follow=True
            ),
        )

    def parse_item(self, response):
        item = DmozprjItem()
        
        # 爬当前网页所属目录
        category = response.url.lstrip('https://curlie.org')
        item['category'] = category
        
        
        # 爬当前网页中的子目录
        subcategory_list = list()
        for web in response.xpath('//section[@class="children"]//div[@class="cat-item"]'):
            i = {}
            i['category_name'] = web.xpath('a/div/text()').extract()[1].strip()
            i['category_path'] = web.xpath('a/@href').extract_first()
            subcategory_list.append(i)
        item['subcategories'] = subcategory_list

        
        # 爬当前网页中的列出的网页信息
        site_list = list()
        for web in response.xpath('//div[@class="site-item "]/div[3]'):
            i = {}
            i['name'] = web.xpath('a/div/text()').extract_first().strip()
            i['link'] = web.xpath('a/@href').extract_first()
            i['descrip'] = web.xpath('div/text()').extract_first().strip()
            site_list.append(i)
        item['sites'] = site_list
        return item

4. 编写整个爬虫项目文件

我们在Win10环境下编写爬虫代码,然后使用xftp将代码放到云服务器上(CentOS)运行,爬取dmoz的Arts分类下的内容。

  • 在适当的目录下,Shift+鼠标右键->在此处打开PowerShell窗口,在窗口中输入如下代码新建名为dmozprj的爬虫项目
PS D:\MyWorkspace\PythonWorkspace> scrapy startproject dmozprj
New Scrapy project 'dmozprj', using template directory 'D:\\ProgramData\\Anaconda3\\lib\\site-packages\\scrapy\\templates\\project', created in:
    D:\MyWorkspace\PythonWorkspace\dmozprj

You can start your first spider with:
    cd dmozprj
    scrapy genspider example example.com
PS D:\MyWorkspace\PythonWorkspace> cd dmozprj
PS D:\MyWorkspace\PythonWorkspace\dmozprj> scrapy genspider example example.com
Created spider 'example' using template 'basic' in module:
  dmozprj.spiders.example
  • 使用vscode编写爬虫项目文件。首先编写items.py文件,然后编写curlie.py文件(代码如1和3中)
  • 进行基本配置,打开settings.py文件,加入如下配置,以保证输出的文件为中文
# 爬取的字段输出到文件的编码为'utf-8'
FEED_EXPORT_ENCODING = 'utf-8'

ITEM_PIPELINES = {
    'dmozprj.pipelines.DmozprjPipeline': 300,
}
  • 使用xftp将整个文件夹上传云服务器/home/ir08cc目录下,进入dmozprj目录,运行爬虫。curlie为spider目录下爬虫文件里定义的爬虫的name,同一个爬虫项目下爬虫name不可以重名。-o web.jl选择输出JSONLINE文件,此文件的特点是,一行一个json形式的数据,即一个item(一个网页爬取到的数据)。相比.json文件,-o web.json输出的json文件内为一个整体的json数据,若是爬取不完整,或者二次往文件里面写入json数据,json数据格式就会出错。
[ir08cc@VM_0_12_centos ~]$ cd dmozprj/
[ir08cc@VM_0_12_centos dmozprj]$ ls
dmozprj  entrypoint.py  scrapy.cfg  web.jl
[ir08cc@VM_0_12_centos dmozprj]$ scrapy crawl curlie -o web.jl

5. curlie.org的反爬虫机制与应对措施

curlie.org有反爬虫机制,我们在第一次试验性的爬取该网站后,发现无法继续访问该网站。查看该网站的robots.txt文件

User-agent: *
Crawl-delay: 1

Crawl-delay设置的主要原因是:蜘蛛程序爬的过快,会给服务器照成负担,影响正常的网站展示速度。
但是该网站的反爬虫机制是禁止识别到的爬虫程序使用的IP一段时间,在这段时间内,无法通过此IP访问该网站。
经过查找资料,我们发现可以通过设置user-agent、设置代理IP和设置DOWNLOAD_DELAY来避免被网站认为是爬虫程序。
5.1 在settings.py文件中,设置下载延迟

DOWNLOAD_DELAY = 1

5.2 设置user-agent,在middlewares.py文件中添加如下类

from scrapy import signals
from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware
import random


class MyUserAgentMiddleware(UserAgentMiddleware):
    '''
    设置User-Agent
    '''

    def __init__(self, user_agent):
        self.user_agent = user_agent # user_agent复制给类变量user_agent

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            # 从settings.py中获取user-agent列表并复制给self.user_agent
            user_agent=crawler.settings.get('MY_USER_AGENT') 
        )

    def process_request(self, request, spider):
        # 随机从user-agent列表中选出一个user-agent
        agent = random.choice(self.user_agent) 
        request.headers['User-Agent'] = agent

同时要在settings.py中添加如下配置,user-agent即设置成功

# user-agent池,每次随机从中选择user-agent进行模拟
MY_USER_AGENT = [
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
    "Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
    "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
    "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
    "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
    "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
    "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5",
    "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
    "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.11 TaoBrowser/2.0 Safari/536.11",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.71 Safari/537.1 LBBROWSER",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; LBBROWSER)",
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E; LBBROWSER)",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.84 Safari/535.11 LBBROWSER",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; QQBrowser/7.0.3698.400)",
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SV1; QQDownload 732; .NET4.0C; .NET4.0E; 360SE)",
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)",
    "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1",
    "Mozilla/5.0 (iPad; U; CPU OS 4_2_1 like Mac OS X; zh-cn) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
    "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b13pre) Gecko/20110307 Firefox/4.0b13pre",
    "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:16.0) Gecko/20100101 Firefox/16.0",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11",
    "Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
    ]

DOWNLOADER_MIDDLEWARES = {
    'scrapy.downloadermiddleware.useragent.UserAgentMiddleware': None, 
    'dmozprj.middlewares.MyUserAgentMiddleware': 400
}

5.3 设置代理
首先在middlewares.py文件中添加如下类

from scrapy import signals
from scrapy.downloadermiddlewares.httpproxy import HttpProxyMiddleware
import random


class MyHttpProxyMiddleware(HttpProxyMiddleware):
    '''
    设置Proxy,本来是随机从代理IP池中获取代理,但是因为稳定的IP不多,频繁
    重新发起请求,所以设置固定代理IP,有较为稳定的下载速度。
    '''

    # def __init__(self, ip):
    #     self.ip = ip

    @classmethod
    # def from_crawler(cls, crawler):
    #     return cls(ip=crawler.settings.get('PROXIES'))

    def process_request(self, request, spider):
        # ip = random.choice(self.ip)
        # 这里设置的为固定IP,可以从西刺免费代理IP获取
        request.meta['proxy'] = "http(s)://xxx.xxx.xxx.xxx:xx"

然后再settings.py文件中加入如下配置

# 开启下载中间件关于代理和user-agent的组件
DOWNLOADER_MIDDLEWARES = {
    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware':None, 
    'dmozprj.middlewares.MyHttpProxyMiddleware':125  
}

6. 关于关闭服务器的远程连接后,让爬虫程序继续运行

经过查找资料,在centos上安装screen可实现断开远程连接后,让爬虫程序继续运行。
关于如何安装和使用screen,可以参考教程CentOS下screen 命令详解

7. 提高爬虫效率

在写完爬虫代码后,在最开始的settings下,爬取一晚上(9小时左右),爬取了10000左右的网页,然后网站还没有爬完,爬虫在继续运行。
查找资料后,我发现这是由于设置的DOWLOAD_DELAY设置为3的原因,这表示下载器下载一个页面后,3秒后再下载队列里的下一个页面。这样在调度器发起请求充足的情况下,最快也只能3秒下载一个页面,24小时最多能下载28800个页面。
但是DOWLOAD_DELAY必须要设置,避免curlie封我们IP。所以将值设为1后,我们重新运行爬虫程序,与之间的爬虫程序一起运行。(同时运行两个爬虫还有一个原因,之前的爬虫的爬取规则有问题,导致子目录的信息没有爬取完全,但是从子目录能跟踪的链接依旧进入调度器等待提取内容)
将DOWNLOAD_DELAY值设为1后,同时设置了并发、DOWNLOAD_TIMEOUT和关闭缓存以提高爬虫速度。
现在两个爬虫程序依旧在运行。爬虫1已经运行了18h,爬虫2已经运行了5.5h。在爬虫2刚开始运行的时候,爬虫1怕下来的数据大小为9M。
现在爬虫1爬下来的数据大小为15.44M,爬虫2爬下来的数据大小为9.15M。所以很明显的看出爬虫2的运行效率更高。
相关的settings.py文件设置如下:

# 设置并发数为100
CONCURRENT_REQUESTS = 100
# 下载超时设为60s
DOWNLOAD_TIMEOUT = 60
# 关闭缓存
COOKIES_ENABLED = False

四. 实验结果

最终代码github链接:https://github.com/RainOfPhone/dmozprj.git
运行爬虫需在终端输入

scrapy crawl curlie -o web.jl

因为输出的JSONLINE文件,所以文件中每一行为一个网页中爬取到的所有内容,具有一定的结构性。JSON数据格式如下:


DMOZ爬取实验报告_第3张图片
JSON数据格式

爬取结束后,spider返回的信息如下:

2018-05-20 23:19:53 [scrapy.core.engine] INFO: Closing spider (finished)
2018-05-20 23:19:53 [scrapy.extensions.feedexport] INFO: Stored jl feed (28791 items) in: web.jl
2018-05-20 23:19:53 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/exception_count': 226,
 'downloader/exception_type_count/twisted.internet.error.ConnectionRefusedError': 7,
 'downloader/exception_type_count/twisted.internet.error.TimeoutError': 153,
 'downloader/exception_type_count/twisted.web._newclient.ResponseFailed': 65,
 'downloader/exception_type_count/twisted.web._newclient.ResponseNeverReceived': 1,
 'downloader/request_bytes': 12605856,
 'downloader/request_count': 29019,
 'downloader/request_method_count/GET': 29019,
 'downloader/response_bytes': 454417204,
 'downloader/response_count': 28793,
 'downloader/response_status_count/200': 28793,
 'dupefilter/filtered': 24402,
 'finish_reason': 'finished',
 'finish_time': datetime.datetime(2018, 5, 20, 15, 19, 53, 571645),
 'item_scraped_count': 28791,
 'log_count/DEBUG': 57812,
 'log_count/ERROR': 1,
 'log_count/INFO': 614,
 'log_count/WARNING': 65,
 'memusage/max': 83525632,
 'memusage/startup': 44892160,
 'request_depth_max': 67,
 'response_received_count': 28793,
 'retry/count': 225,
 'retry/max_reached': 1,
 'retry/reason_count/twisted.internet.error.ConnectionRefusedError': 7,
 'retry/reason_count/twisted.internet.error.TimeoutError': 152,
 'retry/reason_count/twisted.web._newclient.ResponseFailed': 65,
 'retry/reason_count/twisted.web._newclient.ResponseNeverReceived': 1,
 'scheduler/dequeued': 29018,
 'scheduler/dequeued/memory': 29018,
 'scheduler/enqueued': 29018,
 'scheduler/enqueued/memory': 29018,
 'start_time': datetime.datetime(2018, 5, 20, 5, 13, 46, 140300)}

从以上代码可以看出,在Arts子目录下,爬取下来的网页有28793个,有226个异常,没有下载下来。
通过每个网页所属的category,以及每个subcategory项的category_path,可以链接得出/Arts下的目录结构。

五. 总结

通过这次爬取dmoz的实验,我们熟悉了scrapy爬虫框架,学会了scrapy.Spider和CrawlSpider两个类的基本用法,同时对爬虫的爬取规则有了一定的认识。同时初步了解了scrapy的并发机制,如何设置代理和模拟浏览器访问网站。同时对云服务器的重要性有了更深的认识,在云服务器上爬虫可以一直运行,但是在本地电脑爬虫要一直运行电脑就不能关机。所以要灵活运用云服务器。
scrapy的各种设置对爬虫的效率有着很大的影响,下一步需要进一步研究如何提高爬虫的效率。

你可能感兴趣的:(DMOZ爬取实验报告)