## 爬虫框架Scrapy简介 å½“ä½ å†™äº†å¾ˆå¤šä¸ªçˆ¬è™«ç¨‹åºä¹‹åŽï¼Œä½ 会å‘çŽ°æ¯æ¬¡å†™çˆ¬è™«ç¨‹åºæ—¶ï¼Œéƒ½éœ€è¦å°†é¡µé¢èŽ·å–ã€é¡µé¢è§£æžã€çˆ¬è™«è°ƒåº¦ã€å¼‚常处ç†ã€å爬应对这些代ç 从头至尾实现一éï¼Œè¿™é‡Œé¢æœ‰å¾ˆå¤šå·¥ä½œå…¶å®žéƒ½æ˜¯ç®€å•ä¹å‘³çš„é‡å¤åŠ³åŠ¨ã€‚é‚£ä¹ˆï¼Œæœ‰æ²¡æœ‰ä»€ä¹ˆåŠžæ³•å¯ä»¥æå‡æˆ‘们编写爬虫代ç çš„æ•ˆçŽ‡å‘¢ï¼Ÿç”æ¡ˆæ˜¯è‚¯å®šçš„,那就是利用爬虫框架,而在所有的爬虫框架ä¸ï¼ŒScrapy 应该是最æµè¡Œã€æœ€å¼ºå¤§çš„æ¡†æž¶ã€‚ ### Scrapy 概述 Scrapy 是基于 Python 的一个éžå¸¸æµè¡Œçš„网络爬虫框架,å¯ä»¥ç”¨æ¥æŠ“å– Web 站点并从页é¢ä¸æå–结构化的数æ®ã€‚下图展示了 Scrapy 的基本架构,其ä¸åŒ…å«äº†ä¸»è¦ç»„件和系统的数æ®å¤„ç†æµç¨‹ï¼ˆå›¾ä¸å¸¦æ•°å—的红色ç®å¤´ï¼‰ã€‚ <img src="res/20210824003638.png" style="zoom:50%;"> #### Scrapy的组件 我们先æ¥è¯´è¯´ Scrapy ä¸çš„组件。 1. Scrapy 引擎(Engineï¼‰ï¼šç”¨æ¥æŽ§åˆ¶æ•´ä¸ªç³»ç»Ÿçš„æ•°æ®å¤„ç†æµç¨‹ã€‚ 2. 调度器(Scheduler):调度器从引擎接å—请求并排åºåˆ—入队列,并在引擎å‘出请求åŽè¿”还给它们。 3. 下载器(Downloader):下载器的主è¦èŒè´£æ˜¯æŠ“å–网页并将网页内容返还给蜘蛛(Spiders)。 4. 蜘蛛程åºï¼ˆSpiders):蜘蛛是用户自定义的用æ¥è§£æžç½‘页并抓å–特定URL的类,æ¯ä¸ªèœ˜è››éƒ½èƒ½å¤„ç†ä¸€ä¸ªåŸŸå或一组域å,简å•的说就是用æ¥å®šä¹‰ç‰¹å®šç½‘站的抓å–和解æžè§„则的模å—。 5. æ•°æ®ç®¡é“(Item Pipeline):管é“的主è¦è´£ä»»æ˜¯è´Ÿè´£å¤„ç†æœ‰èœ˜è››ä»Žç½‘页䏿нå–çš„æ•°æ®æ¡ç›®ï¼Œå®ƒçš„主è¦ä»»åŠ¡æ˜¯æ¸…ç†ã€éªŒè¯å’Œå˜å‚¨æ•°æ®ã€‚当页é¢è¢«èœ˜è››è§£æžåŽï¼Œå°†è¢«å‘é€åˆ°æ•°æ®ç®¡é“,并ç»è¿‡å‡ 个特定的次åºå¤„ç†æ•°æ®ã€‚æ¯ä¸ªæ•°æ®ç®¡é“组件都是一个 Python 类,它们获å–äº†æ•°æ®æ¡ç›®å¹¶æ‰§è¡Œå¯¹æ•°æ®æ¡ç›®è¿›è¡Œå¤„ç†çš„æ–¹æ³•ï¼ŒåŒæ—¶è¿˜éœ€è¦ç¡®å®šæ˜¯å¦éœ€è¦åœ¨æ•°æ®ç®¡é“ä¸ç»§ç»æ‰§è¡Œä¸‹ä¸€æ¥æˆ–是直接丢弃掉ä¸å¤„ç†ã€‚æ•°æ®ç®¡é“é€šå¸¸æ‰§è¡Œçš„ä»»åŠ¡æœ‰ï¼šæ¸…ç† HTML æ•°æ®ã€éªŒè¯è§£æžåˆ°çš„æ•°æ®ï¼ˆæ£€æŸ¥æ¡ç›®æ˜¯å¦åŒ…å«å¿…è¦çš„å—æ®µï¼‰ã€æ£€æŸ¥æ˜¯ä¸æ˜¯é‡å¤æ•°æ®ï¼ˆå¦‚æžœé‡å¤å°±ä¸¢å¼ƒï¼‰ã€å°†è§£æžåˆ°çš„æ•°æ®å˜å‚¨åˆ°æ•°æ®åº“(关系型数æ®åº“或 NoSQL æ•°æ®åº“)ä¸ã€‚ 6. ä¸é—´ä»¶ï¼ˆMiddlewares):ä¸é—´ä»¶æ˜¯ä»‹äºŽå¼•æ“Žå’Œå…¶ä»–ç»„ä»¶ä¹‹é—´çš„ä¸€ä¸ªé’©åæ¡†æž¶ï¼Œä¸»è¦æ˜¯ä¸ºäº†æä¾›è‡ªå®šä¹‰çš„ä»£ç æ¥æ‹“展 Scrapy 的功能,包括下载器ä¸é—´ä»¶å’Œèœ˜è››ä¸é—´ä»¶ã€‚ #### æ•°æ®å¤„ç†æµç¨‹ Scrapy 的整个数æ®å¤„ç†æµç¨‹ç”±å¼•擎进行控制,通常的è¿è½¬æµç¨‹åŒ…括以下的æ¥éª¤ï¼š 1. 引擎询问蜘蛛需è¦å¤„ç†å“ªä¸ªç½‘站,并让蜘蛛将第一个需è¦å¤„ç†çš„ URL 交给它。 2. 引擎让调度器将需è¦å¤„ç†çš„ URL 放在队列ä¸ã€‚ 3. å¼•æ“Žä»Žè°ƒåº¦é‚£èŽ·å–æŽ¥ä¸‹æ¥è¿›è¡Œçˆ¬å–的页é¢ã€‚ 4. 调度将下一个爬å–çš„ URL 返回给引擎,引擎将它通过下载ä¸é—´ä»¶å‘é€åˆ°ä¸‹è½½å™¨ã€‚ 5. 当网页被下载器下载完æˆä»¥åŽï¼Œå“应内容通过下载ä¸é—´ä»¶è¢«å‘é€åˆ°å¼•擎;如果下载失败了,引擎会通知调度器记录这个 URL,待会å†é‡æ–°ä¸‹è½½ã€‚ 6. 引擎收到下载器的å“应并将它通过蜘蛛ä¸é—´ä»¶å‘é€åˆ°èœ˜è››è¿›è¡Œå¤„ç†ã€‚ 7. 蜘蛛处ç†å“应并返回爬å–åˆ°çš„æ•°æ®æ¡ç›®ï¼Œæ¤å¤–还è¦å°†éœ€è¦è·Ÿè¿›çš„æ–°çš„ URL å‘é€ç»™å¼•擎。 8. 引擎将抓å–åˆ°çš„æ•°æ®æ¡ç›®é€å…¥æ•°æ®ç®¡é“,把新的 URL å‘é€ç»™è°ƒåº¦å™¨æ”¾å…¥é˜Ÿåˆ—ä¸ã€‚ 上述æ“作ä¸çš„第2æ¥åˆ°ç¬¬8æ¥ä¼šä¸€ç›´é‡å¤ç›´åˆ°è°ƒåº¦å™¨ä¸æ²¡æœ‰éœ€è¦è¯·æ±‚çš„ URLï¼Œçˆ¬è™«å°±åœæ¢å·¥ä½œã€‚ ### 安装和使用Scrapy å¯ä»¥ä½¿ç”¨ Python 的包管ç†å·¥å…·`pip`æ¥å®‰è£… Scrapy。 ```Shell pip install scrapy ``` 在命令行ä¸ä½¿ç”¨`scrapy`命令创建å为`demo`的项目。 ```Bash scrapy startproject demo ``` 项目的目录结构如下图所示。 ```Shell demo |____ demo |________ spiders |____________ __init__.py |________ __init__.py |________ items.py |________ middlewares.py |________ pipelines.py |________ settings.py |____ scrapy.cfg ``` 切æ¢åˆ°`demo` 目录,用下é¢çš„命令创建å为`douban`的蜘蛛程åºã€‚ ```Bash scrapy genspider douban movie.douban.com ``` #### 一个简å•çš„ä¾‹å æŽ¥ä¸‹æ¥ï¼Œæˆ‘们实现一个爬å–豆瓣电影 Top250 ç”µå½±æ ‡é¢˜ã€è¯„分和金å¥çš„爬虫。 1. 在`items.py`çš„`Item`ç±»ä¸å®šä¹‰å—æ®µï¼Œè¿™äº›å—æ®µç”¨æ¥ä¿å˜æ•°æ®ï¼Œæ–¹ä¾¿åŽç»çš„æ“ä½œã€‚ ```Python import scrapy class DoubanItem(scrapy.Item): title = scrapy.Field() score = scrapy.Field() motto = scrapy.Field() ``` 2. 修改`spiders`文件夹ä¸å为`douban.py` 的文件,它是蜘蛛程åºçš„æ ¸å¿ƒï¼Œéœ€è¦æˆ‘ä»¬æ·»åŠ è§£æžé¡µé¢çš„代ç 。在这里,我们å¯ä»¥é€šè¿‡å¯¹`Response`对象的解æžï¼ŒèŽ·å–电影的信æ¯ï¼Œä»£ç 如下所示。 ```Python import scrapy from scrapy import Selector, Request from scrapy.http import HtmlResponse from demo.items import MovieItem class DoubanSpider(scrapy.Spider): name = 'douban' allowed_domains = ['movie.douban.com'] start_urls = ['https://movie.douban.com/top250?start=0&filter='] def parse(self, response: HtmlResponse): sel = Selector(response) movie_items = sel.css('#content > div > div.article > ol > li') for movie_sel in movie_items: item = MovieItem() item['title'] = movie_sel.css('.title::text').extract_first() item['score'] = movie_sel.css('.rating_num::text').extract_first() item['motto'] = movie_sel.css('.inq::text').extract_first() yield item ``` 通过上é¢çš„代ç ä¸éš¾çœ‹å‡ºï¼Œæˆ‘们å¯ä»¥ä½¿ç”¨ CSS 选择器进行页é¢è§£æžã€‚å½“ç„¶ï¼Œå¦‚æžœä½ æ„¿æ„也å¯ä»¥ä½¿ç”¨ XPath 或æ£åˆ™è¡¨è¾¾å¼è¿›è¡Œé¡µé¢è§£æžï¼Œå¯¹åº”的方法分别是`xpath`å’Œ`re`。 如果还è¦ç”ŸæˆåŽç»çˆ¬å–的请求,我们å¯ä»¥ç”¨`yield`产出`Request`对象。`Request`对象有两个éžå¸¸é‡è¦çš„属性,一个是`url`,它代表了è¦è¯·æ±‚的地å€ï¼›ä¸€ä¸ªæ˜¯`callback`,它代表了获得å“应之åŽè¦æ‰§è¡Œçš„回调函数。我们å¯ä»¥å°†ä¸Šé¢çš„代ç ç¨ä½œä¿®æ”¹ã€‚ ```Python import scrapy from scrapy import Selector, Request from scrapy.http import HtmlResponse from demo.items import MovieItem class DoubanSpider(scrapy.Spider): name = 'douban' allowed_domains = ['movie.douban.com'] start_urls = ['https://movie.douban.com/top250?start=0&filter='] def parse(self, response: HtmlResponse): sel = Selector(response) movie_items = sel.css('#content > div > div.article > ol > li') for movie_sel in movie_items: item = MovieItem() item['title'] = movie_sel.css('.title::text').extract_first() item['score'] = movie_sel.css('.rating_num::text').extract_first() item['motto'] = movie_sel.css('.inq::text').extract_first() yield item hrefs = sel.css('#content > div > div.article > div.paginator > a::attr("href")') for href in hrefs: full_url = response.urljoin(href.extract()) yield Request(url=full_url) ``` 到这里,我们已ç»å¯ä»¥é€šè¿‡ä¸‹é¢çš„命令让爬虫è¿è½¬èµ·æ¥ã€‚ ```Shell scrapy crawl movie ``` å¯ä»¥åœ¨æŽ§åˆ¶å°çœ‹åˆ°çˆ¬å–到的数æ®ï¼Œå¦‚果想将这些数æ®ä¿å˜åˆ°æ–‡ä»¶ä¸ï¼Œå¯ä»¥é€šè¿‡`-o`傿•°æ¥æŒ‡å®šæ–‡ä»¶å,Scrapy æ”¯æŒæˆ‘们将爬å–到的数æ®å¯¼å‡ºæˆ JSONã€CSVã€XML ç‰æ ¼å¼ã€‚ ```Shell scrapy crawl moive -o result.json ``` ä¸çŸ¥å¤§å®¶æ˜¯å¦æ³¨æ„到,通过è¿è¡Œçˆ¬è™«èŽ·å¾—çš„ JSON æ–‡ä»¶ä¸æœ‰`275`æ¡æ•°æ®ï¼Œé‚£æ˜¯å› 为首页被é‡å¤çˆ¬å–了。è¦è§£å†³è¿™ä¸ªé—®é¢˜ï¼Œå¯ä»¥å¯¹ä¸Šé¢çš„代ç ç¨ä½œè°ƒæ•´ï¼Œä¸åœ¨`parse`方法ä¸è§£æžèŽ·å–æ–°é¡µé¢çš„ URL,而是通过`start_requests`方法æå‰å‡†å¤‡å¥½å¾…爬å–页é¢çš„ URL,调整åŽçš„代ç 如下所示。 ```Python import scrapy from scrapy import Selector, Request from scrapy.http import HtmlResponse from demo.items import MovieItem class DoubanSpider(scrapy.Spider): name = 'douban' allowed_domains = ['movie.douban.com'] def start_requests(self): for page in range(10): yield Request(url=f'https://movie.douban.com/top250?start={page * 25}') def parse(self, response: HtmlResponse): sel = Selector(response) movie_items = sel.css('#content > div > div.article > ol > li') for movie_sel in movie_items: item = MovieItem() item['title'] = movie_sel.css('.title::text').extract_first() item['score'] = movie_sel.css('.rating_num::text').extract_first() item['motto'] = movie_sel.css('.inq::text').extract_first() yield item ``` 3. 如果希望完æˆçˆ¬è™«æ•°æ®çš„æŒä¹…åŒ–ï¼Œå¯ä»¥åœ¨æ•°æ®ç®¡é“ä¸å¤„ç†èœ˜è››ç¨‹åºäº§ç”Ÿçš„`Item`对象。例如,我们å¯ä»¥é€šè¿‡å‰é¢è®²åˆ°çš„`openpyxl`æ“作 Excel 文件,将数æ®å†™å…¥ Excel 文件ä¸ï¼Œä»£ç 如下所示。 ```Python import openpyxl from demo.items import MovieItem class MovieItemPipeline: def __init__(self): self.wb = openpyxl.Workbook() self.sheet = self.wb.active self.sheet.title = 'Top250' self.sheet.append(('åç§°', '评分', 'å言')) def process_item(self, item: MovieItem, spider): self.sheet.append((item['title'], item['score'], item['motto'])) return item def close_spider(self, spider): self.wb.save('豆瓣电影数æ®.xlsx') ``` 上é¢çš„`process_item`å’Œ`close_spider`都是回调方法(钩å函数), 简å•的说就是 Scrapy 框架会自动去调用的方法。当蜘蛛程åºäº§ç”Ÿä¸€ä¸ª`Item`对象交给引擎时,引擎会将该`Item`对象交给数æ®ç®¡é“,这时我们é…置好的数æ®ç®¡é“çš„`parse_item`方法就会被执行,所以我们å¯ä»¥åœ¨è¯¥æ–¹æ³•ä¸èŽ·å–æ•°æ®å¹¶å®Œæˆæ•°æ®çš„æŒä¹…åŒ–æ“作。å¦ä¸€ä¸ªæ–¹æ³•`close_spider`是在爬虫结æŸè¿è¡Œå‰ä¼šè‡ªåŠ¨æ‰§è¡Œçš„æ–¹æ³•ï¼Œåœ¨ä¸Šé¢çš„代ç ä¸ï¼Œæˆ‘们在这个地方进行了ä¿å˜ Excel 文件的æ“作,相信这段代ç 大家是很容易读懂的。 总而言之,数æ®ç®¡é“å¯ä»¥å¸®åŠ©æˆ‘ä»¬å®Œæˆä»¥ä¸‹æ“作: - æ¸…ç† HTML æ•°æ®ï¼ŒéªŒè¯çˆ¬å–的数æ®ã€‚ - 丢弃é‡å¤çš„ä¸å¿…è¦çš„内容。 - 将爬å–的结果进行æŒä¹…化æ“作。 4. 修改`settings.py`文件对项目进行é…置,主è¦éœ€è¦ä¿®æ”¹ä»¥ä¸‹å‡ 个é…置。 ```Python # 用户æµè§ˆå™¨ USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36' # å¹¶å‘è¯·æ±‚æ•°é‡ CONCURRENT_REQUESTS = 4 # 下载延迟 DOWNLOAD_DELAY = 3 # éšæœºåŒ–下载延迟 RANDOMIZE_DOWNLOAD_DELAY = True # 是å¦éµå®ˆçˆ¬è™«åè®® ROBOTSTXT_OBEY = True # é…置数æ®ç®¡é“ ITEM_PIPELINES = { 'demo.pipelines.MovieItemPipeline': 300, } ``` > **说明**:上é¢é…置文件ä¸çš„`ITEM_PIPELINES`选项是一个å—典,å¯ä»¥é…ç½®å¤šä¸ªå¤„ç†æ•°æ®çš„管é“,åŽé¢çš„æ•°å—代表了执行的优先级,数å—å°çš„先执行。