——編輯:大牧莫邪html
爬蟲程序,主要是用與數據採集處理的一種網絡程序,在操做過程當中針對指定的url地址進行數據請求並根據須要採集數據,可是在實際項目開發過程當中,常常會遇到目標url地址數量不明確的狀況,如以前的章節中提到的智聯招聘項目,不一樣的崗位搜索到的崗位數量不必定一致,也就意味着每一個工做搜索到的工做崗位列表頁面的數量不必定一致,爬蟲工程師工做可能搜索到了10頁,Django工做有可能都索到了25頁數據,那麼針對這樣的數據要所有進行爬取,應該怎麼處理呢?答案就是:深度爬蟲python
深度爬蟲:針對其實url地址進行數據採集,在響應數據中進行數據篩選獲得須要進行數據採集的下一波url地址,並將url地址添加到數據採集隊列中進行二次爬取..以此類推,一致到全部頁面的數據所有采集完成便可完成深度數據採集,這裏的深度指代的就是url地址的檢索深度。正則表達式
深度爬蟲能夠經過不一樣的方式實現,在urllib2和requesets模塊中經過輪詢數據篩選獲得目標url地址,而後進行循環爬取數據便可,在scrapy中主要經過兩種方式進行處理:算法
首先完成深度爬蟲以前,先了解Scrapy框架底層的一些操做模式,Scrapy框架運行爬蟲項目,默認調用並執行parse()函數進行數據的解析,可是此時已經由框架完成了請求解析調度和下載的過程,那麼Scrapy到底作了哪些事情呢?shell
咱們首先觀察一下scrapy.Spider源代碼數據庫
class Spider(object_ref): """Base class for scrapy spiders. All spiders must inherit from this class. """ name = None custom_settings = None # 初始化函數,主要進行程序的名稱、起始地址等數據初始化工做 def __init__(self, name=None, **kwargs): if name is not None: self.name = name elif not getattr(self, 'name', None): raise ValueError("%s must have a name" % type(self).__name__) self.__dict__.update(kwargs) if not hasattr(self, 'start_urls'): self.start_urls = [] ... ... # 程序啓動,發送請求的函數 def start_requests(self): cls = self.__class__ # 默認沒有重寫直接調用,重寫的時候根據子類重寫的方式從新定義發送處理方式 # 默認狀況下發送get請求獲取數據,若是要發送Post請求能夠重寫start_reuqests函數進行請求的處理 if method_is_overridden(cls, Spider, 'make_requests_from_url'): warnings.warn( "Spider.make_requests_from_url method is deprecated; it " "won't be called in future Scrapy releases. Please " "override Spider.start_requests method instead (see %s.%s)." % ( cls.__module__, cls.__name__ ), ) for url in self.start_urls: yield self.make_requests_from_url(url) else: # 沒有重寫該方法,直接根據初始地址包裝請求對象發送請求 for url in self.start_urls: yield Request(url, dont_filter=True)
咱們能夠從源代碼中查看到,咱們定義的爬蟲處理類繼承的scrapy.Spider類型中,對於初始化的name和start_urls初始地址進行了初始化,而後自動調用start_requests函數包裝Request請求對象,而後經過協程調用的方法將請求交給調度器進行後續的處理markdown
這裏就須要瞭解請求對象中到底作了哪些事情?!cookie
(1) Request對象 Request請求對象是scrapy框架中的核心對象,經過將字符串url地址包裝成請求對象交給調度器進行調度管理,以後交給下載模塊進行數據採集的操做網絡
Request底層操做部分源碼以下:框架
# scrapy中的Request請求對象 class Request(object_ref): # 默認構建時,method="GET"包裝的是GET請求的採集方式 # 參數url:請求地址字符串 # 參數callback:請求的回調函數 # 參數headers:默認的請求頭 # 參數body: 請求體 # 參數cookies:請求中包含的cookie對象 # 參數encoding:請求編碼方式 def __init__(self, url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding='utf-8', priority=0, dont_filter=False, errback=None, flags=None): self._encoding = encoding # this one has to be set first self.method = str(method).upper() self._set_url(url) self._set_body(body) assert isinstance(priority, int), "Request priority not an integer: %r" % priority self.priority = priority if callback is not None and not callable(callback): raise TypeError('callback must be a callable, got %s' % type(callback).__name__) if errback is not None and not callable(errback): raise TypeError('errback must be a callable, got %s' % type(errback).__name__) assert callback or not errback, "Cannot use errback without a callback" self.callback = callback self.errback = errback self.cookies = cookies or {} self.headers = Headers(headers or {}, encoding=encoding) self.dont_filter = dont_filter self._meta = dict(meta) if meta else None self.flags = [] if flags is None else list(flags)
那麼在實際操做中,咱們經過以下三點詳細說明:
直接編寫爬蟲程序,定義strat_urls中的初始地址和爬蟲的name名稱,而後重寫父類中的parse()函數便可,請求的發送默認就是get()方式進行數據採集:
import scrapy # 定義本身的爬蟲處理類 class MySpider(scrapy.Spider): # 定義爬蟲名稱 name = 'myspider' # 定義初始化url地址列表 start_urls = ("http://www.baidu.com", ) # 定義域名限制 allowed_domains = ["baidu.com"] # 定義數據處理方式 def parse(self, response): # 數據處理部分 pass
由於scarpy默認的Request是get方式發送請求,若是要經過post方式發送請求採集數據,須要從新編寫start_requests()函數覆蓋父類中的請求包裝方式
import scrapy class MySpider(scrapy.Spider): # 定義爬蟲名稱 name = 'myspider' # 定義初始化url地址列表 start_urls = ("http://www.baidu.com", ) # 定義域名限制 allowed_domains = ["baidu.com"] # 重寫父類請求初始化發送方式 def start_requests(self, response): # 循環初始話地址,發送post請求 for url in self.start_urls: yield scrapy.FormRequest( url = url, formdata = {post參數字典}, callback = self.parse_response, ) # 從新編寫響應數據處理函數 def parse_response(self, response): # 處理採集到的response數據 pass
同時,也能夠經過響應對象構建一個POST請求從新發送,以下:
import scrapy class MySpider(scarpy.Spider): # 定義爬蟲名稱 name = 'myspider' # 定義初始化url地址列表 start_urls = ("http://www.baidu.com", ) # 定義域名限制 allowed_domains = ["baidu.com"] # 重寫父類請求初始化發送方式 def parse(self, response): # 經過響應對象從新構建一個POST請求再次發送 return scrapy.FormRequest.from_response( response, formdata = {"post參數字典數據"}, callback = self.parse_response ) # 從新編寫響應數據處理函數 def parse_response(self, response): # 處理採集到的response數據 pass
(2) Response對象 Response對象在項目中的直接操做並非不少,參考源代碼以下:
# 部分代碼 class Response(object_ref): def __init__(self, url, status=200, headers=None, body='', flags=None, request=None): self.headers = Headers(headers or {}) self.status = int(status) # 響應碼 self._set_body(body) # 響應體 self._set_url(url) # 響應url self.request = request # 請求對象 self.flags = [] if flags is None else list(flags) @property def meta(self): try: return self.request.meta except AttributeError: raise AttributeError("Response.meta not available, this response " \ "is not tied to any request")
(3)案例操做:模擬CSDN登陸
scrapy startproject csdnspider
# coding:utf-8 import scrapy class CsdnSpider(scrapy.Spider): ''' CSDN登陸爬蟲處理類 ''' # 爬蟲名稱 name = "cs" # 初始登陸地址 start_urls = ["https://passport.csdn.net/account/login"] def parse(self, response): # 匹配登陸流水號 lt = response.xpath("//form[@id='fm1']/input[@type='hidden']/@value").extract()[1] # 發送post請求完成登陸 return scrapy.FormRequest.from_response( response, formdata = { "username": "15682808270", "password": "DAMUpython2016", "lt": lt, # "execution": "e2s1", # "_eventId": "submit" }, callback=self.parse_response ) def parse_response(self, response): # 獲得登陸後的數據,進行後續處理 with open("csdn.html", "w") as f: f.write(response.body)
(4). 深度採集數據:爬取智聯某工做崗位全部頁面工做數據
scrapy startproject zlspider
# -*- 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 ZhilianItem(scrapy.Item): ''' 定義採集數據的類型,該類型中,會封裝採集到的數據 繼承scrapy.Item類型,scrapy框架纔會調用內建函數繼續自動化操做 ''' # 經過scrapy.Field()定義屬性字段,每一個字段都是採集數據的一部分 job_name = scrapy.Field() company = scrapy.Field() salary = scrapy.Field() 建立數據庫,定義數據表,用於存儲數據 # 建立數據庫 DROP DATABASE py1709_spider; CREATE DATABASE py1709_spider DEFAULT CHARSET 'utf8'; USE py1709_spider; # 建立數據表 CREATE TABLE jobs( id INT AUTO_INCREMENT PRIMARY KEY, job_name VARCHAR(200), company VARCHAR(200), salary VARCHAR(50) ); SELECT COUNT(1) FROM jobs; SELECT * FROM jobs; TRUNCATE TABLE jobs;
在zlspider/zlspider/spider/文件夾中,建立zhilianspider.py文件,編輯爬蟲程序以下:
# coding:utf-8 # 引入scrapy模塊 import scrapy from ..items import ZhilianItem class ZhilianSpider(scrapy.Spider): ''' 智聯招聘數據採集爬蟲程序 須要繼承scrapy.Spider類型,讓scrapy負責調度爬蟲程序進行數據的採集 ''' # name屬性:爬蟲名稱 name = "zl" # allowed_domains屬性:限定採集數據的域名 allowed_domains = ["zhaopin.com"] # 起始url地址 start_urls = [ #"http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E5%8C%97%E4%BA%AC&kw=%E7%88%AC%E8%99%AB&sm=0&sg=cab76822e6044ff4b4b1a907661851f9&p=1", "http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E5%8C%97%E4%BA%AC%2b%E4%B8%8A%E6%B5%B7%2b%E5%B9%BF%E5%B7%9E%2b%E6%B7%B1%E5%9C%B3&kw=python&isadv=0&sg=7cd76e75888443e6b906df8f5cf121c1&p=1", ] def parse(self, response): ''' 採集的數據解析函數(響應數據解析函數) 主要用於進行響應數據的篩選:篩選目標數據分裝成Item對象 :param response: :return: ''' # # 再次從響應中獲取要進行下一次爬取的url地址[其餘頁面請求] # next_page = response.xpath("//div[@class='pagesDown']/ul/li/a/@href").extract() # # 循環處理請求 # for page in next_page: # page = response.urljoin(page) # # 從新發起請求採集下一組url地址的數據[第一個參數:發起的請求地址,第二個參數:請求數據一旦被採集~交個哪一個函數進行處理] # yield scrapy.Request(page, callback=self.parse_response) url = response.urljoin(self.start_urls[0]) yield scrapy.Request(url, callback=self.parse_response) def parse_response(self, response): # 篩選獲得工做列表 job_list = response.xpath("//div[@id='newlist_list_content_table']/table[position()>1]/tr[1]") # 循環獲取採集的字段信息 for job in job_list: # 崗位名稱 job_name = job.xpath("td[@class='zwmc']/div/a").xpath("string(.)").extract()[0] # 公司名稱 company = job.xpath("td[@class='gsmc']/a").xpath("string(.)").extract()[0] # 薪水 salary = job.xpath("td[@class='zwyx']").xpath("string(.)").extract()[0] # 封裝成item對象 item = ZhilianItem() item['job_name'] = job_name item['company'] = company item['salary'] = salary # 經過協程的方式移交給pipeline進行處理 yield item # 再次從響應中獲取要進行下一次爬取的url地址[其餘頁面請求] next_page = response.xpath("//div[@class='pagesDown']/ul/li/a/@href").extract() # 循環處理請求 for page in next_page: page = response.urljoin(page) # 從新發起請求採集下一組url地址的數據[第一個參數:發起的請求地址,第二個參數:請求數據一旦被採集~交個哪一個函數進行處理] yield scrapy.Request(page, callback=self.parse_response) 運行測試程序 在終端命令行窗口中,運行程序 scrapy crawl zl
查看數據庫中的數據記錄
備註:在這樣的深度採集數據時,首頁數據頗有可能會重複,因此,將數據解析函數分紅了兩個步驟執行,第一步經過parse()函數處理首頁地址增長到response.urljoin()中,而後經過parse_response()函數進行實際的數據採集工做,達到首頁數據去重的目的!
Scrapy框架針對深度爬蟲,提供了一種深度爬蟲的封裝類型scrapy.CrawlSpider,咱們本身定義開發的爬蟲處理類須要繼承該類型,才能使用scrapy提供封裝的各項深度爬蟲的功能
scrapy.CrawlSpider是從scrapy.Spider繼承並進行功能擴展的類型,在該類中,經過定義Url地址的提取規則,跟蹤鏈接地址,從已經採集獲得的響應數據中繼續提取符合規則的地址進行跟蹤爬取數據
部分源代碼以下:
class CrawlSpider(Spider): rules = () def __init__(self, *a, **kw): super(CrawlSpider, self).__init__(*a, **kw) self._compile_rules() # 1. 調用重寫父類的parse()函數來處理start_urls中返回的response對象 # 2. parse()則將這些response對象再次傳遞給了_parse_response()函數處理 # 2.1. _parse_response()函數中設置follow爲True,該參數用於打開是否跟進連接提取 # 3. parse將返回item和跟進了的Request對象 def parse(self, response): return self._parse_response(response, self.parse_start_url, cb_kwargs={}, follow=True) # 定義處理start_url中返回的response的函數,須要重寫 def parse_start_url(self, response): return [] # 結果過濾函數 def process_results(self, response, results): return results # 從response中抽取符合任一用戶定義'規則'的連接,並構形成Resquest對象返回 def _requests_to_follow(self, response): if not isinstance(response, HtmlResponse): return seen = set() # 循環獲取定義的url地址提取規則 for n, rule in enumerate(self._rules): # 獲得全部的提取規則列表 links = [l for l in rule.link_extractor.extract_links(response) if l not in seen] # 使用用戶指定的process_links處理每一個鏈接 if links and rule.process_links: links = rule.process_links(links) #將連接加入seen集合,爲每一個連接生成Request對象,並設置回調函數爲_repsonse_downloaded() for link in links: seen.add(link) # 構造Request對象,並將Rule規則中定義的回調函數做爲這個Request對象的回調函數 r = Request(url=link.url, callback=self._response_downloaded) r.meta.update(rule=n, link_text=link.text) # 對每一個Request調用process_request()函數。該函數默認爲indentify,即不作任何處理,直接返回該Request. yield rule.process_request(r) # 採集數據連接處理,從符合規則的rule中提取連接並返回item和request def _response_downloaded(self, response): rule = self._rules[response.meta['rule']] return self._parse_response(response, rule.callback, rule.cb_kwargs, rule.follow) # 解析response對象,經過callback回調函數解析處理,並返回request或Item對象 def _parse_response(self, response, callback, cb_kwargs, follow=True): # 首先判斷是否設置了回調函數。(該回調函數多是rule中的解析函數,也多是 parse_start_url函數) #若是設置了回調函數(parse_start_url()),那麼首先用parse_start_url()處理response對象, # 而後再交給process_results處理。返回cb_res的一個列表 if callback: #若是是parse調用的,則會解析成Request對象 #若是是rule callback,則會解析成Item cb_res = callback(response, **cb_kwargs) or () cb_res = self.process_results(response, cb_res) for requests_or_item in iterate_spider_output(cb_res): yield requests_or_item # 若是須要跟進,那麼使用定義的Rule規則提取並返回這些Request對象 if follow and self._follow_links: #返回每一個Request對象 for request_or_item in self._requests_to_follow(response): yield request_or_item # 規則過濾 def _compile_rules(self): def get_method(method): if callable(method): return method elif isinstance(method, basestring): return getattr(self, method, None) self._rules = [copy.copy(r) for r in self.rules] for rule in self._rules: rule.callback = get_method(rule.callback) rule.process_links = get_method(rule.process_links) rule.process_request = get_method(rule.process_request) # 連接跟蹤全局配置設置 def set_crawler(self, crawler): super(CrawlSpider, self).set_crawler(crawler) self._follow_links = crawler.settings.getbool('CRAWLSPIDER_FOLLOW_LINKS', True)
(1) LinkExtractor連接提取對象
LinkExtract類型,主要目的是用於定義連接的提取匹配方式
該類中的方法extract_link()用於從響應對象response中提取符合定義規則的連接
該類型只會被實例化一次,可是在每次採集獲得數據時重複調用
class scrapy.linkextractors.LinkExtractor( allow = (), # 正則表達式,符合規則的連接會提取 deny = (), # 正則表達式,負責規則的連接會排除 allow_domains = (), # 容許的域名 deny_domains = (), # 禁止的域名 deny_extensions = None, # 是否容許擴展 restrict_xpaths = (), # xpath表達式,和allow配合使用精確提取數據 tags = ('a','area'), # 標籤~ attrs = ('href'), # 指定提取的屬性 canonicalize = True, unique = True, # 惟一約束,是否去重 process_value = None )
上述的參數中,咱們能夠看到經過一個linkextractors.LinkExtractor對象,能夠定義各類提取規則,而且不須要考慮是否會將重複的連接添加到地址列表中
經過srapy shell作一個簡單的測試,首先打開智聯工做列表頁面,終端命令行執行以下命令:
scrapy shell "http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E5%8C%97%E4%BA%AC%2b%E4%B8%8A%E6%B5%B7%2b%E5%B9%BF%E5%B7%9E%2b%E6%B7%B1%E5%9C%B3&kw=python&isadv=0&sg=5b827b7808f548ad8261595837624f24&p=4"
此時scrapy就會自動從指定的地址中採集數據,幷包含在response變量中,打開了python命令行,導入LinkExtractor類型並定義提取規則:
# 導入LinkExtractor類型 >>> from linkextractors import LinkExtractor # 定義提取規則,包含指定字符的連接被提取 >>> links = LinkExtractor(allow=('7624f24&p=\d+'))
接下來,從響應數據中提取符合規則的超連接,執行extract_links()函數以下:
next_urls = links.extract_links(response)
打印next_urls,獲得以下結果:
[Link(url='http://sou.zhaopin.com/jobs/searchresult.ashx ?jl=%E5%8C%97%E4%BA%AC%2b%E4%B8%8A%E6%B5%B7%2b%E5%B9%BF%E 5%B7%9E%2b%E6%B7%B1%E5%9C%B3&kw=python&isadv=0&sg=5b827b7 808f548ad8261595837624f24&p=4',text=u'\u767b\u5f55', frag ment='', nofollow=True), Link(url='http://sou.zhaopin.com /jobs/searchresult.ashx?j l=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5% b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&sg=5b827b780 8f548ad8261595837624f24&p=3', text=u'\u4e0a\u4e00\u9875', fragment='', nofollow=False), Link(url='http://sou.zhaopi n.com/jobs/searchresult.ashx?j l=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5%b 7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&sg=5b827b7808 f548ad8261595837624f24&p=1', text='1', fragment='', nofoll ow=False), Link(url='http://sou.zhaopin.com/jobs/searchre sult.ashx?jl=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e 5%b9%bf%e5%b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&s g=5b827b7808f548ad8261595837624f24&p=2', text='2', fragme nt='', nofollow=False), Link(url='http://sou.zhaopin.com/ jobs/searchresult.ashx?j l=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5% b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&sg=5b827b780 8f548ad8261595837624f24&p=5', text='5', fragment='', nofo llow=False), Link(url='http://sou.zhaopin.com/jobs/search result.ashx?jl=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b %e5%b9%bf%e5%b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0 &sg=5b827b7808f548ad8261595837624f24&p=6', text='6', frag ment='', nofollow=False), Link(url='http://sou.zhaopin.co m/jobs/searchresult.ashx?j l=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5% b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&sg=5b827b780 8f548ad8261595837624f24&p=7', text='7', fragment='', nofo llow=False), Link(url='http://sou.zhaopin.com/jobs/search result.ashx?jl=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b %e5%b9%bf%e5%b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0 &sg=5b827b7808f548ad8261595837624f24&p=8', text='8', frag ment='', nofollow=False), Link(url='http://sou.zhaopin.co m/jobs/searchresult.ashx?j l=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5% b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&sg=5b827b780 8f548ad8261595837624f24&p=9', text='...', fragment='', no follow=False)]
咱們能夠很直觀的看到,全部符合規則的鏈接所有被提取了出來
(2) Rule規則對象
Rule對象是連接操做規則對象,主要定義了對於LinkExtractor類型提取的超連接url地址的操做行爲,能夠在一個爬蟲程序中定義多個Rule對象,包含在一個rules列表中便可
class scrapy.spiders.Rule( # LinkExtractor對象 link_extractor, # 回調函數,獲得數據庫以後調用的函數 callback = None, # 回調函數調用時傳遞的參數列表 cb_kwargs = None, # 是否從返回的響應數據中根據LinkExtractor繼續提取,通常選擇True follow = None, # 從LinkExtractor中提取的鏈接,會自動調用該選項指定的函數,用來進行超連接的篩選 process_links = None, # 指定每一個請求封裝處理時要調用的函數 process_request = None )
(3) 案例操做
智聯招聘深度爬蟲操做案例:
scrapy startproject zhilianspider2
# coding:utf-8 # 引入CrawlSpider, Rule, LinkExtractor模塊 from scrapy.linkextractors import LinkExtractor from scrapy.spider import CrawlSpider, Rule class ZhilianSpider(CrawlSpider): """ 智聯招聘深度爬蟲處理類 繼承scrapy.spiders.CrawlSpider類型 """ # 定義爬蟲名稱 name = "cs2" # 定義域名限制 allowed_domains = ["zhaopin.com"] # 定義起始地址 start_urls = ("http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E5%8C%97%E4%BA%AC%2b%E4%B8%8A%E6%B5%B7%2b%E5%B9%BF%E5%B7%9E%2b%E6%B7%B1%E5%9C%B3&kw=python&isadv=0&sg=5b827b7808f548ad8261595837624f24&p=1",) # 定義提取規則 links = LinkExtractor( allow=("5837624f24&p=\d+") ) # 定義操做規則 rules = [ # 定義一個操做規則 Rule(links, follow=True, callback='parse_response'), ] # 定義數據處理函數 def parse_response(self, response): # 提取數據 job_list = response.xpath("//div[@id='newlist_list_content_table']/table[@class='newlist'][position()>1]") # 循環篩選數據 for job in job_list: job_name = job.xpath("tr[1]/td[@class='zwmc']/div/a").xpath("string(.)").extract()[0] print job_name print("*************************************************")
在終端命令行中執行以下命令運行爬蟲程序
scrapy crawl cs2
能夠在控制檯看到具體的爬取信息,對於提取的數據所有進行了跟蹤處理
.. [scrapy.core.engine] DEBUG: Crawled (200) <GET http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%e5%8c%97%e4%ba%ac%2 b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5%b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&sg=5b827b7808f548ad8261595837624f24&p=13> (referer: http ://sou.zhaopin.com/jobs/searchresult.ashx?jl=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5%b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python &isadv=0&sg=5b827b7808f548ad8261595837624f24&p=9) .... 圖像算法工程師 軟件測試工程師 ******************************************************************** 軟件測試經理 高級軟件測試工程師 ...... 'scheduler/enqueued/memory': 17, 'spider_exceptions/IOError': 3, 'spider_exceptions/UnicodeEncodeError': 1, 'start_time': datetime.datetime(2018, 1, 17, 4, 33, 38, 441000)} 2018-01-17 12:35:56 [scrapy.core.engine] INFO: Spider closed (shutdown)