話說python爬蟲界,有個(gè)非常知名的框架Scrapy。異步爬取,使用簡單,功能強(qiáng)大。小編學(xué)習(xí)之,練習(xí)之。貝克街,一個(gè)推理愛好者論壇網(wǎng)站,用戶數(shù)據(jù)量12W左右,很適合Scrapy學(xué)習(xí)練習(xí)爬取。本篇前半部分會介紹一點(diǎn)點(diǎn)基礎(chǔ),畢竟要照顧小白同學(xué)們嘛~
Tip:本文僅供學(xué)習(xí)與交流,切勿用于非法用途!??!
01. 寫在前面的話
本博客在編寫代碼的同時(shí),會簡單介紹Scrapy這個(gè)框架。相較于小編寫的前兩篇爬蟲博客,本篇博客爬取的數(shù)據(jù)量較大。
在寫代碼之前,我想說下貝克街這個(gè)網(wǎng)站。在兩三年前,我上過幾天這個(gè)網(wǎng)站,當(dāng)時(shí)好像也就5、6萬人,現(xiàn)在發(fā)展到12W多用戶挺不容易的。一群推理愛好者的精神家園。本網(wǎng)站好像也沒什么反爬措施,在再次申明免責(zé)聲明的同時(shí),小編懇請大家,僅僅學(xué)習(xí)交流,不要把人家服務(wù)器搞崩了哦。同時(shí),本博客爬取的鏈接都在貝克街robots文件要求以外,絕對ok~
?

如果大家在學(xué)習(xí)中遇到困難,想找一個(gè)python學(xué)習(xí)交流環(huán)境,可以加入我們的python圈,裙號930900780,可領(lǐng)取python學(xué)習(xí)資料,會節(jié)約很多時(shí)間,減少很多遇到的難題。
02. Scrapy安裝
首先需要安裝 lxml庫
pip install lxml
復(fù)制代碼
然后分別去以下兩個(gè)鏈接,安裝和自己本機(jī)python版本一致的 whl 文件
pywin32 twisted
接著安裝上面那兩個(gè)庫
pip install 你完整的pywin32whl 文件路徑
pip install 你完整的twistedwhl 文件路徑
例如:
pip install C:\Users\Administrator\Downloads\pywin32-228-cp38-cp38-win_amd64.whl
復(fù)制代碼
最后,可以安裝Scrapy了
pip install scrapy
復(fù)制代碼
查看一下是否安裝成功
?

以上,我們就安裝完畢了。
03. 項(xiàng)目結(jié)構(gòu)簡介
Scrapy為我們提供了一些好用的命令行,比如上一節(jié)的scrapy -h。我們還可以使用命令行創(chuàng)建項(xiàng)目
scrapy startproject beikejie
復(fù)制代碼
然后,我們得到了如下的項(xiàng)目結(jié)構(gòu)
?

簡單介紹下幾個(gè)文件
BeiKeJieSpider.py:一個(gè)爬蟲,咱們代碼主要寫和里面,后面會詳細(xì)說。 items.py:數(shù)據(jù)實(shí)例,一個(gè)數(shù)據(jù)結(jié)構(gòu)。 pipelines.py:數(shù)據(jù)爬取之后,進(jìn)行數(shù)據(jù)清晰儲存的地方。 middlewares.py:一些中間件,這里可以設(shè)置每次請求前的代理、cookie等。本項(xiàng)目不使用這個(gè)模塊,畢竟人家沒什么反爬措施嘛。 settings.py:一些項(xiàng)目的設(shè)置,可以設(shè)置很多東西,包括 pipelines 內(nèi)管道的優(yōu)先級等等,后面用到的地方會詳細(xì)說。 scrapy.cfg:一些全局設(shè)置,本項(xiàng)目基本不適用。
04. 需求分析
本次爬取的目的,是獲取貝克街所有的用戶信息
思路:一批網(wǎng)站的大V,爬取他們的關(guān)注列表和粉絲列表,然后再以某個(gè)關(guān)注者或者粉絲為起點(diǎn),繼續(xù)爬取其關(guān)注列表和粉絲列表。這樣可以爬取大部分用戶,并不能爬取全部,因?yàn)楫吘箍赡軙袩o關(guān)注無粉絲的用戶的用戶孤島。
所以我們要做的是:
根據(jù)某用戶主頁,獲取一些用戶信息。
獲取某用戶的關(guān)注列表,獲取每個(gè)關(guān)注者主頁,并執(zhí)行第一步。
獲取某用戶的粉絲列表,獲取每個(gè)粉絲主頁,并執(zhí)行第一步。
05. 獲取用戶信息
首先,我們需要編寫根據(jù)某用戶主頁,獲取用戶信息,并儲存mongo功能。
?

先在 BeiKeJieSpider中編寫代碼,這個(gè)是爬蟲主要邏輯編寫的地方。
class BeiKeJieSpider(scrapy.Spider):
? ? name = "beikejie"
? ? logger = logging.getLogger()
? ? allowed_domains = ['tuilixy.net']
cookies = {'你自己的cookie'}
? ? def start_requests(self):
? ? ? ? urls = [
? ? ? ? ? ? 'http://www.tuilixy.net/space-uid-45001.html'
? ? ? ? ]
? ? ? ? for url in urls:
? ? ? ? ? ? yield scrapy.Request(url=url, callback=self.parse)
? ? def parse(self, response):
? ? ? ? item = self.main_page_parse(response)
? ? ? ? yield item
? ? # 解析主頁數(shù)據(jù)
? ? def main_page_parse(self, response):
? ? ? ? select = Selector(response)
? ? ? ? uid = select.xpath('//*[@id="main"]/div[2]/div/div/div[2]/div[1]/h1/span/text()').get(default='- -').split(' ')[1]
? ? ? ? name = select.xpath('//*[@id="main"]/div[2]/div/div/div[2]/div[1]/h1/text()').get(default='-')
? ? ? ? register_time = select.xpath('//*[@id="pbbs"]/tbody/tr[2]/td[2]/text()').get(default='-')
? ? ? ? follower_numbers = select.xpath('//*[@id="ct"]/div[2]/div[1]/ul/a[1]/li/h4/text()').get(default=0)
? ? ? ? fans_numbers = select.xpath('//*[@id="ct"]/div[2]/div[1]/ul/a[2]/li/h4/text()').get(default=0)
? ? ? ? item = BeikejieItem()
? ? ? ? item['uid'] = uid
? ? ? ? item['name'] = name
? ? ? ? item['register_time'] = register_time
? ? ? ? item['follower_numbers'] = follower_numbers
? ? ? ? item['fans_numbers'] = fans_numbers
? ? ? ? return item
復(fù)制代碼
上面我們說了,這個(gè)類實(shí)際上就是一直爬蟲,name就是爬蟲的名字,allowed_domains是此爬蟲可以爬取的域名,start_requests是起始爬取頁面,這里面urls就是那些大V的主頁,為了方便說明,我們這邊從一個(gè)大V的主頁開始。
爬取完了,進(jìn)入回調(diào)函數(shù)parse進(jìn)行解析,注意此方法內(nèi)返回?cái)?shù)據(jù)使用的是yield,此關(guān)鍵字實(shí)際上是生成了一個(gè)迭代器,再次進(jìn)入函數(shù)時(shí),會接著從yield處開始執(zhí)行,后面會有妙用。然后解析完了,會返回一個(gè)BeikejieItem實(shí)例,因?yàn)榉祷氐氖莍tem,所以會讓pipelines.py進(jìn)行進(jìn)一步處理。
那么們先看下數(shù)據(jù)結(jié)構(gòu)BeikejieItem所在的items.py文件吧。
class BeikejieItem(scrapy.Item):
? ? uid = scrapy.Field()
? ? name = scrapy.Field()
? ? register_time = scrapy.Field()
? ? follower_numbers = scrapy.Field(serializer=int)
? ? fans_numbers = scrapy.Field(serializer=int)
復(fù)制代碼
需要繼承scrapy.Item,然后定義一些需要儲存的數(shù)據(jù)字段??梢钥吹?,字段還可以設(shè)置儲存類型。
有了數(shù)據(jù)結(jié)構(gòu),那么接著看下管道處理pipelines.py文件吧。
class MongoPipeline:
? ? def __init__(self, mongo_uri, mongo_db):
? ? ? ? self.mongo_uri = mongo_uri
? ? ? ? self.mongo_db = mongo_db
? ? ? ? self.mongo_collection = None
? ? @classmethod
? ? def from_crawler(cls, crawler):
? ? ? ? return cls(
? ? ? ? ? ? mongo_uri=crawler.settings.get('MONGO_URI'),
? ? ? ? ? ? mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
? ? ? ? )
? ? def open_spider(self, spider):
? ? ? ? self.client = MongoClient(self.mongo_uri)
? ? ? ? self.db = self.client[self.mongo_db]
? ? ? ? self.mongo_collection = self.db['beikejie']
? ? def close_spider(self, spider):
? ? ? ? self.client.close()
? ? def process_item(self, item, spider):
? ? ? ? self.mongo_collection.insert_one(ItemAdapter(item).asdict())
? ? ? ? return item
class DuplicatesPipeline:
? ? def __init__(self):
? ? ? ? self.ids_seen = set()
? ? def process_item(self, item, spider):
? ? ? ? adapter = ItemAdapter(item)
? ? ? ? if adapter['uid'] in self.ids_seen:
? ? ? ? ? ? raise DropItem(f"Duplicate item found: {item!r}")
? ? ? ? else:
? ? ? ? ? ? self.ids_seen.add(adapter['uid'])
? ? ? ? ? ? return item
? ? def close_spider(self, spider):
? ? ? ? print(self.ids_seen)
復(fù)制代碼
這里有兩個(gè)Pipeline分別都會處理傳進(jìn)來的item,優(yōu)先級待會會被配置到文件settings.py里面。
先看下這兩個(gè)Pipeline,DuplicatesPipeline是去重用的,內(nèi)存中維護(hù)了一個(gè)set,存放存入庫中的uid,避免重復(fù),如果存在就報(bào)錯(cuò)。其實(shí)本博客的去重不太完美,首先內(nèi)存問題,一個(gè)12w個(gè)元素的大集合維護(hù)是個(gè)問題,并且沒有持久化(當(dāng)然可以最開始,從mongo中讀出所有庫中已存在的uid),沒有考慮到分布式。未來后期,去重應(yīng)該交給Redis這種緩存中間件,這邊只是演示作用。
第二個(gè)MongoPipeline,是進(jìn)行mongo儲存,細(xì)品一番~ 首先執(zhí)行類方法from_crawler,從配置文件settings.py中讀取mongo庫的信息,然后執(zhí)行__init__初始化信息,初始化實(shí)例屬性mongo_uri、mongo_db 。接著執(zhí)行open_spider,這個(gè)方法開啟一個(gè)爬蟲的時(shí)候會被執(zhí)行,生成真正的數(shù)據(jù)庫鏈接mongo_collection。每次進(jìn)入管道,都會執(zhí)行process_item方法,進(jìn)行數(shù)據(jù)插入操作。close_spider這個(gè)方法顧名思義,只有關(guān)閉一個(gè)爬蟲的時(shí)候會執(zhí)行,將數(shù)據(jù)庫鏈接關(guān)閉。
去重和存庫的管道都介紹完了,下面查看一下配置信息,looksettings.py。
ITEM_PIPELINES = {
? 'beikejie.pipelines.DuplicatesPipeline': 299,
? 'beikejie.pipelines.MongoPipeline': 300
}
MONGO_URI = '127.0.0.1:27017'
MONGO_DATABASE = 'pjjlt'
復(fù)制代碼
ITEM_PIPELINES 就是開啟以上的兩個(gè)管道,數(shù)字越小,優(yōu)先級越高,所以去重優(yōu)先于存庫管道,符合邏輯。下面是數(shù)據(jù)庫配置信息。當(dāng)然,settings.py還有很多配置信息,有用到你可以自己看撒~
以上,我們實(shí)現(xiàn)了一個(gè)用戶主頁數(shù)據(jù)的讀取和儲存。下面我們?nèi)プ龅诙胶偷谌健?/p>
06. 獲取關(guān)注者列表
繼續(xù)回到BeiKeJieSpider這個(gè)類,我們繼續(xù)爬取關(guān)注者列表。
?

可以看出,關(guān)注者列表是分頁的,我們可以根據(jù)下一頁按鈕獲取下一頁url進(jìn)行翻頁操作。每頁關(guān)注者列表都可以獲取到它們的uid,從而我們又可以去拼湊出每個(gè)關(guān)注者的主頁url。獻(xiàn)上代碼~
cookies = {'你自己的cookie'}
? ? def parse(self, response):
? ? ? ? item = self.main_page_parse(response)
? ? ? ? yield item
? ? ? ? uid = item['uid']
? ? ? ? try:
? ? ? ? ? ? # # 點(diǎn)擊關(guān)注連接,進(jìn)入關(guān)注頁,爬取每個(gè)關(guān)注者的信息 http://www.tuilixy.net/home.php?mod=follow&uid=45001&do=following
? ? ? ? ? ? follower_url = f'http://www.tuilixy.net/home.php?mod=follow&uid={uid}&do=following'
? ? ? ? ? ? yield scrapy.Request(url=follower_url, cookies=self.cookies, callback=self.follower_parse)
? ? ? ? except Exception as e:
? ? ? ? ? ? logging.error("失?。簎id:"+uid+"\n錯(cuò)誤原因是: "+str(e))
? ? def follower_parse(self, response):
? ? ? ? logging.info('開始爬取關(guān)注列表,'+response.url)
? ? ? ? doc = pq(response.text)
? ? ? ? lis = doc('.flw_ulist.prw.plw').children('.ptf.pbf.cl')
? ? ? ? for li in lis:
? ? ? ? ? ? try:
? ? ? ? ? ? ? ? url_end = pq(li)('.z.avt.w60.br4').attr('href')
? ? ? ? ? ? ? ? if url_end:
? ? ? ? ? ? ? ? ? ? url = 'http://www.tuilixy.net/'+url_end
? ? ? ? ? ? ? ? ? ? yield scrapy.Request(url=url, callback=self.parse)
? ? ? ? ? ? except Exception as e:
? ? ? ? ? ? ? ? logging.error("爬取關(guān)注失?。簎rl:" + response.url +"\n錯(cuò)誤原因是: "+str(e))
? ? ? ? try:
? ? ? ? ? ? # 翻頁
? ? ? ? ? ? turn_page_url_start = doc('.nxt').attr('href')
? ? ? ? ? ? if turn_page_url_start:
? ? ? ? ? ? ? ? turn_page_url = 'http://www.tuilixy.net/'+turn_page_url_start
? ? ? ? ? ? ? ? yield scrapy.Request(url=turn_page_url, cookies=self.cookies, callback=self.follower_parse)
? ? ? ? except Exception as e:
? ? ? ? ? ? logging.error("爬取關(guān)注失?。簎rl:" + response.url+"\n錯(cuò)誤原因是: "+str(e))
復(fù)制代碼
首先,補(bǔ)充下我們上面寫的parse方法,儲存完某用戶的信息后,爬取這個(gè)用戶的關(guān)注者列表的第一頁。獲取到第一頁關(guān)注著列表后執(zhí)行回調(diào)函數(shù)follower_parse。
follower_parse主要干了兩件事,獲取本頁所有用戶的uid,拼湊其用戶主頁url,并且執(zhí)行獲取用戶主頁的回調(diào)函數(shù)parse。(就是去做我們需求分析的第一步)。第二件事就是,獲取下一頁url,進(jìn)行翻頁操作,獲取下一頁關(guān)注者列表,并執(zhí)行獲取關(guān)注者列表的回調(diào)函數(shù)follower_parse。如此往復(fù),知道該用戶的關(guān)注者列表被翻到最后一步。
請求關(guān)注者列表需要加入cookie,這個(gè)需要你自己從瀏覽器獲取(需要登錄,都爬人家了,不得注冊一個(gè)賬號嘛,嗯哼?)。值得一提的是,Scrapy的Request方法,設(shè)置cookie必須顯示設(shè)置,不能通過將cookie放到headers中!這個(gè)知識點(diǎn)消耗了小編好多時(shí)間找問題,害,還是太菜。。知道點(diǎn)開Request源碼。
?

以上,我們就完成了某用戶的關(guān)注者列表的爬取。
07. 獲取粉絲列表
和爬取關(guān)注者邏輯一樣,直接上代碼吧。
? ? def parse(self, response):
? ? ? ? item = self.main_page_parse(response)
? ? ? ? yield item
? ? ? ? uid = item['uid']
? ? ? ? try:
? ? ? ? ? ? # # 點(diǎn)擊關(guān)注連接,進(jìn)入關(guān)注頁,爬取每個(gè)關(guān)注者的信息 http://www.tuilixy.net/home.php?mod=follow&uid=45001&do=following
? ? ? ? ? ? follower_url = f'http://www.tuilixy.net/home.php?mod=follow&uid={uid}&do=following'
? ? ? ? ? ? yield scrapy.Request(url=follower_url, cookies=self.cookies, callback=self.follower_parse)
? ? ? ? ? ? # # 點(diǎn)擊粉絲連接,進(jìn)入粉絲頁,爬取每個(gè)粉絲的信息 http://www.tuilixy.net/home.php?mod=follow&uid=45001&do=follower
? ? ? ? ? ? fans_url = f'http://www.tuilixy.net/home.php?mod=follow&uid={uid}&do=follower'
? ? ? ? ? ? yield scrapy.Request(url=fans_url, cookies=self.cookies, callback=self.fans_parse)
? ? ? ? except Exception as e:
? ? ? ? ? ? logging.error("失?。簎id:"+uid+"\n錯(cuò)誤原因是: "+str(e))
? ? def fans_parse(self, response):
? ? ? ? logging.info('開始爬取粉絲列表,'+response.url)
? ? ? ? doc = pq(response.text)
? ? ? ? lis = doc('.flw_ulist.prw.plw').children('.ptf.pbf.cl')
? ? ? ? for li in lis:
? ? ? ? ? ? try:
? ? ? ? ? ? ? ? url_end = pq(li)('.z.avt.w60.br4').attr('href')
? ? ? ? ? ? ? ? if url_end:
? ? ? ? ? ? ? ? ? ? url = 'http://www.tuilixy.net/'+url_end
? ? ? ? ? ? ? ? ? ? yield scrapy.Request(url=url, callback=self.parse)
? ? ? ? ? ? except Exception as e:
? ? ? ? ? ? ? ? logging.error("爬取粉絲失?。簎rl:" + response.url+"\n錯(cuò)誤原因是: "+str(e))
? ? ? ? # 翻頁
? ? ? ? try:
? ? ? ? ? ? turn_page_url_start = doc('.nxt').attr('href')
? ? ? ? ? ? if turn_page_url_start:
? ? ? ? ? ? ? ? turn_page_url = 'http://www.tuilixy.net/'+turn_page_url_start
? ? ? ? ? ? ? ? yield scrapy.Request(url=turn_page_url, cookies=self.cookies, callback=self.fans_parse)
? ? ? ? except Exception as e:
? ? ? ? ? ? logging.error("爬取粉絲失敗:url:" + response.url+"\n錯(cuò)誤原因是: "+str(e))
復(fù)制代碼
08. 運(yùn)行
邏輯寫完了,加上寫try、except,還有關(guān)鍵性注釋,利用Scrapy命令行操作,開始跑吧。
scrapy crawl beikejie
復(fù)制代碼
crawl就是執(zhí)行一個(gè)爬蟲,后面的參數(shù)是爬蟲的name。
由于小編原始大V url只有一條,所以只爬了部分?jǐn)?shù)據(jù),只有16429個(gè)用戶信息,大約耗時(shí)將近2個(gè)小時(shí)。你可以多選幾個(gè)大V,選的越多,數(shù)據(jù)就會越無限接近12W。后期我們還可以利用分布式,多開幾個(gè)scrapy實(shí)例,提高爬取速度。
?

09. 結(jié)束
相比于前面兩篇博客,這篇Scrapy數(shù)據(jù)量大,代碼編寫時(shí)間也較長,希望各位讀者小伙伴會喜歡~
喜歡的小伙伴,點(diǎn)個(gè)贊再走吧,您的支持,小編感激不盡。
最后多說一句,想學(xué)習(xí)Python可聯(lián)系小編,這里有我自己整理的整套python學(xué)習(xí)資料和路線,想要這些資料的都可以進(jìn)q裙930900780領(lǐng)取。
本文章素材來源于網(wǎng)絡(luò),如有侵權(quán)請聯(lián)系刪除。