Scrapy爬蟲筆記


Scrapy是一個優秀的Python爬蟲框架,可以很方便的爬取web站點的信息供我們分析和挖掘,在這記錄下最近使用的一些心得。

1.安裝

通過pip或者easy_install安裝:
1
sudo pip install scrapy

2.創建爬蟲項目

1
scrapy startproject youProjectName

3.抓取數據

首先在items.py里定義要抓取的內容,以豆瓣美女為例:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 
from scrapy.item import Field,Item  class DoubanmeinvItem(Item):  feedId = Field() #feedId  userId = Field() #用戶id  createOn = Field() #創建時間  title = Field() #feedTitle  thumbUrl = Field() #feed縮略圖url  href = Field() #feed鏈接  description = Field() #feed簡介  pics = Field() #feed的圖片列表  userInfo = Field() #用戶信息  class UserItem(Item):  userId = Field() #用戶id  name = Field() #用戶name  avatar = Field() #用戶頭像
創建爬蟲文件,cd到工程文件夾下后輸入命令:
1
scrapy crawl XXX(爬蟲名字)

另外可以在該爬蟲項目的根目錄創建一個main.py,然后在pycharm設置下運行路徑

那么就不用每次都運行上面那行代碼,直接運行main.py就能啟動爬蟲了

輸入代碼:

from scrapy import cmdline
cmdline.execute('scrapy crawl amazon_products -o items.csv -t csv'.split())
#-o 代表輸出文件 -t 代表文件格式

  

接着編輯爬蟲文件,實例如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 
# -*- coding: utf-8 -*- import scrapy import re from DoubanMeinv.items import DoubanmeinvItem,UserItem import json import time from datetime import datetime from scrapy.exceptions import CloseSpider  import sys reload(sys) sys.setdefaultencoding('utf8')  class DbmeinvSpider(scrapy.Spider):  name = "dbMeinv"  allowed_domains = ["www.dbmeinv.com"]  start_urls = (  'http://www.dbmeinv.com/dbgroup/rank.htm?pager_offset=1',  )  baseUrl = 'http://www.dbmeinv.com'  close_down = False   def parse(self, response):  request = scrapy.Request(response.url,callback=self.parsePageContent)  yield request   #解析每一頁的列表  def parsePageContent(self, response):  for sel in response.xpath('//div[@id="main"]//li[@class="span3"]'):  item = DoubanmeinvItem()  title = sel.xpath('.//div[@class="bottombar"]//a[1]/text()').extract()[0]  #用strip()方法過濾開頭的\r\n\t和空格符  item['title'] = title.strip()  item['thumbUrl'] = sel.xpath('.//div[@class="img_single"]//img/@src').extract()[0]  href = sel.xpath('.//div[@class="img_single"]/a/@href').extract()[0]  item['href'] = href  #正則解析id  pattern = re.compile("dbgroup/(\d*)")  res = pattern.search(href).groups()  item['feedId'] = res[0]  #跳轉到詳情頁面  request = scrapy.Request(href,callback=self.parseMeinvDetailInfo)  request.meta['item'] = item  yield request  #判斷是否超過限制應該停止  if(self.close_down == True):  print "數據重復,close spider"  raise CloseSpider(reason = "reach max limit")  else:  #獲取下一頁並加載  next_link = response.xpath('//div[@class="clearfix"]//li[@class="next next_page"]/a/@href')  if(next_link):  url = next_link.extract()[0]  link = self.baseUrl + url  yield scrapy.Request(link,callback=self.parsePageContent)   #解析詳情頁面  def parseMeinvDetailInfo(self, response):  item = response.meta['item']  description = response.xpath('//div[@class="panel-body markdown"]/p[1]/text()')  if(description):  item['description'] = description.extract()[0]  else:  item['description'] = ''  #上傳時間  createOn = response.xpath('//div[@class="info"]/abbr/@title').extract()[0]  format = "%Y-%m-%d %H:%M:%S.%f"  t = datetime.strptime(createOn,format)  timestamp = int(time.mktime(t.timetuple()))  item['createOn'] = timestamp  #用戶信息  user = UserItem()  avatar = response.xpath('//div[@class="user-card"]/div[@class="pic"]/img/@src').extract()[0]  name = response.xpath('//div[@class="user-card"]/div[@class="info"]//li[@class="name"]/text()').extract()[0]  home = response.xpath('//div[@class="user-card"]/div[@class="opt"]/a[@target="_users"]/@href').extract()[0]  user['avatar'] = avatar  user['name'] = name  #正則解析id  pattern = re.compile("/users/(\d*)")  res = pattern.search(home).groups()  user['userId'] = res[0]  item['userId'] = res[0]  #將item關聯user  item['userInfo'] = user  #解析鏈接  pics = []  links = response.xpath('//div[@class="panel-body markdown"]/div[@class="topic-figure cc"]')  if(links):  for a in links:  img = a.xpath('./img/@src')  if(img):  picUrl = img.extract()[0]  pics.append(picUrl)  #轉成json字符串保存  item['pics'] = json.dumps(list(pics))  yield item
需要說明的幾點內容:
  • allowed_domin指定Spider在哪個網站爬取數據
  • start_urls包含了Spider在啟動時進行爬取的url列表
  • parse方法繼承自父類,每個初始URL完成下載后生成的Response對象將會作為唯一的參數傳遞給該函數。該方法負責解析返回的數據(response),提取數據(生成item)以及生成需要進一步處理的URL的Request對象
  • xpath解析數據的時候使用(也可以使用css),關於xpath和css的詳細用法請自行搜索
  • xpath從某個子元素里解析數據時要使用element.xpath('./***')而不能使用element.xpath('/***'),否則是從最外層解析而不是從element下開始解析
  • web站點爬取的text經常包含了我們不想要的\r\n\t或者是空格等字符,這個時候就要使用Python的strip()方法來過濾掉這些數據
  • 抓取的web頁面時間經常是2015-10-1 12:00:00格式,但是我們存儲到數據庫時要想轉成timeStamp的格式,這里用Python的time相關類庫來處理,代碼見上面
  • 抓取完某個頁面的時候,可能我們還需要抓取跟它相關的詳情頁面數據,這里用生成Scrapy.Request的方式來繼續抓取,並且將當前的item存儲到新的request的meta數據中以供后面的代碼中讀取到已抓取的item
  • 如果我們想要在某些情況下停止Spider的抓取,在這里設置一個flag位,並在適當的地方拋出一個CloseSpider的異常來停止爬蟲,后面會接着提到這個技巧

4.運行爬蟲

1
scrapy crawl youSpiderName

5.編寫Pipeline

如果我們要將數據存儲到MySQL數據庫中,需要安裝MySQLdb,安裝過程很多坑,遇到了再Google解決吧。一切搞定之后開始編寫pipelines.py和settings.py文件
首先在settings.py文件中定義好連接MySQL數據庫的所需信息,如下所示:
1
2 3 4 5 6 7 8 9 10 
DB_SERVER = 'MySQLdb' DB_CONNECT = {  'host' : 'localhost',  'user' : 'root',  'passwd' : '',  'port' : 3306,  'db' :'dbMeizi',  'charset' : 'utf8',  'use_unicode' : True }
然后編輯pipelines.py文件,添加代碼如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 
from scrapy.conf import settings from scrapy.exceptions import DropItem from twisted.enterprise import adbapi import json  class DoubanmeinvPipeline(object):  #插入的sql語句  feed_key = ['feedId','userId','createOn','title','thumbUrl','href','description','pics']  user_key = ['userId','name','avatar']  insertFeed_sql = '''insert into MeiziFeed (%s) values (%s)'''  insertUser_sql = '''insert into MeiziUser (%s) values (%s)'''  feed_query_sql = "select * from MeiziFeed where feedId = %s"  user_query_sql = "select * from MeiziUser where userId = %s"  feed_seen_sql = "select feedId from MeiziFeed"  user_seen_sql = "select userId from MeiziUser"  max_dropcount = 50  current_dropcount = 0   def __init__(self):  dbargs = settings.get('DB_CONNECT')  db_server = settings.get('DB_SERVER')  dbpool = adbapi.ConnectionPool(db_server,**dbargs)  self.dbpool = dbpool  #更新看過的id列表  d = self.dbpool.runInteraction(self.update_feed_seen_ids)  d.addErrback(self._database_error)  u = self.dbpool.runInteraction(self.update_user_seen_ids)  u.addErrback(self._database_error)   def __del__(self):  self.dbpool.close()   #更新feed已錄入的id列表  def update_feed_seen_ids(self, tx):  tx.execute(self.feed_seen_sql)  result = tx.fetchall()  if result:  #id[0]是因為result的子項是tuple類型  self.feed_ids_seen = set([int(id[0]) for id in result])  else:  #設置已查看過的id列表  self.feed_ids_seen = set()   #更新user已錄入的id列表  def update_user_seen_ids(self, tx):  tx.execute(self.user_seen_sql)  result = tx.fetchall()  if result:  #id[0]是因為result的子項是tuple類型  self.user_ids_seen = set([int(id[0]) for id in result])  else:  #設置已查看過的id列表  self.user_ids_seen = set()   #處理每個item並返回  def process_item(self, item, spider):  query = self.dbpool.runInteraction(self._conditional_insert, item)  query.addErrback(self._database_error, item)   feedId = item['feedId']  if(int(feedId) in self.feed_ids_seen):  self.current_dropcount += 1  if(self.current_dropcount >= self.max_dropcount):  spider.close_down = True  raise DropItem("重復的數據:%s" % item['feedId'])  else:  return item   #插入數據  def _conditional_insert(self, tx, item):  #插入Feed  tx.execute(self.feed_query_sql, (item['feedId']))  result = tx.fetchone()  if result == None:  self.insert_data(item,self.insertFeed_sql,self.feed_key)  else:  print "該feed已存在數據庫中:%s" % item['feedId']  #添加進seen列表中  feedId = item['feedId']  if int(feedId) not in self.feed_ids_seen:  self.feed_ids_seen.add(int(feedId))  #插入User  user = item['userInfo']  tx.execute(self.user_query_sql, (user['userId']))  user_result = tx.fetchone()  if user_result == None:  self.insert_data(user,self.insertUser_sql,self.user_key)  else:  print "該用戶已存在數據庫:%s" % user['userId']  #添加進seen列表中  userId = user['userId']  if int(userId) not in self.user_ids_seen:  self.user_ids_seen.add(int(userId))   #插入數據到數據庫中  def insert_data(self, item, insert, sql_key):  fields = u','.join(sql_key)  qm = u','.join([u'%s'] * len(sql_key))  sql = insert % (fields,qm)  data = [item[k] for k in sql_key]  return self.dbpool.runOperation(sql,data)   #數據庫錯誤  def _database_error(self, e):  print "Database error: ", e
說明幾點內容:
  • process_item:每個item通過pipeline組件都需要調用該方法,這個方法必須返回一個Item對象,或者拋出DropItem異常,被丟棄的item將不會被之后的pipeline組件所處理。
  • 已經抓取到的數據不應該再處理,這里創建了兩個ids_seen方法來保存已抓取的id數據,如果已存在就Drop掉item
  • 如果重復抓取的數據過多時,這里設置了個上限值(50),如果超過了上限值就改變spider的關閉flag標志位,然后spider判斷flag值在適當的時候拋出CloseSpider異常,關閉Spider代碼見爬蟲文件。這里通過設置flag標志位的方式來關閉爬蟲主要是因為我測試的時候發現在pipelines中調用停止爬蟲的方法都不起效果,故改成這種方式
  • 因為Scrapy是基於twisted的,所以這里用adbapi來連接並操作MySQL數據庫
最后在settings.py文件中啟用pipeline
1
2 3 4 
ITEM_PIPELINES = {  'DoubanMeinv.pipelines.DoubanmeinvPipeline': 300,  # 'DoubanMeinv.pipelines.ImageCachePipeline': 500, }

6.變換User-Agent,避免爬蟲被ban

我們抓取的網站可能會檢查User-Agent,所以為了爬蟲正常運行我們需要設置請求的User-Agent。對於頻繁的請求,還要對User-Agent做隨機變換以防被ban,這里通過設置Downloader Middleware來修改爬蟲的request和respons
在setting.py文件中添加User-Agent列表
1
2 3 4 5 6 7 8 9 10 11 
DOWNLOADER_MIDDLEWARES = {  'DoubanMeinv.middlewares.RandomUserAgent': 1, }  USER_AGENTS = [  "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; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",  "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.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", ]
修改middlewares.py文件添加如下代碼:
1
2 3 4 5 6 7 8 9 10 11 12 
import random  class RandomUserAgent(object):  def __init__(self, agents):  self.agents = agents   @classmethod  def from_crawler(cls, crawler):  return cls(crawler.settings.getlist('USER_AGENTS'))   def process_request(self, request, spider):  request.headers.setdefault('User-Agent', random.choice(self.agents)) 

7.禁用Cookie+設置請求延遲

某些網站可能會根據cookie來分析爬取的軌跡,為了被ban,我們最好也禁用掉cookie;同時為了避免請求太頻繁而造成爬蟲被ban,我們還需要設置請求間隔時間,在settings.py文件中添加以下代碼:
1
2 
DOWNLOAD_DELAY=1 COOKIES_ENABLED=False

8.抓取圖片並保存到本地

有時候我們想把抓取到的圖片直接下載並保存到本地,可以用Scrapy內置的ImagesPipeline來處理,因為ImagesPipeline用到了PIL這個圖片處理模塊,所以我們首先需要使用pip來安裝Pillow
安裝成功后,在pipelines.py代碼中添加以下代碼:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 
from scrapy.pipelines.images import ImagesPipeline from scrapy import Request import json  class ImageCachePipeline(ImagesPipeline):  def get_media_requests(self, item, info):  pics = item['pics']  list = json.loads(pics)  for image_url in list:  yield Request(image_url)   def item_completed(self, results, item, info):  image_paths=[x['path'] for ok,x in results if ok]  if not image_paths:  print "圖片未下載好:%s" % image_paths  raise DropItem('圖片未下載好 %s'%image_paths)
ImagesPipeline類有一個get_media_requests方法來進行下載的控制,所以我們在這里解析imgUrl並發起進行一個Request,在下載完成之后,會把結果傳遞到item_completed方法,包括 下載是否成功( True or False) 以及下載下來保存的路徑和下載的路徑,這里改寫這個方法讓他把下載失敗的(Flase)的圖片的路徑輸出出來
接下來在settings.py里設置下載圖片的文件目錄並啟用ImageCachePipeline
1
2 3 4 5 6 7 8 
#設置圖片保存到本地的地址和過期時間 IMAGES_STORE='/Users/chen/Pictures/Meizi' IMAGES_EXPIRES = 90  ITEM_PIPELINES = {  'DoubanMeinv.pipelines.DoubanmeinvPipeline': 300,  'DoubanMeinv.pipelines.ImageCachePipeline': 500, }
等待爬蟲執行完之后去IMAGES_STORE路徑下查看圖片就是了

9.自動運行爬蟲

為了源源不斷獲取數據,可通過命令讓爬蟲每天都運行來抓取數據
1
2 3 4 
// 為當前用戶新增任務 crontab -e // 增加如下記錄 注意替換自己的爬蟲目錄 由於環境變量的原因,scrapy要給出全路徑 0 10 * * * cd /home/chen/pyWork/DoubanMeinvScrapy && /usr/bin/scrapy crawl dbmeinv
上面的命令添加了一個任務,這個任務會每天早上10:00啟動,這個任務要做得就是進入爬蟲目錄,並啟動爬蟲。
如果你不知道自己的scrapy的全路徑,可以用終端下用which scrapy來查看

 

最后秀一下抓取到的數據:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM