進階——scrapy登錄豆瓣解決cookie傳遞問題並爬取用戶參加過的同城活動©seven_clear


    最近在用scrapy重寫以前的爬蟲,由於豆瓣的某些信息要登錄后才有權限查看,故要實現登錄功能。豆瓣登錄偶爾需要輸入驗證碼,這個在以前寫的爬蟲里解決了驗證碼的問題,所以只要搞清楚scrapy怎么提交表單什么的就OK了。從網上找了點資料,說要重寫CrawlSpider的start_requests,在重寫的函數里發個request,在其回調函數里提交表單。至於request是啥,參考scrapy文檔(中文版:http://scrapy-chs.readthedocs.io/zh_CN/latest/intro/tutorial.html)。廢話少說,上代碼。

    重寫start_requests:

def start_requests(self):
        return [Request('https://www.douban.com/accounts/login',
                        meta={'cookiejar': 1}
                        callback=self.post_login)]
Scrapy通過使用cookiejar Request meta key來支持單spider追蹤多cookie session。 默認情況下其使用一個cookie jar(session),不過可以傳遞一個標示符來使用多個。如meta={'cookiejar': 1}這句,后面那個1就是標示符。下面就是post_login函數了,這個函數主要是提交登錄表單,要處理有驗證碼和無驗證碼兩種情況:
def post_login(self, response):
        print 'Preparing login====', response.url
        # s = 'index_nav'
        html = urllib2.urlopen(response.url).read()
        # print 'htnl:', html
        # 驗證碼圖片地址
        imgurl = re.search('<img id="captcha_image" src="(.+?)" alt="captcha" class="captcha_image"/>', html)
        if imgurl:
            url = imgurl.group(1)
            # 將圖片保存至同目錄下
            res = urllib.urlretrieve(url, 'v.jpg')
            # 獲取captcha-id參數
            captcha = re.search('<input type="hidden" name="captcha-id" value="(.+?)"/>', html)
            if captcha:
                vcode = raw_input('請輸入圖片上的驗證碼:')
                return [FormRequest.from_response(response,
                                                  meta={'cookiejar': response.meta['cookiejar']},
                                                  formdata={
                                                      'source': 'index_nav',
                                                      # 'source': s,
                                                      'form_email': 'your_email',
                                                      'form_password': 'your_password',
                                                      'captcha-solution': vcode,
                                                      'captcha-id': captcha.group(1),
                                                      'user_login': '登錄'
                                                  },
                                                  callback=self.after_login,
                                                  dont_filter=True)
                        ]
        return [FormRequest.from_response(response,
                                          meta={'cookiejar': response.meta['cookiejar']},
                                          formdata={
                                              'source': 'index_nav',
                                              # 'source': s,
                                              'form_email': 'your_email',
                                              'form_password': 'your_password'
                                          },
                                          callback=self.after_login,
                                          dont_filter=True)
                ]
這里通過meta傳遞cookie(為什么要傳cookie,因為cookiejar meta key不是“粘性的”),並且source:index_nav這句是必須的,如果沒有這句會登錄不上,dont_filter是為了不過濾,因為前面寫了自定義的Rule(后面會貼出代碼),下面就是回調的after_login函數了:
def after_login(self, response):
        print 'after_login============'
        
        for url in self.start_urls:
            print '======url:', url
            # 上面定義了Rule,這里只需要生成初始爬取Request即可
            req = Request(url, meta={'cookiejar': response.meta['cookiejar']})
            
            yield req
這里同樣用meta傳遞cookie,不過這樣是不成功的,運行時老是302重定向,肯定是cookie沒傳過去,后來找到一種方法,要重寫下面的這個方法:
def _requests_to_follow(self, response):
        
        if not isinstance(response, HtmlResponse):
            return
        seen = set()
        for n, rule in enumerate(self._rules):
            links = [l for l in rule.link_extractor.extract_links(response) if l not in seen]
            if links and rule.process_links:
                links = rule.process_links(links)
            for link in links:
                seen.add(link)
                r = Request(url=link.url, callback=self._response_downloaded)
                # 下面這句是重寫的
                r.meta.update(rule=n, link_text=link.text, cookiejar=response.meta['cookiejar'])
                yield rule.process_request(r)
重寫那句是每次更新meta都將cookie也更新了,這樣傳遞即可cookie。當然,我這么菜,肯定不是自己想出來的(->_->)©seven_clear
最關鍵一步,在配置文件里啟用cookie,在settings里設置COOKIES_ENABLED=True。如果想看cookie的傳遞信息,可設置COOKIES_DEBIG=True(別問我怎么知道的,文檔上看的)
登錄問題解決了,就搞點事,網上爬電影的一大堆,不想盲目隨大流,搞個同城爽一下,爬別的模塊看下網頁源碼,稍微動一下腦子就能搞出來。首先要把規則寫好,在rules里加上同城的Rule:
Rule(SgmlLinkExtractor(allow=(r'https://www.douban.com/location/people/[0-9a-zA-Z]*/events/attend$')),
             callback='parse_eventlist'),
SgmLinkExtractor是用來篩選鏈接的,用法最好還是看文檔(文檔往往是最好的參考資料)。

    哦哦,還得確定爬取哪些內容,寫一個Item,隨便打開幾個活動頁面,發現活動都有時間、地點、費用、類型、主辦方(發起人)這五項,則爬取這些信息:

class EventItem(Item):
    itype = Field()  # 類型
    title = Field()  # 活動標題
    time = Field()  # 時間
    address = Field()  # 地點
    fee = Field()  # 費用
    etype = Field()  # 類型
    actor = Field()  # 發起人/主辦方

寫好Item,還要處理Item,ItemPipiLines就是干這事的,這里將數據寫入json,如果爬的數據量很大,json就不太合適了,可以用JsonLinesItemExporter,每個Item一項,具體用法以后應該會寫個例子。pipeline代碼:

class EventPipeLine(object):
    def __init__(self):
        self.file = codecs.open('event.json', 'w', encoding='utf-8')

    def process_item(self, item, spider):

        if item['itype'] is not 'event':
            return item
        name = json.dumps('活動名:' + str(item['title']), ensure_ascii=False) + '\n'
        time = json.dumps('時間:' + str(item['time']), ensure_ascii=False) + '\n'
        address = json.dumps('地點:' + str(item['address']), ensure_ascii=False) + '\n'
        fee = json.dumps('費用:' + str(item['fee']), ensure_ascii=False) + '\n'
        etype = json.dumps('類型:' + str(item['etype']), ensure_ascii=False) + '\n'
        actor = json.dumps('發起人/主辦方:' + str(item['actor']), ensure_ascii=False) + '\n'
        line = name + owner + time + address + fee + etype + actor
        self.file.write(line)
        self.file.write('==================================================\n')
        return item

    def spider_closed(self, spider):
        self.file.close()

這樣的代碼在網上爛大街了,自己看看理解思路就行,ensure_ascii=False解決寫入時中文亂碼問題。對了,item類里有兩個類型,注釋沒寫好,itype是說明Item類的類型,即這個item是處理什么類型的數據(電影、音樂或者同城等等),下面那個etype是活動詳情頁里的那個類型(活動的),在pipeline里用itype來判斷item的類型,這只是一個思路,當爬的模塊多的時候避免pipeline沖突,因為我爬的模塊多,設置pipeline優先級后老沖突,爬取過程中返回的item順序不一定和pipeline的順序一致,於是就加了這么一個字段判斷,當然要把各pipeline的優先級設一樣(在settings里設置ITEM_PIPELINES)。

都OK了,可以寫rule里的回調函數了,流程大致為:從用戶主頁篩選用戶同城活動鏈接->進入鏈接后篩已過期鏈接(參加過的當然是已過期)->進入鏈接篩活動列表->對列表的活動進入詳情頁篩要爬的數據©seven_clear。一個三個函數:

def parse_eventlist(self, response):
        print 'url=====', response.url
        url = response.url
        nextp = url + '/expired'
        print 'nextpage:', nextp
        req = Request(nextp, callback=self.event_list)
        # ===========================================================================
        req.meta['cookiejar'] = response.meta['cookiejar']  # 傳遞cookie
        # ===========================================================================
        yield req

    def event_list(self, response):
       page = response.body
        soup = BeautifulSoup(page, 'html.parser')
        # 獲取條目總頁數
        pagenum = soup.find("span", {"class", "thispage"})['data-total-page'] \
            if soup.find("span", {"class", "thispage"}) else 0
        print 'before====pagenum===:', pagenum
        print 'type:', type(pagenum)
        if int(pagenum) > 5:
            pagenum = 5
        print '====pagenum===:', pagenum
        # 獲取當前頁數
        thispage = soup.find("span", {"class", "thispage"}).next_element \
            if soup.find("span", {"class", "thispage"}) else 0
        print 'thispage:', thispage
        # 獲取下一頁鏈接
        if soup.find('span', {'class': 'next'}).find('a'):
            nexturl = soup.find('span', {'class': 'next'}).find('a')['href']
            nexturl = 'https://www.douban.com' + nexturl
            print 'nexturl:', nexturl
        result = soup.findAll("div", {"class": "info"})
        print 'num:', len(result) - 1
        for item in result:
            if item.find('h1'):  # 去除雜項
                pass
            else:
                event_url = item.find('a')['href']
                # print '========================================='
                # print 'address:', item.find('a')['href']
                req = Request(event_url, callback=self.parse_event)  # 請求條目詳情頁
                # ===========================================================================
                req.meta['cookiejar'] = response.meta['cookiejar']  # 傳遞cookie
                # ===========================================================================
                yield req
        print '==========page', thispage, 'print done!============'
        if int(thispage) < int(pagenum):  # 若當前頁不到最后頁或者指定頁數,則請求下一頁
            req = Request(nexturl, callback=self.event_list)
            # ===========================================================================
            req.meta['cookiejar'] = response.meta['cookiejar']  # 傳遞cookie
            # ===========================================================================
            yield req

    def parse_event(self, response):
        sel = Selector(response)
        item = EventItem()
        item['itype'] = 'event'
        item['title'] = sel.xpath('//*[@id="event-info"]/div[1]/h1/text()').extract()[0].strip()
        item['time'] = sel.xpath('//*[@id="event-info"]/div[1]/div[1]/ul/li/text()').extract()[0]
        item['address'] = ''.join(sel.xpath('//*[@id="event-info"]/div[1]/div[2]/span[2]/span/text()').extract())
        item['fee'] = sel.xpath('//*[@id="event-info"]/div[1]/div[3]/text()').extract()[1].strip()
        item['etype'] = sel.xpath('//*[@id="event-info"]/div[1]/div[4]/a/text()').extract()[0]
        item['actor'] = sel.xpath('//*[@id="event-info"]/div[1]/div[5]/a/text()').extract()[0]
        yield item
       


在實現過程中還是有些坑的,比如判斷總頁數的時候,因為爬的數據是Unicode類型的,要轉為int再與指定頁數比較,不然可能會有這種情況(假設指定頁數為5):活動總頁數是4,但是if pagenum > 5的結果是True,導致爬取過程中出錯;還有下一頁的鏈接是不全的,要在前面加上前綴;活動的地址分級不一樣,可以都爬下來用join方法連接起來。處理event信息是用xpath,這個很好用,可以看一下。

這樣就O了,做出來覺得沒什么,但做的過程是很曲折的,主要是思路的突破口,看了說一句這很簡單也不容易,背后還是要付出點的,繼續前行©seven_clear

對了,還有要import的東西,這里提一下:

from scrapy.contrib.spiders import CrawlSpider, Rule  
from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
from scrapy.selector import Selector  # 選擇器,xpath找數據
from ..items import EventItem  # 導入item
import re  #以下3個用於驗證碼登錄時下載驗證碼
import urllib
import urllib2
from scrapy.http import Request, FormRequest, HtmlResponse  # 表單登錄

 最后一句,我是菜鳥,別介意我的代碼爛,哈哈哈


免責聲明!

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



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