V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
practicer
V2EX  ›  Python

python 多线程爬虫问题

  •  1
     
  •   practicer · 2016-06-13 00:06:21 +08:00 · 7680 次点击
    这是一个创建于 3091 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我刚入门 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 文件

    可以看到每个函数接受上一个函数返回的结果。

    我的问题是,怎样可以把这些函数变成多线程处理,我在网上花了点时间搜索没有找到答案,也许多线程属于高级主题,对我这种初学者来说理解比较困难,请网友不吝赐教。

    33 条回复    2016-11-21 16:25:54 +08:00
    itlr
        1
    itlr  
       2016-06-13 02:27:55 +08:00
    步骤 1 后可以用 multiprocessing 对各个 page 并行采集,用 Pool , starmap_async()这样的调用,具体要参考文档 https://docs.python.org/2/library/multiprocessing.html
    YUX
        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)
    YUX
        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
    practicer
        4
    practicer  
    OP
       2016-06-13 08:29:50 +08:00
    @itlr
    @YUX
    很感谢两位的指教,能不能再帮我指点一下这种需求属于多线程还是多进程。
    ila
        5
    ila  
       2016-06-13 08:51:19 +08:00 via Android
    试试了,不同图书分类一个进程
    araraloren
        6
    araraloren  
       2016-06-13 09:46:01 +08:00
    ~~把每个步骤放在一个线程里面就是多线程了,不过要注意公共数据的访问可能需要互斥
    刚入门 python ,还是先来一个模块化的吧,然后学习多线程改进程序
    wbt
        7
    wbt  
       2016-06-13 10:08:05 +08:00
    Python 是多线程性能并不好
    先一个线程试试吧,不行就开多个进程。
    qianbaooffer
        8
    qianbaooffer  
       2016-06-13 10:10:33 +08:00
    对于这种网络 io,python 多线程对 GIL 做了优化,性能没有问题,如果不是 IO 类处理,那多线程确实有问题
    @wbt
    wbt
        9
    wbt  
       2016-06-13 10:44:31 +08:00
    @qianbaooffer 学习了~
    laoni
        10
    laoni  
       2016-06-13 10:52:01 +08:00
    PY scipy 为啥不用。。。。还自己写。。 一直跑 scipy 相当稳定靠谱。。
    Allianzcortex
        11
    Allianzcortex  
       2016-06-13 11:07:44 +08:00
    用 multiprocessing 库, Queue 来实现 FIFO 的任务队列,当时爬的是拉钩,自己之前写过一个学习用的 demo ,比较简答,有注释,可以直接套用:

    <script src="https://gist.github.com/Allianzcortex/99effde0ae0e4ddb51411262c6675e50.js"></script>
    practicer
        12
    practicer  
    OP
       2016-06-13 11:19:34 +08:00
    @araraloren
    @YUX
    @itlr
    @ila
    @wbt
    @qianbaooffer

    我的 1 、 2 、 3 个函数里面都设置了等待时间,也就是爬 page 链接的时候等一段时间,爬 book 链接的时候也等一段时间,爬 book 信息的时候还是会等一段时间,这样做是为了不给对方太大压力,虽然我知道我的小爬虫根本不会给他们带来任何负担,但这就是我的原则吧。我想改进的地方是,如何让这三个函数之间有(异步|多线程|多进程)处理的可能,从而改善爬虫的速度
    practicer
        13
    practicer  
    OP
       2016-06-13 11:23:31 +08:00
    @Allianzcortex 看起来不错喔,刚好在看 multiprocessing 和 queue 的手册,冥冥中感觉到是我想要的,感谢分享。
    Jblue
        14
    Jblue  
       2016-06-13 11:30:54 +08:00
    1 可以单独抽出来,把所有的需爬 url 去重之后集中放在一起(比如队列),然后 23 放在一起,每个线程从队列中获取一个 url 单独消化。
    EchoUtopia
        15
    EchoUtopia  
       2016-06-13 12:41:15 +08:00
    practicer
        16
    practicer  
    OP
       2016-06-13 13:52:23 +08:00
    @Jblue 做你说的多线程和队列要用到哪些库和方法?能详细说一下吗?
    geek123
        17
    geek123  
       2016-06-13 13:54:49 +08:00
    @practicer 我们网站上有个网友写过一个 free 的课程,你可以看看。

    http://www.hubwiz.com/course/570dce425fd193d76fcc723d/
    YUX
        18
    YUX  
       2016-06-13 13:59:02 +08:00 via iPhone
    @practicer requests futures 有 ThreadPoolExecutor 和 ProcessPoolExecutot 两个用法
    用 max worker 直接控制频率多好
    louk78
        19
    louk78  
       2016-06-13 14:16:14 +08:00
    如果有 A,B,C,D 四件事情,单线程是一件事情完成之后在做另外一件事情,而多线程则可以, a 线程做 A 事情, b 线程做 B 事情, c 线程做 C 事情,d 线程做 D 事情,这四件事情可以同时做,当然有做的快的,也有做的慢,四个线程可以看出四个人,四件事情可以看成,喂孩子吃奶,做饭,扫地,洗碗
    JhOOOn
        20
    JhOOOn  
       2016-06-13 16:06:34 +08:00
    学爬虫一定是 python , 爬网站一定是 douban , douban :“我特么的招谁惹谁了”
    YUX
        21
    YUX  
       2016-06-13 16:21:10 +08:00 via iPhone
    @JhOOOn 还有知乎 好像都想爬知乎 也不知道爬完了做什么 好像只有一个看知乎还有点意思
    likuku
        22
    likuku  
       2016-06-13 16:29:45 +08:00
    python 多线程因为 GIL 所以,对 CPU 密集型应用没改善,需要等 IO 的,有帮助;
    多进程可以用到多核 /多 CPU, 应对 CPU 密集型应用。
    practicer
        23
    practicer  
    OP
       2016-06-13 16:35:19 +08:00
    @YUX
    @louk78
    @geek123
    @Jblue
    @EchoUtopia

    谢谢各位,我再改一下看看,边改边学好欢乐, HOHO
    practicer
        24
    practicer  
    OP
       2016-06-13 16:37:01 +08:00
    @JhOOOn 还有爬知乎妹子头像、 1024 ,都是入门爬虫的标准目标啊,因为他们都拥有有价值的数据。
    practicer
        25
    practicer  
    OP
       2016-06-13 16:38:19 +08:00
    @likuku 请教大大我的需求是哪种的?
    alexapollo
        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
    practicer
        27
    practicer  
    OP
       2016-06-14 12:42:08 +08:00
    @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()))
    EchoUtopia
        28
    EchoUtopia  
       2016-06-14 12:55:40 +08:00
    @practicer 爬虫最好用异步,这有一篇教程,用 python3 异步模块编写爬虫,真的很经典
    http://aosabook.org/en/500L/a-web-crawler-with-asyncio-coroutines.html
    practicer
        29
    practicer  
    OP
       2016-06-14 14:18:10 +08:00
    @EchoUtopia 好难懂,我慢慢啃吧,谢谢分享。
    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
    practicer
        31
    practicer  
    OP
       2016-06-28 15:24:22 +08:00
    这段时间一直在熟悉 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 模块,都是正确的爬虫姿势。
    nik
        32
    nik  
       2016-11-16 15:17:22 +08:00
    @practicer 不知你是否在北京?我们公司需要爬虫工程师
    practicer
        33
    practicer  
    OP
       2016-11-21 16:25:54 +08:00
    @nik 我在十八线省会城市, 不在北京
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   361 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 21:55 · PVG 05:55 · LAX 13:55 · JFK 16:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.