本着交流和学习的心态和大家分享本人的第一篇博客(客套话就不说了,其实就是说说自己编写的思路和及对问题的解决办法)。
先说说技术路线,选择docker,scrapy,scrapy_redis 的原因很简单,省钱又方便。(苦比的大四党并不享用云主机优惠)
本爬虫主要抓取了豆瓣movie,book,music分类中的资源。
先看看最后抓取的数据量(大概12万的数据(爬虫待优化))
好了,下面就讲讲我的心路历程。
思路:
url分析 --> 分析待抓取项 --> 编写 item --> 构建mysql数据表 --> 解析待抓取项--> 编写spider -->
编写pipeline --> 构建ip_pool --> 构建ua_pool --> 编写middleware -->调用scrapy_redis -->
配置setting --> 配置mysql,redis --> docker部署 -->crawl
(大概就这样吧)
详解过程:
1.url分析,分析待抓取项,编写item,构建mysql,解析待抓取项
主要就规范了spider编写的思路以及通过re,xpath对内容进行提取规则(可使用scrapy shell 或者写个test),没啥可以详细写的。
2.编写spider
由于book,music,movie各自的数据提取规则和url规则并不一样,movie自身有json形式的数据,而其他俩个是html,所以需要在spider中对其进行分类编写。
if 'movie' in response.url:
#豆瓣movie,由于豆瓣的movie自身是个api,只需要将这些url交给下一层提取url的函数处理
movie_api_list =['https://movie.douban.com/j/new_search_subjects?sort=T&range=0,10&tags=%E7%94%B5%E5%BD%B1&start=0',
'https://movie.douban.com/j/new_search_subjects?sort=T&range=0,10&tags=%E7%94%B5%E8%A7%86%E5%89%A7&start=0',
'https://movie.douban.com/j/new_search_subjects?sort=T&range=0,10&tags=%E7%BB%BC%E8%89%BA&start=0',
'https://movie.douban.com/j/new_search_subjects?sort=T&range=0,10&tags=%E5%8A%A8%E7%94%BB&start=0',
'https://movie.douban.com/j/new_search_subjects?sort=T&range=0,10&tags=%E7%9F%AD%E7%89%87&start=0'
]
for movie_api in movie_api_list:
yield Request(url=movie_api, callback=self.parse_detial)
else:
#处理书籍和音乐,将分类页面中所有可用的url都提取出来交给下一层处理
all_urls = response.xpath('//a/@href').extract()
if all_urls:
all_urls = list(set([parse.urljoin(response.url,i) for i in all_urls]))
for this_url in all_urls:
key_url = re.compile('(.*douban\.com/tag/.*)',re.DOTALL).findall(this_url)
if key_url:
key_url = key_url[0]
yield Request(url=key_url, callback=self.parse_detial)
接下来为了提取二级分类,需要在url中提取数据,并将详细数据页面的url交给下个函数处理,下一页则递归(以movie为例子)
#处理movie api
if 'movie' in response.url:
tag1='movie'
movie_json = json.loads(response.text)
movie_tag = re.compile('.*&tags=(.*?)&.*').findall(response.url)
if movie_tag:
#将url中的字符转换成可识别的字符串
movie_tag = parse.unquote(movie_tag[0])
else:
movie_tag="None"
for detial_url in movie_json["data"]:
detial_url = detial_url["url"]
#判断此api接口是否有数据
if detial_url:
yield Request(url =detial_url,callback=self.get_data,meta={'tag1':tag1,'tag2':'{0}'.format(movie_tag)})
start = re.match(r'.*start=(\d+)',response.url)
if start:
num = str(int(start.group(1))+20)
page =re.compile('start=\d+')
next_movie_api =page.sub('start={0}'.format(num),response.url)
if next_movie_api:
yield Request(url=next_movie_api,callback=self.parse_detial)
详细数据页面的提取使用了itemloader(以movie为例,简单的截个图吧)
spider的编写就结束了。
3.编写pipeline
pipeline主要将数据写入到mysql中,担心mysql堵塞的问题,就借用了twisted异步插入mysql。
#通过twisted异步将数据插入mysql中,防止堵塞
class MysqlTwistedPipeline(object):
#通过twisted框架调用adbapi实现异步的数据写入
def __init__(self,dbpool):
self.dbpool = dbpool
@classmethod
def from_settings(cls,settings):
#通过settings中设置的数据库信息,将数据库导入到pool中
dbparms =dict(
host = settings["MYSQL_HOST"],
db = settings["MYSQL_DBNAME"],
user = settings["MYSQL_USER"],
password = settings["MYSQL_PASSWORD"],
charset = "utf8mb4",
cursorclass = pymysql.cursors.DictCursor,
use_unicode = True,
)
dbpool = adbapi.ConnectionPool("pymysql",**dbparms)
return cls(dbpool)
def process_item(self,item,spider):
#调用twisted的api实现异步插入
query = self.dbpool.runInteraction(self.do_insert,item)
query.addErrback(self.handle_err,item,spider)
print(">>>insert into mysql")
def handle_err(self,failure,item,spider):
print(failure)
def do_insert(self,cursor,item):
#具体的插入函数
#根据不同的item,构建不同的sql语句,并且插入到mysql中
#plan1 if item.__class__.__name__ =="ArticleItem" (通过item的名字来区别sql语句的写入)
#plan 2 见items
insert_sql,params =item.get_sql()
# print(params[2])
其中插入语句都写在item中(数据的精加工在Utill中):
def get_sql(self):
insert_sql = '''
insert into movie(
url,url_id,movie,classify,director,scriptwriter,
actor,score,comments_num,screen_time,movie_type,country_areas,language,
hot_comments,crawl_time
)values(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
'''
url = "".join(self.get("movie_url",""))
url_id = "".join(self.get("movie_url_id",""))
movie = get_ration_element(self.get("movie",""))
classify = get_ration_element(self.get("movie_classify",""))
director = get_ration_element(self.get("movie_director",""))
scriptwriter = get_ration_element(self.get("movie_scriptwriter",""))
actor = get_ration_element(self.get("movie_actor",""))
type = get_ration_element(self.get("movie_type",""))
country_areas = get_ration_element(self.get("movie_country_areas",""))
language = get_ration_element(self.get("movie_language",""))
screen_time = get_date(self.get("movie_screen_time",""))
score = get_num2(self.get("movie_score",""))
comments_num = get_num1(self.get("movie_comments_num",""))
hot_comments = get_hot_comments(self.get("movie_hot_comments",""))
crawl_time = now()
params = (url,url_id,movie,classify,director,scriptwriter,
actor,score,comments_num,screen_time,type,country_areas,language,
hot_comments,crawl_time)
return insert_sql,params
4.构建ip_pool
使用了代理ip商提供的ip,将ip保存至本地文件中,定时更换,确保ip的效用。(墙裂推荐蘑菇代理,便宜又高效)。
5.构建ua_pool
fake-useragent包有250个ua,可以pip安装使用。见文档 https://github.com/hellysmile/fake-useragent
6.编写Middleware
只需要编写随机ua和代理ip,settings中关闭默认middleware,开启自己写的即可。
#随机生成ua并调用
class RandomUAMiddlerware(object):
def __init__(self,crawler):
super(RandomUAMiddlerware,self).__init__()
self.ua = ua()
@classmethod
def from_crawler(cls,crawler):
return cls(crawler)
def process_request(self,spider,request):
request.headers.setdefault(b'User-Agent',self.ua)
#调用ippool池中的proxy并形成request
class RandomProxyIPMiddlware(object):
def process_request(self, request, spider):
proxy =product_ip()
print("this_ip:"+str(proxy))
request.meta["proxy"] = proxy
7.scrapy_redis
scrapy_redis的使用很方便,直接附上其github https://github.com/rmax/scrapy-redis
记得跟换spider的超类为RedisSpider,并设置redis_key 。settings中需要的设置见文档即可,很详细实用
from scrapy_redis.spiders import RedisSpider
class BasicDoubanSpider(RedisSpider):
name = 'basic_douban'
redis_key = 'DouBan:start_urls'#设置redis的起始key
# allowed_domains = ['www.douban.com']
# start_urls = [ 'https://music.douban.com/tag/',
# 'https://movie.douban.com/tag/#/',
# 'https://book.douban.com/tag/?view=type&icn=index-sorttags-all',
#]
8.配置settings
settings中需要关闭cookie,禁止REDIRECT,开启延迟,retry_code,retry次数,retry的优先级等等。不贴代码了,见我github吧
9.redis和mysql的配置
redis:进入redis.conf文件将bind更改为主机doker的ip (终端ifconfig查看)
mysql:1 进入配置文件注释bind,使本地mysql可以访问
2 添加用户并赋予权限 :mysql中 grant all privileges on *.* to "name"@"%" indetified by password;
10.docker 部署
宿主机共享文件夹(共享ip-pool和spider) 创建contianer时 docker run -tiv 主机文件path:docker path image_id
11 可以开始crawl啦
往scrapy_redis 中添加start-urls lpush your redis_key+your start_url
好了,详细的过程就是这样子,具体代码见我的github吧。https://github.com/kilort/douban