从零开始写Python爬虫 --- 爬虫应用:IT之家热门段子(评论)爬取

从零开始写Python爬虫 --- 爬虫应用:IT之家热门段子(评论)爬取

不知道这里有没有喜欢刷it之家的小伙伴,我反正每天早上醒来第一件事就是打开it之家,看看有没有新鲜的段子<热评> 逃~
其实这次是要来抓取it之家的热门评论,因为数量较多(上万),所以我们这次采用MongoDB来存数数据

关键词:

这次爬虫不像原本的小脚本啦,
对速度和质量都有比较高的要求,
所以会涉及到一些我原本没有介绍的知识:

  • Ajax
  • 多进程
  • MongoDB
  • 生成器...

先来看一下成果

数据库展示:



这是MongoDB的GUI软件(RoBo 3T)的截图

可以看到 在 ithome这个数据库里我们
抓到了 21745条object 即21745条热门评论

点开一条记录是这样显示的:


既抓取的内容为:
发帖人、评论内容、手机厂商、手机型号、位置、时间

爬虫运行的时候是这样的:



思路分析

首先看一下项目目录:

.
├── __pycache__
│   ├── config.cpython-36.pyc
│   └── pipeline.cpython-36.pyc
├── apple.json # 导出的数据结果
├── config.py  #  数据库的配置信息
├── datahandleer.py # 数据后期处理的文件
├── pipeline.py # 数据处理
└── spider.py  # 爬虫主文件

如和获取热评呢

我们先随便点开一篇it之家新闻的链接
ithome.com/html/it/3230

一开始肯定是直接尝试用requests访问新闻的链接,
并用解析工具解析出热评的位置,在抓取就好

可是我尝试后发现,requests抓去下来的源文件
并没有评论的内容

仔细一想,那一般是通过ajax来动态加载内容的

用到ajax的情形一般是这样的:你访问一个网页,不用刷新跳转,只要点一个按钮,或者鼠标往下滚,新的信息就会自动出来,例如Facebook的timeline

猜到了大概方向之后,

我们 切换到开发者工具的 Network
并勾选Preserve log
最后切换到 XHR这个标签页

这个时候我们在刷新页面,就能捕捉到网页的各种请求了。




热评是在一个名为(getajaxdata.aspx)的连接里出来的
通过这个名字,大家也能看出来这是一个ajax请求吧?

我们再来看一下这个请求的headers



仔细分析一下,不难得出,
这是一个向:dyn.ithome.com/ithome/g
发送的POST请求,PSOT的参数是:

newsID:323076 type:hotcomment

再仔细的分析一下,发现这个newsid 就明文写在新闻的url之中

我们重新整理一下思路

  • 热评是通过ajax请求获取的
  • 该POST请求需要两个参数:newsid 、type

那是不是意味着,我们不需要访问新闻的页面,
直接向it之家ajax服务器发送符合要求的请求就可以获取到数据呢?

我直接告诉你们,是可以的!

那么,只剩下一个问题了:
newsid如何获取呢?

这个其实更简单了,我们只要点开随便一个新闻分类

我这里用苹果作为演示分类

url: it.ithome.com/apple/

打开这个页面,发现所有的新闻链接都包裹在li 标签之中,
我们只需要找到并用办法解析出最后的newsid就可以了。




等等,最下面那个红色按钮是不是感到很眼熟?
是不是又是通过Ajax请求来获取下一页的新闻?
当然是的!(这里我就不截图演示了,大家可以自己去尝试一下分析)

爬虫流程
当所有的思路都通顺之后,
我们的热评爬虫是这样运行的:

  • spider不停的访问某一新闻分类下的page页面,并获取newsid
  • 将newisd 交给hotcomment爬虫来爬取热评数据
  • 将数据交给pipeline里的函数,
  • 最后将数据存入数据库

代码展示:

由于前面的思路已经详细的讲过了
代码里也有比较详细的注释,
我就不一一说明了

获取newsid的部分

def parse_news_id(categoryid, page_start):
    '''
    找到当前分类下首页的文章的id

    retrun newsid <str>
    '''
    data = {
        'categoryid': categoryid,
        'type': 'pccategorypage',
        'page': '1',
    }

    # 循环获取newsid 最早可到2014年12月
    # 默认每次取10页
    for page in range(page_start, page_start + 11):
        data['page'] = str(page)
        try:
            r = requests.post(
                'http://it.ithome.com/ithome/getajaxdata.aspx', data=data)
            soup = BeautifulSoup(r.text, 'lxml')
            news_list = soup.find_all('a', class_='list_thumbnail')
            # 找到当前页的所有新闻链接之后,用生成器返回newsid
            for news in news_list:
                yield news['href'].split('/')[-1].replace('.htm', '')
    except:
            return None

这里我稍微说一下,
这里用到了关键字 yeild
这样,就将我们的parse_news_id()函数变成了一个生成器函数

用起来是这样的:每当抓到一个newsid,就返回给parse_hot_comment使用,当热评抓完了之后,调回头来据需抓取下一个newsid。这样动态的抓取,既节省了内存,又能提高效率,Scrapy框架也是这么设计的哟

获取热评的部分

def parse_hot_comment(newsid):
    '''
    找到it之家新闻的热评

    return :info_list <list>
    '''
    info_list = []
    data = {
        'newsID': newsid,
        'type': 'hotcomment'
    }
    try:
        r = requests.post(
            'https://dyn.ithome.com/ithome/getajaxdata.aspx', data=data)
        r.raise_for_status()
        r.encoding = r.apparent_encoding
        soup = BeautifulSoup(r.text, 'lxml')
        comment_list = soup.find_all('li', class_='entry')
        for comment in comment_list:
            # 评论内容
            content = comment.find('p').text
            # 用户名
            name = comment.find('strong', class_='nick').get_text()
            # 其他信息
            info = comment.find('div', class_='info rmp').find_all('span')
            # 判断用户是否填写了手机尾巴
            # 对信息做出咸蛋的处理
            # 抓取到 手机厂商、型号、位置、时间
            # 方便最后做数据分析
            if len(info) > 1:
                phone_com = info[0].text.split(' ')[0]
                phone_model = info[0].text.split(' ')[1]
                loc = info[1].text.replace('IT之家', '').replace(
                    '网友', ' ').replace('\xa0', '').split(' ')[0]
                time = info[1].text.replace('IT之家', '').replace(
                    '网友', ' ').replace('\xa0', '').split(' ')[2]
            else:
                phone_com = '暂无'
                phone_model = '暂无'
                loc = info[0].text.replace('IT之家', '').replace(
                    '网友', ' ').replace('\xa0', '').split(' ')[0]
                time = info[0].text.replace('IT之家', '').replace(
                    '网友', ' ').replace('\xa0', '').split(' ')[2]

            info_list.append(
                {'name': name, 'content': content, 'phone_com': phone_com, 'phone_model': phone_model, 'loc': loc, 'time': time, })

        return info_list
    except:
        return None

数据存储的部分

# config.py
# 数据库url
MONGO_URL = 'localhost'
# 数据库名
MONGO_DB = 'ithome'
# 数据库表
MONGO_TABLE = 'hotcomment_it'

# pipeline.py
from pymongo import MongoClient
from config import *

client = MongoClient(MONGO_URL, connect=True)
db = client[MONGO_DB]

# 将记录写入数据库
def save_to_mongo(result):
    if db[MONGO_TABLE].insert(result):
        print('存储成功', result)
        return True
    return False

有可以优化的地方么?

有!速度!

# 写了一个检测函数运行时间的装饰器
def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)  # 装饰被装饰的函数

        timepassed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)

        print('[{:.8f}s]   {}({})  -> {}'.format(timepassed, name, arg_str, result))
    return clocked
    
@clock
def main(page_start):
    # 新闻分类的id
    ID = '31'
    # 建立苹果新闻分类对象
    apple = parse_news_id(ID, page_start)

    # 利用迭代器抓取热评
    for newsid in apple:
        hot_comment_dic = parse_hot_comment(newsid)
        if hot_comment_dic:
            for comment in hot_comment_dic:
                save_to_mongo(comment)
        else:
            print('没有抓取到热评,一般是文章太过久远')
            
'''
开启多进程之前 ,抓取一页新闻的所有热评所话费的时间
[8.45930967s]   main()  -> None

抓取10页:
[112.86940903s]   main(61)  -> None
'''

利用进程池开启多进程

if __name__ == '__main__':

    # 单进程模式
    # main(1)
    
    # 开启多进程模式
    from multiprocessing import Pool
    pool = Pool()  
    # 进程池,每个进程抓取10页新闻的热评
    groups = ([x for x in range(111, 191,10)])
    pool.map(main, groups)
    pool.close()
    pool.join()

'''
开启后:
不能使用装饰器测时间了
AttributeError: Can't pickle local object 'clock.<locals>.clocked'
改为第三方秒表计时:

爬取1~40页:
time:1:56.54
可以看到 速度快了三倍!
'''

由于整体的代码太长了,
就不再贴上来了,有需要全部代码的,
可以去我的github(放在文末了)上看

最后总结一下

这次一共抓取了2W+条数据
但如果仅仅是有这些数据,
而不去分析,整理,那么数据就没有任何意义。

我们很多人是通过学爬虫入门Python的。
但爬虫重要的并不是爬虫本身,而是爬出来的数据。

那么我们是不是应该想想,
有什么办法能够将数据整理并展示出来呢?

熟悉我的小伙伴都知道,
我现在主要在发展的方向是Python Web
对于数据分析 我是有所逃避的

因为在我的潜意识里,我就是个文科生,
拼 算法、思路、最优解、对数据的敏感程度
我是肯定拼不过那些理科工科生的

帮助我入门的@路人甲就是做数据分析的
大家都喊他数据帝

甲哥他在我最迷茫的时候,(刚刚入门)帮助过我,让我走出了迷茫,想知道的可以看这里: zhihu.com/question/4650

现在在他的建议下,我不在逃避这方面的弱点,
开始经常刷leetcode上的算法题,
虽然过程很难熬,有的时候半个小时都没有思路。
但毕竟会是有用的对吧?

咳咳,其实说了这么多,

一是想告诉大家,不要为了写爬虫而写爬虫
二是,我其实已经将这数据做出了一点点分析,回头我整理好会发出来
题目大概叫 帮你上热评

大家要多来点赞哦
逃 ~

每天的学习记录都会 同步更新到:
微信公众号: findyourownway

知乎专栏:zhuanlan.zhihu.com/Ehco

blog : www.ehcoblog.ml

Github: github.com/Ehco1996/Pyt

编辑于 2017-08-26

文章被以下专栏收录