Scrapy爬取伯樂在線文章
準(zhǔn)備工作:
- python環(huán)境,我是用Anaconda
- Scrapy環(huán)境,上一篇文章提到過
- MySQL,我們準(zhǔn)備將爬取的數(shù)據(jù)保存到MySQL數(shù)據(jù)庫中
創(chuàng)建項目
首先通過scrapy命令創(chuàng)建項目
爬取數(shù)據(jù)整體邏輯
分析一下整個流程,可以分為兩個部分。一,分析列表頁面結(jié)構(gòu),獲取每一篇文章的鏈接和圖片地址以及下一個列表頁地址。二,進(jìn)入文章單頁獲取想要的內(nèi)容數(shù)據(jù)。因此代碼如果都寫在一起顯得非常臃腫,難以閱讀。因此可以在parse函數(shù)處理第一部分邏輯,然后通過Request函數(shù)發(fā)送請求進(jìn)行文章內(nèi)容頁的處理。
def parse(self, response):
"""
1獲取文章列表頁的url并交給scrapy下載后進(jìn)行解析
2獲取下一頁url,交給scrapy下載,下載完成后交給parse
"""
#解析列表中所有文章url,并交給scrapy
post_nodes = response.css("#archive .floated-thumb .post-thumb a")
for post_node in post_nodes:
image_url = post_node.css("img::attr(src)").extract_first()
post_url = post_node.css("::attr(href)").extract_first()
yield Request(url=parse.urljoin(response.url,post_url),meta={"front_image_url":image_url},callback=self.parse_detail)
#提取下一頁并交給scrapy
next_urls = response.css(".next.page-numbers::attr(href)").extract_first("")
if next_urls:
yield Request(url=parse.urljoin(response.url, next_urls), callback=self.parse)
分析爬取頁面內(nèi)容
本次爬取的內(nèi)容為伯樂在線的文章,我們采取css方式來獲取想要爬取的內(nèi)容,具體css的使用方法我們在上一篇文章提到過,可以參看。
title = response.css('div.entry-header h1::text').extract_first()
create_data = response.css('p.entry-meta-hide-on-mobile::text').extract_first().strip().replace("·","").strip()
praise_nums = response.css('span.vote-post-up h10::text').extract_first()
fav_nums = response.css(".bookmark-btn::text").extract_first()
comment_nums = response.css("a[href='#article-comment'] span::text").extract_first()
tag_list = response.css('p.entry-meta-hide-on-mobile a::text').extract()
文章圖片的獲取
我們可以發(fā)現(xiàn)文章的圖片只是在列表頁里面存在,如果是文章正文中,可能就不會出現(xiàn),因此我們在處理文章鏈接的時候要同時處理文章的圖片。這里用到了Request的一個變量meta,傳遞的內(nèi)容為一個字典。meta={"front_image_url":image_url}
Items
我們數(shù)據(jù)爬取的主要目的是從非結(jié)構(gòu)的數(shù)據(jù)源轉(zhuǎn)化為結(jié)構(gòu)化的數(shù)據(jù)。但是提取數(shù)據(jù)之后,怎么將數(shù)據(jù)進(jìn)行返回呢?數(shù)據(jù)以什么形式返回呢?這時候發(fā)現(xiàn)數(shù)據(jù)缺少了結(jié)構(gòu)化的定義,為了將數(shù)據(jù)進(jìn)行定義,方便格式化和處理,就用到了Item類。此時我們爬取的數(shù)據(jù)可以通過Item進(jìn)行實(shí)例化。Scrapy發(fā)現(xiàn)yield的是一個Item類后,會將我們的Item路由到pipliens中,方便數(shù)據(jù)處理和保存。
class ArticleItem(scrapy.Item):
title = scrapy.Field()
create_date = scrapy.Field()
url = scrapy.Field()
url_object_id= scrapy.Field()
front_image_url = scrapy.Field()
front_image_path = scrapy.Field()
praise_nums = scrapy.Field()
comment_nums = scrapy.Field()
fav_nums = scrapy.Field()
tags = scrapy.Field()
content = scrapy.Field()
scrapy圖片自動下載機(jī)制
scrapy提供了一個圖片下載機(jī)制,只需要在settings.py文件夾下的ITEM_PIPELINES增加一句配置
'scrapy.pipelines.images.ImagesPipeline':1,
,意思是用scrapy提供的pipline中的images里面的ImagesPipeline。具體路徑如下
我們可以看到scrapy給我們提供了兩個已經(jīng)完成的pipeline,一個是圖片的一個是媒體的。后面的數(shù)字1代表進(jìn)入pipeline的優(yōu)先級,越小代表優(yōu)先級越高,在多個pipeline同時存在是應(yīng)該注意。
但是還有一個問題,pipeline怎么知道圖片的地址呢?item中的字段那么多,又有哪一個該被傳給pipeline呢?這個還需要在setting文件中配置
IMAGES_URLS_FIELD = "front_image_url"
project_dir = os.path.abspath(os.path.dirname(__file__))
IMAGES_STORE = os.path.join(project_dir,'images')
這樣運(yùn)行的時候會報一個錯:raise ValueError('Missing scheme in request url %s' % self._url),這是因為pipline將IMAGES_URLS_FIELD = "front_image_url"按數(shù)組處理,但是我們item中的圖片地址是一個值,而不是一個數(shù)組。我們可以將item中的值賦值的時候做一下修改:
article_item['front_image_url'] = [front_image_url]
,在front_image_url上加了一個[],使其可迭代
獲取圖片保存路徑
我們?nèi)绾伟驯镜貓D片地址與文章關(guān)聯(lián)起來呢?比如item中一個字段是圖片的本地地址,我們應(yīng)該怎么做呢?
解決方法就是自己定義一個pipeline,繼承圖片下載的pipeline
class ArticleImagePipeline(ImagesPipeline):
def item_completed(self, results, item, info):
if "front_image_url" in item:
for ok, value in results:
image_file_path = value["path"]
item["front_image_path"] = image_file_path
return item
以JSON形式保存
class JsonExporterPipleline(object):
#調(diào)用scrapy提供的json export導(dǎo)出json文件
def __init__(self):
self.file = open('articleexport.json', 'wb')
self.exporter = JsonItemExporter(self.file, encoding="utf-8", ensure_ascii=False)
self.exporter.start_exporting()
def close_spider(self, spider):
self.exporter.finish_exporting()
self.file.close()
def process_item(self, item, spider):
self.exporter.export_item(item)
return item
定義MySQL表結(jié)構(gòu)
DROP TABLE IF EXISTS `jobbole_article`;
CREATE TABLE `jobbole_article` (
`title` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`create_date` date NULL DEFAULT NULL,
`url` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`url_object_id` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`front_image_url` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`front_image_path` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`comment_nums` int(11) NOT NULL DEFAULT 0,
`fav_nums` int(11) NOT NULL DEFAULT 0,
`praise_nums` int(11) NOT NULL DEFAULT 0,
`tags` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
PRIMARY KEY (`url_object_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
安裝MySQL庫
使用pip install mysqlclient
可以安裝mysqlclient,如果是python2那么可以安裝mysqldb,是一樣的功能,API都相同。Linux下安裝可能報錯,如果是ubuntu需要執(zhí)行sudo apt-get install libmysqlclient-dev
,如果是centos可以執(zhí)行sudo yum install python-devel mysql-devel
定義pipeline保存數(shù)據(jù)到MySQL
class MysqlPipeline(object):
def __init__(self):
self.conn = MySQLdb.connect('127.0.0.1', 'root', 'root', 'article_spider', charset='utf8', use_unicode=True)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
insert_sql = 'INSERT INTO jobbole_article (`title`, `create_date`, `url`, `url_object_id`, `content`, `front_image_path`, `comment_nums`, `fav_nums`, `praise_nums`, `tags`) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)'
self.cursor.execute(insert_sql, (item['title'], item['create_date'], item['url'], item['url_object_id'], item['content'], item["front_image_path"], item['comment_nums'], item['fav_nums'], item['praise_nums'], item['tags']))
self.conn.commit()
定義pipeline異步保存數(shù)據(jù)到MySQL
- 上述的插入方法是同步插入,意味著這句話執(zhí)行不結(jié)束下面的工作沒法去做
- 另外一個原因是spider的解析速度遠(yuǎn)大于插入數(shù)據(jù)的速度,這樣到后期爬取的item越來越多,插入速度跟不上解析速度,就會造成堵塞
- 另外異步插入可以根據(jù)不同的item定制插入語句,而不用寫多個pipeline
class MysqlTwistedPipline(object):
def __init__(self,dbpool):
self.dbpool = dbpool
@classmethod
def from_settings(cls,settings):
dbparms = dict(
host = settings["MYSQL_HOST"],
db = settings["MYSQL_DBNAME"],
user = settings["MYSQL_USER"],
passwd = settings["MYSQL_PASSWORD"],
charset = 'utf8',
cursorclass = MySQLdb.cursors.DictCursor,
use_unicode = True
)
dbpool = adbapi.ConnectionPool("MySQLdb",**dbparms)
return cls(dbpool)
def process_item(self,item,spider):
#使用twisted將MYSQL插入編程異步執(zhí)行
query = self.dbpool.runInteraction(self.do_insert,item,)
query.addErrback(self.handle_error,item,spider)#處理異常
def handle_error(self,failure,item,spider):
print(failure) #處理異步插入的異常
def do_insert(self,cursor,item):
# 執(zhí)行具體的插入
# 根據(jù)不同的item 構(gòu)建不同的sql語句并插入到mysql中
insert_sql, params = item.get_insert_sql()
cursor.execute(insert_sql, params)
使用itemloader
既然已經(jīng)有了item,那為什么要使用itemloader呢?我們可以看到不管是xpath,或者css,都是需要extract,然后可能還需要正則化處理,這樣以后的維護(hù)工作會變得很困難。
# 通過item loader加載item
front_image_url = response.meta.get("front_image_url", "") # 文章封面圖
item_loader = ArticleItemLoader(item=JobBoleArticleItem(), response=response)
# 通過css選擇器將后面的指定規(guī)則進(jìn)行解析。
item_loader.add_css("title", ".entry-header h1::text")
item_loader.add_value("url", response.url)
item_loader.add_value("url_object_id", get_md5(response.url))
item_loader.add_css("create_date", "p.entry-meta-hide-on-mobile::text")
item_loader.add_value("front_image_url", [front_image_url])
item_loader.add_css("praise_nums", ".vote-post-up h10::text")
item_loader.add_css("comment_nums", "a[href='#article-comment'] span::text")
item_loader.add_css("fav_nums", ".bookmark-btn::text")
item_loader.add_css("tags", "p.entry-meta-hide-on-mobile a::text")
item_loader.add_css("content", "div.entry")
# 調(diào)用這個方法來對規(guī)則進(jìn)行解析生成item對象
article_item = item_loader.load_item()
# 已經(jīng)填充好了值調(diào)用yield傳輸至pipeline
yield article_item