最近写了个爬取豆瓣电子书信息的爬虫(常来豆瓣看书评和影评。。),爬取的站点是https://read.douban.com/kind/100?start=%s&sort=hot&promotion_only=False&min_price=None&max_price=None&works_type=None
使用的是requests库来获得html(超级好用有木有!自带的urllib相比之下还比较麻烦。。)
网站结构清晰,使用xpath(lxml库)解析,感觉和BeautifulSoup用起来差不多,但BS是基于DOM的,它会载入整个文档,解析整个
DOM树,内存和时间开销会比xpath大很多,并且BS是python编写的,lxml是用c编写的。大家都说BS更好用,但我觉得易用性差不多
啊, 所以果断数据量稍微大点我就用xpath了。
xpath教程在这里:http://www.w3school.com.cn/xpath/index.asp
数据库使用mysql,数据库驱动是mysql-connector。
我将代码分为两部分:数据的获取与数据的存储,分别用两个类表示。
数据获取:
1 class ParsePageThread(threading.Thread): 2 '''用于解析每个页面的线程,每个页面包含20项''' 3 def __init__(self, url, queue=connector_queue, headers=None, host="https://read.douban.com"): 4 super(ParsePageThread, self).__init__() 5 self.url = url 6 self.headers = headers 7 self.host = host 8 self.queue = queue 9 self.res = [] 10 11 def run(self): 12 self.getBookContent() 13 print(self.res) 14 self.queue.put(self.res) 15 16 def __getHTMLText(self,url): 17 try: 18 r = requests.get(url, self.headers) 19 r.raise_for_status() 20 return r.text 21 except: 22 return '' 23 24 def getBookContent(self): 25 text = self.__getHTMLText(self.url) 26 html = etree.HTML(text) 27 book_urls = html.xpath('//li[@class="item store-item"]/div[@class="info"]/div[@class="title"]/a/@href') 28 for book_url in book_urls: 29 url = self.host + book_url 30 text = self.__getHTMLText(url) 31 html = etree.HTML(text) 32 name = html.xpath('//h1[@class="article-title"]/text()') 33 if not name: 34 name = html.xpath('//h1[@itemprop="name"]/text()') 35 author = html.xpath('//a[@class="author-item"]/text()') 36 price = html.xpath('//span[@class="current-price-count"]/text()') 37 if not price: 38 price = html.xpath('//span[@class="discount-price current-price-count"]/text()') 39 press = html.xpath('//a[@itemprop="provider"]/text()') 40 if not press: 41 press = html.xpath('//div[@class="provider"]/a/text()') 42 words = html.xpath('//span[@class="labeled-text"]/text()') 43 if not words: 44 words = ['unknown'] 45 word = words[1] if len(words) > 1 else words[0] 46 try: 47 self.res.append([str(name[0]), str(author[0]), \ 48 str(price[0]), str(press[0]), str(word)]) 49 except: 50 pass
这里使用了多线程解析每个页面。
数据存储使用mysql,因为要频繁操作数据库,这里使用数据库连接池以减小连接数据库的开销。
1 class MySQLPool(object): 2 '''MySql连接池类以减小用于请求连接、创建连接和关闭连接的开销''' 3 def __init__(self, host='localhost', port='3306', user='root', password='password',\ 4 database='test', pool_name='douban', pool_size=5): 5 res = {} 6 # self._content = content 7 self._host = host 8 self._port = port 9 self._user = user 10 self._password = password 11 self.database = database 12 res['host'] = self._host 13 res['port'] = self._port 14 res['user'] = self._user 15 res['password'] = self._password 16 res['database'] = self.database 17 self.dbconfig = res 18 self.pool = self.create_pool(pool_name=pool_name, pool_size=pool_size) 19 20 def insert(self, content): 21 if not content : 22 return 23 #若有重复的id(书名),直接忽略 24 sql_ = "insert ignore into douban (id, author, price, press, words) \ 25 values (%s, %s, %s, %s, %s)" 26 for item in content: 27 print("insert operating...\r") 28 self.execute(sql_, args=tuple(item)) 29 30 def create_pool(self, pool_name='douban', pool_size=5): 31 pool = mysql.connector.pooling.MySQLConnectionPool( 32 pool_name=pool_name, 33 pool_size=pool_size, 34 pool_reset_session=True, 35 **self.dbconfig 36 ) 37 return pool 38 39 def close(self, conn, cursor): 40 cursor.close() 41 conn.close() 42 43 def execute(self, sql, args=None, commit=True): 44 conn = self.pool.get_connection() 45 cursor = conn.cursor() 46 if args: 47 cursor.execute(sql, args) 48 else: 49 cursor.execute(sql) 50 if commit: 51 conn.commit() 52 self.close(conn, cursor) 53 return None 54 else: 55 res = cursor.fetchall() 56 self.close(conn, cursor) 57 return res
细心的朋友应该看到了这里使用了queue用于在数据的获取和存储之间架起桥梁,因为queue自带锁,
可保证线程间公共数据的安全,不过目前数据存储方面还没使用多线程。
爬虫终于跑起来了,不过我预料之中的事也发生了:在爬取3000+数据的时候,连接断开了,大概是
被豆瓣发现了?发现才是正常的。。
我已经加入了user-agent和referer,后面考虑使用ip代理吧。如果不行就试试selenium,但那样又会
降低爬取速度,加上我这龟速的网,想想都可怕。
试完之后再来更新罢了。就酱,睡觉(~﹃~)~zZ