我刚入门 python ,最近想要写爬虫爬取豆瓣图书信息。目前已完成以下函数附说明:
初始页面是 https://book.douban.com/tag/%E7%BC%96%E7%A8%8B
1.pages = fetchPages() # 获取初始页面的翻页链接,返回所有翻页链接的列表
2.books = fetchBooks(pages) # 获取初始页面及所有翻页页面的书籍网址,返回所有书籍链接的列表
3.data = fetchBookInfo(books) # 获取所有书籍的信息,信息包含书名、评分等,返回包含书籍信息的元组组成的列表
4.savingCsv(data) # 将所有书籍信息写入 csv 文件
可以看到每个函数接受上一个函数返回的结果。
我的问题是,怎样可以把这些函数变成多线程处理,我在网上花了点时间搜索没有找到答案,也许多线程属于高级主题,对我这种初学者来说理解比较困难,请网友不吝赐教。
1
itlr 2016-06-13 02:27:55 +08:00
步骤 1 后可以用 multiprocessing 对各个 page 并行采集,用 Pool , starmap_async()这样的调用,具体要参考文档 https://docs.python.org/2/library/multiprocessing.html
|
2
YUX 2016-06-13 03:42:59 +08:00
from concurrent.futures import ThreadPoolExecutor
from requests_futures.sessions import FuturesSession session = FuturesSession(executor=ThreadPoolExecutor(max_workers=20)) import requests from bs4 import BeautifulSoup import re def fetchPages(first_page): headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'} content = requests.get(first_page, headers=headers).text soup = BeautifulSoup(content, "html.parser") a_tags_final = soup.find("div", { "class" : "paginator" }).find_all("a")[-2].get("href") page_max = int(re.findall("start=(.*)&",a_tags_final)[0]) pages = [] for k in range(0,page_max+20,20): pages.append(first_page+"?start="+str(k)) return pages def fetchBooks(pages): headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'} books = [] for page in pages: books.append(session.get(page, headers = headers)) def get_books_url(book): soup = BeautifulSoup(book, "html.parser") book_list = list(map(lambda li: li.find("div", { "class" : "info" }).find("h2").find("a").get("href"), soup.find_all("li", { "class" : "subject-item" }))) return book_list books = list(map(lambda book: get_books_url(book.result().text), books)) books_url = [] for book in books: books_url += book return books_url def fetchBookInfo(books): headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'} books_info = [] for book in books: books_info.append(session.get(book, headers = headers)) def get_books_data(book_info): soup = BeautifulSoup(book_info, "html.parser") info = soup.find("div", { "id" : "info" }) return info book_data = list(map(lambda book: get_books_data(book.result().text), books_info)) return book_data if __name__ == '__main__': pages = fetchPages("https://book.douban.com/tag/%E7%BC%96%E7%A8%8B") books = fetchBooks(pages) data = fetchBookInfo(books) |
3
YUX 2016-06-13 03:46:05 +08:00
Python3.5 运行通过 需要 BeautifulSoup 和 requests_futures
max_workers=20 这里根据你的需要你自己改一下 我只写到了 data = fetchBookInfo(books)这一步,怎么弄这些个数据就看你了 其实有用的只有一句话 用 requests_futures https://github.com/ross/requests-futures |
5
ila 2016-06-13 08:51:19 +08:00 via Android
试试了,不同图书分类一个进程
|
6
araraloren 2016-06-13 09:46:01 +08:00
~~把每个步骤放在一个线程里面就是多线程了,不过要注意公共数据的访问可能需要互斥
刚入门 python ,还是先来一个模块化的吧,然后学习多线程改进程序 |
7
wbt 2016-06-13 10:08:05 +08:00
Python 是多线程性能并不好
先一个线程试试吧,不行就开多个进程。 |
8
qianbaooffer 2016-06-13 10:10:33 +08:00
对于这种网络 io,python 多线程对 GIL 做了优化,性能没有问题,如果不是 IO 类处理,那多线程确实有问题
@wbt |
9
wbt 2016-06-13 10:44:31 +08:00
@qianbaooffer 学习了~
|
10
laoni 2016-06-13 10:52:01 +08:00
PY scipy 为啥不用。。。。还自己写。。 一直跑 scipy 相当稳定靠谱。。
|
11
Allianzcortex 2016-06-13 11:07:44 +08:00
用 multiprocessing 库, Queue 来实现 FIFO 的任务队列,当时爬的是拉钩,自己之前写过一个学习用的 demo ,比较简答,有注释,可以直接套用:
<script src="https://gist.github.com/Allianzcortex/99effde0ae0e4ddb51411262c6675e50.js"></script> |
12
practicer OP @araraloren
@YUX @itlr @ila @wbt @qianbaooffer 我的 1 、 2 、 3 个函数里面都设置了等待时间,也就是爬 page 链接的时候等一段时间,爬 book 链接的时候也等一段时间,爬 book 信息的时候还是会等一段时间,这样做是为了不给对方太大压力,虽然我知道我的小爬虫根本不会给他们带来任何负担,但这就是我的原则吧。我想改进的地方是,如何让这三个函数之间有(异步|多线程|多进程)处理的可能,从而改善爬虫的速度 |
13
practicer OP @Allianzcortex 看起来不错喔,刚好在看 multiprocessing 和 queue 的手册,冥冥中感觉到是我想要的,感谢分享。
|
14
Jblue 2016-06-13 11:30:54 +08:00
1 可以单独抽出来,把所有的需爬 url 去重之后集中放在一起(比如队列),然后 23 放在一起,每个线程从队列中获取一个 url 单独消化。
|
15
EchoUtopia 2016-06-13 12:41:15 +08:00
https://github.com/EchoUtopia/my-python-practices/blob/master/simplemultithreadsCrawler.py
重写 parse_links ,写自己的解析逻辑就行了 |
17
geek123 2016-06-13 13:54:49 +08:00
|
18
YUX 2016-06-13 13:59:02 +08:00 via iPhone
@practicer requests futures 有 ThreadPoolExecutor 和 ProcessPoolExecutot 两个用法
用 max worker 直接控制频率多好 |
19
louk78 2016-06-13 14:16:14 +08:00
如果有 A,B,C,D 四件事情,单线程是一件事情完成之后在做另外一件事情,而多线程则可以, a 线程做 A 事情, b 线程做 B 事情, c 线程做 C 事情,d 线程做 D 事情,这四件事情可以同时做,当然有做的快的,也有做的慢,四个线程可以看出四个人,四件事情可以看成,喂孩子吃奶,做饭,扫地,洗碗
|
20
JhOOOn 2016-06-13 16:06:34 +08:00
学爬虫一定是 python , 爬网站一定是 douban , douban :“我特么的招谁惹谁了”
|
22
likuku 2016-06-13 16:29:45 +08:00
python 多线程因为 GIL 所以,对 CPU 密集型应用没改善,需要等 IO 的,有帮助;
多进程可以用到多核 /多 CPU, 应对 CPU 密集型应用。 |
23
practicer OP |
26
alexapollo 2016-06-13 19:49:53 +08:00
送几个老例子:
Scrapy: 爬取豆瓣书籍 //以及几个简单实例 http://www.oschina.net/code/snippet_1026739_33016 128 进程,图片爬虫,增量更新 http://www.oschina.net/code/snippet_1026739_43930 以及可以戳这里: https://github.com/geekan/scrapy-examples |
27
practicer OP @alexapollo
@YUX @likuku @geek123 @EchoUtopia @Jblue 最后我放弃用多线程|多进程改这个爬虫了,还是没弄懂,打算多读一读各位列出的源码。 后面修改了一次爬虫,从逻辑上减少了一轮解析 HTML 的次数,也算是减少了爬取网页的时间: 1.fetchBooks(u'爬虫') 2.exportCsv(bookUrls) 解析页面分页的时候把 book 的详细页和翻页链接一次保存,上一个版本中为了得到他们 urlopen 了两次,比较浪费时间,另外用 global variable 来更新 book 详细页,翻页链接用递归来获取。 # -*- coding: UTF-8 -*- import os import re import time import json import random import urlparse import unicodecsv as csv from urllib2 import urlopen from urllib2 import HTTPError from bs4 import BeautifulSoup import logging logging.basicConfig(filename='douban.log', level=logging.DEBUG) bookUrls = set() def fetchBooks(start): '''递归爬取翻页链接,同时获取该标签下所有书籍的 url''' first = u'https://book.douban.com/tag/' + start newPage = findPages(first) while newPage: newPage = findPages(newPage) print 'Scraping books on page {!r} done'.format(newPage) logging.info('Scraping books on page {!r} done'.format(newPage)) time.sleep(random.randint(1, 10)) def exportCsv(books): '''写书籍详细信息到 csv 文件''' data = (download(book) for book in books) with open(os.path.join(os.path.dirname(__file__), 'books.csv'), 'wb') as f: # with open('books.csv', 'wb') as f: writer = csv.writer(f) headers = (u'书名', u'原书名', u'出版日期', u'页数', u'豆瓣评分', u'评价人数', u'ISBN', u'网址', u'TOP 评论') writer.writerow(headers) for line in data: writer.writerow(line) print 'Saving the book {} done'.format(line[6]) logging.info('Saving the book {} done'.format(line[6])) time.sleep(random.randint(1, 10)) print 'Saving ALL done' logging.info('Saving ALL done') def findPages(pageUrl): '''解析豆瓣图书分页 html ,获取翻页按钮链接,每页一个链接''' html = urlopen(iriToUri(pageUrl)) bsObj = BeautifulSoup(html) linkEle = bsObj.find('link', {'rel': 'next'}) if linkEle is not None: if 'href' in linkEle.attrs: findBooks(bsObj) return u'https://book.douban.com' + linkEle.attrs['href'] def findBooks(bsObj): '''解析豆瓣图书分页 html ,获取书籍详细页链接,每页 20 个链接''' global bookUrls books = bsObj.findAll('a', {'class': 'nbg'}) try: if books is not None: for book in books: if 'href' in book.attrs and book.attrs['href'] not in bookUrls: print 'Found new book: {}'.format(book.attrs['href']) logging.info('Found new book: {}'.format(book.attrs['href'])) bookUrls.add(book.attrs['href']) return bookUrls except Exception as e: print e.message logging.exception('{}'.format(e)) def urlEncodeNonAscii(b): """将 non-ascii 转成 ascii 字符""" return re.sub('[\x80-\xFF]', lambda c: '%%%02x' % ord(c.group(0)), b) def iriToUri(iri): """打开带中文的网址,将 iri 转为 uri ,""" parts = urlparse.urlparse(iri) return urlparse.urlunparse( part.encode('idna') if parti == 1 else urlEncodeNonAscii(part.encode('utf-8')) for parti, part in enumerate(parts) ) def getFullReview(reviewId): '''抓包解析 review 内容''' url = 'https://book.douban.com/j/review/' + str(reviewId) + '/fullinfo' try: html = json.loads(urlopen(url).read())['html'] except HTTPError as e : print e.message logging.error('Error: {}'.format(e)) return None fullReview = re.search('.*(?=<div)', html).group() if fullReview is not None: return fullReview def download(bookUrl): '''解析书籍详细页''' html = urlopen(bookUrl) bsObj = BeautifulSoup(html) try: isbn = bsObj.find(id='info').find( text=re.compile('(\d{10})|(\d{13})')).strip() except AttributeError as e: print e.message logging.exception('{}'.format(e)) isbn = '' try: publishY = bsObj.find(id='info').find( text=re.compile('\d{4}-\d{1,2}(-\d{1,2})?')).strip() except AttributeError as e: print e.message logging.exception('{}'.format(e)) publishY = '' try: pageNum = bsObj.find(id='info').find( text=re.compile('^\s\d{3,4}$')).strip() except AttributeError as e: print e.message logging.exception('{}'.format(e)) pageNum = '' try: origName = bsObj.find(id='info').find(text=u'原作名:') if origName is not None: origName = bsObj.find(id='info').find( text=u'原作名:').parent.next_sibling.strip() except AttributeError as e: print e.message logging.exception('{}'.format(e)) origName = '' try: rating = bsObj.find( 'strong', {'class': 'll rating_num '}).get_text().strip() except AttributeError as e: print e.message logging.exception('{}'.format(e)) rating = '' try: numRating = bsObj.find( 'span', {'property': 'v:votes'}).get_text() except AttributeError as e: print e.message logging.exception('{}'.format(e)) numRating = '' try: reviewId = bsObj.find( 'div', {'id': re.compile(r'tb-(\d+)')}).attrs['id'][3:] review = getFullReview(reviewId) except AttributeError as e: print e.message logging.exception('{}'.format(e)) review = '' title = bsObj.find('span', {'property': 'v:itemreviewed'}).get_text() addr = bookUrl return (title, origName, publishY, pageNum, rating, numRating, isbn, addr, review) if __name__ == '__main__': print 'Starting at: {}'.format(time.ctime()) logging.info('Starting at: {}'.format(time.ctime())) fetchBooks(u'股票') exportCsv(bookUrls) print 'All finished at: {}'.format(time.ctime()) logging.info('All finished at: {}'.format(time.ctime())) |
28
EchoUtopia 2016-06-14 12:55:40 +08:00
@practicer 爬虫最好用异步,这有一篇教程,用 python3 异步模块编写爬虫,真的很经典
http://aosabook.org/en/500L/a-web-crawler-with-asyncio-coroutines.html |
29
practicer OP @EchoUtopia 好难懂,我慢慢啃吧,谢谢分享。
|
30
EchoUtopia 2016-06-14 15:48:30 +08:00
@practicer 可以直接看代码,结合着 python 的 asyncio 模块文档,很快的
https://github.com/aosabook/500lines/blob/master/crawler/code/crawling.py |
31
practicer OP 这段时间一直在熟悉 scrapy ,得知它由异步框架 twisted 搭建的,并且用 scrapy 对比自己写的爬虫,深深感受到 scrapy 异步回调的威力。
爬虫的正确姿势是异步编程。推荐一个讲解异步模型( twisted 框架)的电子书,从浅到深介绍如何将同步程序重构成异步非阻塞程序 https://www.gitbook.com/book/likebeta/twisted-intro-cn/details 该书第 17 章----生成器实现的异步方式,便是 scrapy 中最常使用的方法了 https://likebeta.gitbooks.io/twisted-intro-cn/content/zh/p17.html 。还有 @EchoUtopia 推荐的文章中介绍的的 asyncio 模块,都是正确的爬虫姿势。 |