1.
過年也沒啥事干,繼續(xù)搗鼓爬蟲土铺。開始是準備爬豆瓣電影的胶滋,豆瓣存在一些反爬機制,爬一會就爬不動了悲敷。當然后面是突破了這個限制究恤,使用隨機bid,設置cookie后德。據說會出現驗證碼部宿,我爬了幾萬部電影也沒有出現這個問題。初期的想法是使用代理ip瓢湃,網絡上的免費代理ip大都不靠譜理张,懶得搗鼓。
在豆瓣電影這個爬蟲中绵患,我其實是使用兩個步驟來執(zhí)行涯穷。第一部分是按照年標簽查找電影,從1900到2017年藏雏,將每個電影鏈接存入到mongodb中,然后進行去重(手動)作煌。
class DoubanMovies(scrapy.Spider):
name = "douban"
prefix = 'https://movie.douban.com/tag/'
start_urls = []
for i in range(1900,2018):
start_urls.append(prefix+str(i))
def parse(self,response):
names = response.xpath('//div[@class="pl2"]/a/text()').extract()
names = [name.strip('\n/ ') for name in names]
names = [name for name in names if len(name)>0] #去掉空名字
movie_urls = response.xpath('//div[@class="pl2"]/a/@href').extract()
hrefs = response.xpath('//div[@class="paginator"]/a/@href').extract()#獲取分頁鏈接
for href in hrefs:
yield scrapy.Request(href, callback=self.parse)
for i in range(len(names)):
yield {'name':names[i],'url':movie_urls[i]}
關于mongodb去重的問題掘殴,我使用的臨時表。主要是我對mongodb確實不熟悉粟誓,而且我對JavaScript這樣的語法著實不感冒奏寨。下面的代碼很簡單,每個電影鏈接就是https://movie.douban.com/subject/26685451/ 鹰服,我這里特地把這中間的數字提取出來病瞳,然后進行對比,這個肯定是唯一的悲酷。distinct會把獲取的數據重復的進行合并套菜,這在sql中也有這功能。
nums = movies.distinct("number")#我把鏈接中的數字提取了出來
m = db.movies1
for num in mums:
movie = movies.find_one({"number":num})
m.insert_one(movie)
moives.drop()#刪除原來的數據表
m.rename('movie')#把新表命名
還有個問題就是針對douban的時間限制设易,需要使用DOWNLOAD_DELAY
設置時間間隔逗柴,當然我使用bid突破了這個限制。
下面是第二個爬蟲的代碼顿肺,這個代碼就是訪問每個電影頁面提取相關數據戏溺,然后存到mongodb中渣蜗。數據提取沒什么難度,為了快速判斷xpath有效旷祸,最好開一個scrapy shell進行測試耕拷。
class DoubanMovies(scrapy.Spider):
name = "doubansubject"
def start_requests(self):
MONGO_URI = 'mongodb://localhost/'
client = MongoClient(MONGO_URI)
db = client.douban
movie = db.movie
cursor = movie.find({})
urls = [c['url'] for c in cursor]
for url in urls:
bid = "".join(random.sample(string.ascii_letters + string.digits, 11))
yield scrapy.Request(url,callback=self.parse,cookies={"bid":bid})
def parse(self,response):
title = response.xpath('//span[@property="v:itemreviewed"]/text()').extract_first()
year = response.xpath('//span[@class="year"]/text()').extract_first()#(2016)
pattern_y = re.compile(r'[0-9]+')
year = pattern_y.findall(year)
if len(year)>0:
year = year[0]
else:
year = ""
directors = response.xpath('//a[@rel="v:directedBy"]/text()').extract()#導演?有沒有可能有多個導演呢
'''
評分人數
'''
votes= response.xpath('//span[@property="v:votes"]/text()').extract_first()#評分人數
'''
分數
'''
score = response.xpath('//strong[@property="v:average"]/text()').extract_first()#抓取分數
#編劇不好找等會弄
'''
演員
'''
actors = response.xpath('//a[@rel="v:starring"]/text()').extract()#演員
genres = response.xpath('//span[@property="v:genre"]/text()').extract()#電影類型
html = response.body.decode('utf-8')
pattern_zp = re.compile(r'<span class="pl">制片國家/地區(qū):</span>(.*)<br/>')
nations = pattern_zp.findall(html)
if len(nations)>0 :
nations = nations[0]
nations = nations.split('/')
nations = [n.strip() for n in nations]
'''
多個國家之間以/分開托享,前后可能出現空格也要刪除
'''
pattern_bj = re.compile(r"<span ><span class='pl'>編劇</span>: <span class='attrs'>(.*)</span></span><br/>")
bj_as = pattern_bj.findall(html)
'''
bj_as 內容是
[<a>編劇</a>,<a></a>,<a></a>,<a></a>,]
需要進一步提取
'''
p = re.compile(r'>(.*)<')
bj = [p.findall(bj) for bj in bj_as]
'''
p.findall也會產生數組骚烧,需要去掉括號,只有有數據才能去掉
'''
bj = [b[0].strip() for b in bj if len(b)>0]#編劇的最終結果
'''
語言
<span class="pl">語言:</span> 英語 / 捷克語 / 烏克蘭語 / 法語<br/>
'''
pattern_lang = re.compile(r'<span class="pl">語言:</span>(.*)<br/>')
langs = pattern_lang.findall(html)
if len(langs)>0:
langs = langs[0]
langs = langs.split('/')
langs = [l.strip() for l in langs]
runtime = response.xpath('//span[@property="v:runtime"]/@content').extract_first()
'''
上映日期也有多個
'''
releasedates = response.xpath('//span[@property="v:initialReleaseDate"]/text()').extract()
'''
標簽
'''
tags = response.xpath('//div[@class="tags-body"]/a/text()').extract()
##這里不能用return
yield {"title":title,"year":year,"directors":directors,"score":score,"votes":votes,
"actors":actors,"genres":genres,"nations":nations,"bj":bj,"langs":langs,"runtime":runtime,
"releasedates":releasedates,"url":response.url
}
上面的代碼確實能正常工作嫌吠,但是有個缺點就是太慢止潘,不到五萬個頁面就要幾個小時,顯然瓶頸在分析這一塊辫诅。性能問題會在下面一個例子中討論凭戴。
2
性能問題確實是個大問題,在滿足能爬取的情況下炕矮,速度要優(yōu)化么夫。這幾天抓取一個AV網站,沒錯AV網站的種子文件肤视。先抓取文章列表档痪,再抓取每個詳細頁面,訪問種子下載頁面邢滑,最后下載里面的種子文件腐螟。
- 方法一
這個代碼很簡單使用的是requests來下載文件,里面的下載功能代碼就是從requests教程中拷貝出來的困后。
def process_item(self, item, spider):
try:
bt_urls = item['bt_urls']
if not os.path.exists(self.dir_path):
os.makedirs(self.dir_path)
'''
檢查文件夾是否存在這段代碼應該放到open_spider中去才是合適的乐纸,啟動檢查一下后面就不管了
'''
for url in bt_urls:
response = requests.get(url,stream=True)
attachment = response.headers['Content-Disposition']
pattern = re.compile(r'filename="(.+)"')
filename = pattern.findall(attachment)[0]
filepath = '%s/%s' % (self.dir_path,filename)
with open(filepath, 'wb') as handle:
#response = requests.get(image_url, stream=True)
for block in response.iter_content(1024):
if not block:
break
handle.write(block)
'''
整個代碼肯定會嚴重影響爬蟲的運行速度,是否考慮多進程方法
'''
except Exception as e:
print(e)
return item
bt_url種子的鏈接就放在bt_urls摇予,由于是從下載頁面返回的item汽绢,實際中最多只有一個鏈接。這個代碼運行起來沒什么問題侧戴,但是速度相當慢宁昭。scrapy使用的是異步網絡框架,但是requests是實實在在的同步方法酗宋,單線程的情況下必然影響到整個系統(tǒng)的執(zhí)行积仗。必須要突破這個瓶頸,實際中要先考慮代碼能正確運行再考慮其它方面蜕猫。
- 方法二
既然在本線程中直接下載會造成線程阻塞斥扛,那開啟一個新的進程如何。
class DownloadBittorrent2(object):
def __init__(self, dir_path):
self.dir_path = dir_path
# self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(
dir_path =crawler.settings.get('DIR_PATH'),
)
def open_spider(self, spider):
if not os.path.exists(self.dir_path):
os.makedirs(self.dir_path)
def close_spider(self, spider):
pass
def downloadprocess(self,url):
try:
response = requests.get(url,stream=True)
attachment = response.headers['Content-Disposition']
pattern = re.compile(r'filename="(.+)"')
filename = pattern.findall(str(attachment))[0]#這里attachment是bytes必須要轉化
filepath = '%s/%s' % (self.dir_path,filename)
with open(filepath, 'wb') as handle:
#response = requests.get(image_url, stream=True)
for block in response.iter_content(1024):
if not block:
break
handle.write(block)
except Exception as e:
print(e)
def process_item(self, item, spider):
bt_urls = item['bt_urls']
if len(bt_urls)>0:#最多只有一個url
p = Process(target=self.downloadprocess,args=(bt_urls[0],))
p.start()
return item
這個代碼也能正常工作,但是報錯稀颁,直接導致服務器掛了芬失。
HTTPConnectionPool(host='taohuabbs.info', port=80): Max retries exceeded with url: /forum.php?mod=attachment&aid=MjAxNzk 0fDA0OTkxZjM0fDE0ODU4NjY0OTZ8MHwxMzMzNDA= (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConne ction object at 0x00000000041F9048>: Failed to establish a new connection: [WinError 10060]
由于連接方在一段時間后沒有正確答復或連接的主機沒有反應,連接嘗試失敗匾灶。',))
這個可能跟設置的延遲有關系(我就沒有設置延遲)棱烂,反正就是把服務器弄死了。還有就是requests在這種異常情況下容錯能力有問題阶女。
- 方法三
既然scrapy自帶了一個Filespipeline颊糜,那么是不是可以考慮用這個來下載呢!可以試試秃踩!
class DownloadBittorrent3(FilesPipeline):
def get_media_requests(self, item, info):
for file_url in item['bt_urls']:
yield scrapy.Request(file_url)
代碼報錯了衬鱼,原因是文件名打不開。這個就涉及到如何命名下載文件名的問題憔杨。如果鏈接中帶*.jpg這樣類似的名字鸟赫,程序不會有問題,如果不是會怎么樣消别,鏈接中可能出現操作系統(tǒng)不允許在文件名中出現的字符抛蚤,這就會報錯。我對系統(tǒng)自帶的這個pipeline了解甚少寻狂,就沒有繼續(xù)研究岁经。
還有一點我希望文件名來自于服務器的反饋,對于下載文件服務器可能會把文件名發(fā)過來蛇券,這個就在headers的Content-Disposition
字段中缀壤。也就是是說我必須要先訪問網絡之后才能確定文件名。
- 方法四
前面我們都使用了pipeline來處理纠亚,實際上我們完全可以不用pipeline而直接在spider中處理诉位。
def download(self,response):
'''
在爬取過程中發(fā)現有可能返回不是torrent文件,這時候要考慮容錯性問題菜枷,雖然爬蟲并不會掛掉
'''
attachment = response.headers['Content-Disposition']
pattern = re.compile(r'filename="(.+)"')
filename = pattern.findall(attachment.decode('utf-8'))[0]
filepath = '%s/%s' % (self.settings['DIR_PATH'],filename)
with open(filepath, 'wb') as handle:
handle.write(response.body)
這種方法性能不錯,對比前面50/min速度叁丧,這個可以達到100/min啤誊。其實我們可以更快。
3
在實際的下載中拥娄,我們要充分利用scrapy的網絡下載框架蚊锹,這個性能好容錯性高,而且也好排錯稚瘾。上面的10060錯誤牡昆,我估計放在http中可能就是503(服務器無法到達)。
前面的方法都在單線程中運作,雖然后面有多進程版的下載代碼丢烘,由于沒有scrapy穩(wěn)定所以我考慮用多個爬蟲來實現柱宦。如果啟動兩個scrapy爬蟲,一個負責爬頁面播瞳,一個負責下載掸刊,這樣的效率應該會高不少。雖然前面的筆記中有提到相關代碼赢乓,使用redis來實現分布式忧侧。當然在單機上稱不上分布式,但是使用redis作為進程間通訊手段確實極好的牌芋,不管是多進程還是分布式都能非常高效的工作蚓炬。github上有基于redis版本的scrapy,這里我的想法是第一個爬蟲負責爬頁面的屬于一般爬蟲(使用原版的scrapy)躺屁,而第二個爬蟲使用基于redis的爬蟲肯夏。
- 1 scrapy-redis安裝
pip install scrapy-redis
安裝方法倒是很簡單,但是這個代碼比較舊了楼咳,版本是0.6.3熄捍,這個版本在python3.5上工作不正常(出錯跟轉碼有關str,具體情況不懂)母怜,處理的辦法就是把0.6.7的代碼下載下來直接覆蓋就可以了(反正我也看不懂代碼余耽,覆蓋了能工作)。 - 2 配置
scrapy-redis的配置還是在settings中苹熏,參考文檔
文檔中有幾個必須配置的參數:
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
后面還可以配置redis服務器端口號碟贾,還有redis服務器地址。
REDIS_START_URLS_BATCH_SIZE
上面的參數對代表每次redis從redis服務器中獲取的鏈接數量轨域,這個調高可能會增加性能袱耽。 - 3 頁面爬蟲
class BtSpiderEx(scrapy.Spider):
name = 'btspiderex'
start_urls = ['http://taohuabbs.info/forum-181-1.html']
def parse(self,response):
urls = response.xpath('//a[@onclick="atarget(this)"]/@href').extract()
for url in urls:
yield scrapy.Request(response.urljoin(url),callback=self.parsedetail)
page_urls = response.xpath('//div[@class="pg"]/a/@href').extract()
for url in page_urls:
yield scrapy.Request(response.urljoin(url),callback=self.parse)
def parsedetail(self,response):
hrefs = response.xpath('//p[@class="attnm"]/a/@href').extract()
for h in hrefs:
yield scrapy.Request(response.urljoin(h),callback=self.parsedown)
def parsedown(self,response):
'''
其實每次只能分析出一個bt鏈接
'''
bt_urls = response.xpath('//div[@style="padding-left:10px;"]/a/@href').extract()
yield {'bt_urls':bt_urls}
頁面爬蟲代碼其實相對于前面的實現,變得更加簡單干发,這里把將下載鏈接推送到redis服務器的任務交給pipeline朱巨。
class DownloadBittorrent(object):
def __init__(self, dir_path):
self.dir_path = dir_path
@classmethod
def from_crawler(cls, crawler):
return cls(
dir_path =crawler.settings.get('DIR_PATH'),
)
def open_spider(self, spider):
if not os.path.exists(self.dir_path):
os.makedirs(self.dir_path)
self.conn = redis.Redis(port=6666)
def close_spider(self,spdier):
pass
def process_item(self, item, spider):
bt_urls = item['bt_urls']
for url in bt_urls:
self.conn.lpush('redisspider:start_urls',url)
return item
open_spider
在爬蟲啟動的時候啟動,這里就可以打開redis和建立下載文件夾枉长。redisspider:start_urls
這個是redis隊列名冀续,缺省情況下scrapy-redis
爬蟲的隊列就是爬蟲名+start_urls。
- 4 下載爬蟲
下載爬蟲只負責從redis獲取鏈接然后下載必峰。
from scrapy_redis.spiders import RedisSpider
import re
class DistributeSpider(RedisSpider):
name = 'redisspider'
def parse(self,response):
DIR_PATH = "D:/bt"
if 'Content-Disposition' in response.headers:
attachment = response.headers['Content-Disposition']
pattern = re.compile(r'filename="(.+)"')
filename = pattern.findall(attachment.decode('utf-8'))[0]
filepath = '%s/%s' % (DIR_PATH,filename)#DIR_PATH = "D:/bt"
with open(filepath, 'wb') as handle:
handle.write(response.body)
settings.py配置洪唐,只列出了主要參數,這里修改了默認端口號:
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
REDIS_PORT = 6666