文章原文地址:https://python.gotrained.com/scrapy-tutorial-web-scraping-craigslist/
爬的目標(biāo)網(wǎng)站;Craigslist
爬蟲(chóng)初步
安裝:pip install scrapy,用蘋(píng)果或李牛的高端用戶(hù)需要在前面加上sudo
創(chuàng)建項(xiàng)目
Scrapy startproject craigslist
craigslist是項(xiàng)目名稱(chēng)。
創(chuàng)建一只小蛛蛛(SPIDER)
在終端進(jìn)入文件夾craigslist,使用genspider命令,建立一個(gè)小蛛蛛。
如在這個(gè)項(xiàng)目中,我們用如下命令:
scrapy genspider jobs https://newyork.craigslist.org/search/egr
編輯小蛛蛛
在craiglist文件夾,你可以看出項(xiàng)目的文件情況:
現(xiàn)在你會(huì)發(fā)現(xiàn)在,在spiders文件夾里,有一個(gè)名為job.py的文件,就是我們剛剛創(chuàng)建的小蛛蛛。
打開(kāi)編輯器,開(kāi)始編輯這個(gè)東東:
# -*- coding: utf-8 -*-
import scrapy
class JobsSpider(scrapy.Spider):
? ? name = "jobs"
? ? allowed_domains = ["craigslist.org"]
? ? start_urls = ['https://newyork.craigslist.org/search/egr']
? ? def parse(self, response):
? ? ? ? pass
解釋一個(gè)這個(gè)文件:
name,是我們給這個(gè)小蛛蛛起的名字,這個(gè)小蛛蛛名為jobs
allowed-domains列出了小蛛蛛可以活動(dòng)的范圍
start_urls列出了一個(gè)或多個(gè)小蛛蛛開(kāi)起運(yùn)動(dòng)的起點(diǎn)。
Parse是小蛛蛛的主函數(shù),注意,不要,千萬(wàn)不要改這個(gè)函數(shù)的名字。如有所需,你可以增加其他函數(shù)。
提示:如果你用和講義相同的方法創(chuàng)建了小蛛蛛,它會(huì)自己在start_urls中加上http://,一定要注意檢查,如果出現(xiàn)了重復(fù)的http://,蟲(chóng)子不會(huì)正常運(yùn)動(dòng)。
最簡(jiǎn)單的一只,單項(xiàng)爬蟲(chóng)
刪除pass,在函數(shù)中加入以下行:
titles = response.xpath('//a[@class="result-title hdrlnk"]/text()').extract()
啥意思?
titles是根據(jù)一定的規(guī)則解析出來(lái)的內(nèi)容組成的列表
response是一個(gè)命令,獲取整個(gè)頁(yè)面的HTML。如果:
print(response)
你得到什么結(jié)果?
<200 http://*****>
星號(hào)代表你請(qǐng)求的鏈接。
如果:
print(response.body)
你則會(huì)得到頁(yè)面主體的代碼。
你可以用xpath()來(lái)解析。命令為:
response.xpath()
Xpath是個(gè)復(fù)雜的話題,但有個(gè)簡(jiǎn)單的方法讓你得到相應(yīng)的xpath,打開(kāi)你的Chrone瀏覽器,復(fù)制剛才的鏈接,選取相應(yīng)的頁(yè)面元素,單擊右鍵,選取“檢查”(inspect)
你就會(huì)看到這部分元素的HTML代碼,如:
<a href="/brk/egr/6085878649.html" data-id="6085878649" class="result-title hdrlnk">Chief Engineer</a>
這是一個(gè)鏈接,鏈接文字是“Chief Engineer” ,可以用text()查看。
其class被標(biāo)為:result-title hdrlnk
用extract()可以獲取列表中的項(xiàng)。
我們要把title項(xiàng)顯示出來(lái):
print(titles)
于是這個(gè)小蛛蛛的完整代碼是:
# -*- coding: utf-8 -*-
import scrapy
class JobsSpider(scrapy.Spider):
? ? name = "jobs"
? ? allowed_domains = ["craigslist.org"]
? ? start_urls = ['https://newyork.craigslist.org/search/egr']
? ? def parse(self, response):
? ? ? ? titles = response.xpath('//a[@class="result-title hdrlnk"]/text()').extract()
? ? ? ? print(titles)
動(dòng)起來(lái),蟲(chóng)子
在終端項(xiàng)目文件夾下,輸入以下命令,開(kāi)動(dòng)蟲(chóng)子。
scrapy crawl jobs
Jobs是這個(gè)蟲(chóng)子的名字。
終端將列表結(jié)果打印出來(lái)。
接下來(lái),我們可以用yield命令,將列表中的內(nèi)容取出來(lái),放入一個(gè)字典:
for title in titles:
? ? yield {'Title': title}
于是這個(gè)蟲(chóng)子的完整美圖如下:
# -*- coding: utf-8 -*-
import scrapy
class JobsSpider(scrapy.Spider):
? ? name = "jobs"
? ? allowed_domains = ["craigslist.org"]
? ? start_urls = ['https://newyork.craigslist.org/search/egr']
? ? def parse(self, response):
? ? ? ? titles = response.xpath('//a[@class="result-title hdrlnk"]/text()').extract()
? ? ? ? for title in titles:
? ? ? ? ? ? yield {'Title': title}
將爬取的結(jié)果存到CSV等類(lèi)型的文件里
可以在前述爬蟲(chóng)運(yùn)行命令后加上 -o 指定文件名,將結(jié)果存入相應(yīng)文件,文件類(lèi)型包括csv,json,xml。
如
scrapy crawl jobs -o result-titles.csv
第二只,單頁(yè)爬蟲(chóng)
如果你想得到與工作有關(guān)的其他項(xiàng)目,你是不是應(yīng)該多寫(xiě)幾個(gè)單項(xiàng)爬蟲(chóng),來(lái)讓它們完成不同的工作?
答案是否定的,你不必如此。你可以把頁(yè)面每一個(gè)工作相關(guān)的元素的容器抓取下來(lái),解出其中的項(xiàng)目。
例如,在這個(gè)頁(yè)上,https://newyork.craigslist.org/search/egr
你查看元素,會(huì)看到如相內(nèi)容:
在頁(yè)上,有列表項(xiàng)(li)標(biāo)志,前面有個(gè)小三角,點(diǎn)擊,可以展開(kāi)每個(gè)列表項(xiàng),在其中,包含你需要的與該項(xiàng)工作有關(guān)的全部信息,你可以把這個(gè)列表項(xiàng)視為封套或容器。
Li標(biāo)簽的class被指定為“result-row”,在其中,包括一個(gè)鏈接,還有一個(gè)段落標(biāo)簽(p),這個(gè)標(biāo)簽的class被指定為”result-info”,我們把這人容器拿出來(lái),就需要在爬蟲(chóng)函數(shù)里寫(xiě)下:
jobs = response.xpath('//p[@class="result-info"]')
然后解出其中的title項(xiàng)目:
for job in jobs:
? ? title = job.xpath('a/text()').extract_first()
? ? yield{'Title':title}
這是一個(gè)循環(huán),其中,你無(wú)需再用response了,你使用了一個(gè)名為job的選擇項(xiàng)。在解析容器時(shí),我們用的是//,指示xpath是從<html>直到<p>,而現(xiàn)在,我們則不用//,因?yàn)楝F(xiàn)在的選擇是以jobs為基礎(chǔ)的。你也可以用.//
我們使用extrat_first(),而不是extract(), 因?yàn)槲覀冎幌氲靡淮蔚玫揭粋€(gè)值。
現(xiàn)在我們添加其他項(xiàng)目:
for job in jobs:
? ? title = job.xpath('a/text()').extract_first()
? ? address = job.xpath('span[@class="result-meta"]/span[@class="result-hood"]/text()').extract_first("")[2:-1]
? ? relative_url = job.xpath('a/@href').extract_first()
? ? absolute_url = response.urljoin(relative_url)
? ? yield{'URL':absolute_url, 'Title':title, 'Address':address}
我們加入了address等項(xiàng)目。注意,通過(guò)xpath我們得到的是一個(gè)相對(duì)鏈接,我們需要用response.urljion()轉(zhuǎn)換成完整的鏈接。
第三只蟲(chóng)子:多頁(yè)蟲(chóng)子
在內(nèi)容比較多時(shí),網(wǎng)站采取了分頁(yè)技術(shù),這樣,我們有必要通過(guò)獲取“下一頁(yè)”的地址,將所有的項(xiàng)目都拿下來(lái)。
在這個(gè)頁(yè)面上,下一頁(yè)next的HTML代碼是這樣的:
<a href="/search/egr?s=120" class="button next" title="next page">next > </a>
于是,我們?cè)诘诙幌x(chóng)子的基礎(chǔ)上,加入以下代碼,取得下一頁(yè)的鏈接,傳到主函數(shù)self.parse,讓它繼續(xù)獲取其中的項(xiàng)目。
relative_next_url = response.xpath('//a[@class="button next"]/@href').extract_first()
absolute_next_url = response.urljoin(relative_next_url)
yield Request(absolute_next_url, callback=self.parse)
你也可以不寫(xiě)callback=self.parse,因?yàn)檫@是默認(rèn)的。
另外,由于使用了Request,我們必須將它引入:
From scrapy import Request
注意,R是大寫(xiě)。
運(yùn)行下,可以得到更多的結(jié)果。
第四只蟲(chóng)子 獲取詳細(xì)頁(yè)內(nèi)容
下面,我們要讓小蛛蛛打開(kāi)其獲取的鏈接,然后從中取出有關(guān)工作的描述。在第三只蟲(chóng)子基礎(chǔ)上,我們繼續(xù)以下內(nèi)容。第三只蟲(chóng)子讓我們得到了絕對(duì)鏈接,標(biāo)題和地址:
yield{'URL':absolute_url, 'Title':title, 'Address':address}
我們要建立一個(gè)函數(shù),把已經(jīng)取得的鏈接傳遞給它,讓它獲得詳細(xì)頁(yè),這個(gè)函數(shù)我們將它命名為parse_page()。我們還將用meta.get()傳遞已經(jīng)取得的項(xiàng)目。
yield Request(absolute_url, callback=self.parse_page, meta={'URL': absolute_url, 'Title': title, 'Address':address})
這個(gè)函數(shù)總體是這樣的:
def parse_page(self, response):
? ? url = response.meta.get('URL')
? ? title = response.meta.get('Title')
? ? address = response.meta.get('Address')
? ? description = "".join(line for line in response.xpath('//*[@id="postingbody"]/text()').extract())
? ? compensation = response.xpath('//p[@class="attrgroup"]/span/b/text()')[0].extract()
? ? employment_type = response.xpath('//p[@class="attrgroup"]/span/b/text()')[1].extract()
? ? yield{'URL': url, 'Title': title, 'Address':address, 'Description':description}
你已經(jīng)注意到了,我們加入了一個(gè)變量,discription,由于工作描述可能多于一個(gè)段落,所以要用jion()把它們合起來(lái)。
同樣的,我們加入comensation,以及employment_type。
設(shè)置settings.py
可以設(shè)置CSV的輸出:
FEED_EXPORT_FIELDS = ['Title','URL', 'Address', 'Compensation', 'Employment Type','Description']
可以設(shè)定代理,讓你的蟲(chóng)子運(yùn)動(dòng)看起來(lái)像個(gè)正常的瀏覽行為。
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1'
可以設(shè)定遲延,如3秒或在一定區(qū)間。
完整代碼:
import scrapy
from scrapy import Request
class JobsSpider(scrapy.Spider):
? ? name = "jobs"
? ? allowed_domains = ["craigslist.org"]
? ? start_urls = ["https://newyork.craigslist.org/search/egr"]
? ? def parse(self, response):
? ? ? ? jobs = response.xpath('//p[@class="result-info"]')
? ? ? ? for job in jobs:
? ? ? ? ? ? relative_url = job.xpath('a/@href').extract_first()
? ? ? ? ? ? absolute_url = response.urljoin(relative_url)
? ? ? ? ? ? title = job.xpath('a/text()').extract_first()
? ? ? ? ? ? address = job.xpath('span[@class="result-meta"]/span[@class="result-hood"]/text()').extract_first("")[2:-1]
? ? ? ? ? ? yield Request(absolute_url, callback=self.parse_page, meta={'URL': absolute_url, 'Title': title, 'Address':address})
? ? ? ? relative_next_url = response.xpath('//a[@class="button next"]/@href').extract_first()
? ? ? ? absolute_next_url = "https://newyork.craigslist.org" + relative_next_url
? ? ? ? yield Request(absolute_next_url, callback=self.parse)
? ? def parse_page(self, response):
? ? ? ? url = response.meta.get('URL')
? ? ? ? title = response.meta.get('Title')
? ? ? ? address = response.meta.get('Address')
? ? ? ? description = "".join(line for line in response.xpath('//*[@id="postingbody"]/text()').extract())
? ? ? ? compensation = response.xpath('//p[@class="attrgroup"]/span[1]/b/text()').extract_first()
? ? ? ? employment_type = response.xpath('//p[@class="attrgroup"]/span[2]/b/text()').extract_first()
? ? ? ? yield{'URL': url, 'Title': title, 'Address':address, 'Description':description, 'Compensation':compensation, 'Employment Type':employment_type}