Python 与 网络爬虫

好久不见. 提交于 2020-02-15 02:11:41

Python 与 网络爬虫

本文由 CDFMLR 原创,收录于个人主页 https://clownote.github.io,并同时发布到 CSDN。本人不保证 CSDN 排版正确,敬请访问 clownote 以获得良好的阅读体验。

爬虫的基本原理

爬虫是获取网页并提取保存信息的自动化程序

可以说,我们能在浏览器中看到的一切内容,都可以通过爬虫得到(包括那些由JavaScript渲染出来的网页)。

爬虫主要解决以下几个问题:

获取网页

构造一个请求并发送给服务器,然后接收到响应并将其解析出来。

我们可以用urllib、 requests 等库来帮助我们实现 HTTP请求操作,请求和响应都可以用类库提供的数据结构来表示,得到响应之后只需要解析数据结构中的 Body 部分即可,即得到网页的源代码。

提取信息

分析网页源代码,从中提取我们想要的数据。

最通用的方法是采用 正则表达式 提取,这是一个万能的方法,但是在构造正则表达式时比较复杂且容易出错。
使用 Beautiful Soup、 pyquery、 lxml 等库,我们可以高效快速地从中提 取网页信息,如节点的属性、文本值等。

保存数据

将提取到的数据保存到某处以便后续使用。

自动化程序

让爬虫来代替人,完成上述这些操作。
并在抓取过程中进行各种异常处理、错误重试等操作,确保爬取持续高效地运行。

爬虫实践 – 抓取电影排行

爬虫实践:利用 Requests 和正则表达式来抓取猫眼电影 TOP100 的相关内容。

目标

我们打算提取出 猫眼电影 TOP100 榜 的电影名称、时间、评分、图片等信息,提取的结果我们以文件形式保存下来。

准备

  • 系统环境:macOS High Sierra 10.13.6
  • 开发语言:Python 3.7.2 (default)
  • 第三方库:Requests

分析

目标站点https://maoyan.com/board/4

打开页面后我们可以发现,页面中显示的有效信息有 影片名称主演上映时间上映地区评分图片

然后,我们查看一下排名第一的条目的 html 源码,一会儿我们就从这段代码入手设计正则表达式:

<dd>
        <i class="board-index board-index-1">1</i>
        <a href="/films/1203" title="霸王别姬" class="image-link" data-act="boarditem-click" data-val="{movieId:1203}">
                <img src="//s0.meituan.net/bs/?f=myfe/mywww:/image/loading_2.e3d934bf.png" alt=""
                        class="poster-default" />
                <img data-src="https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c"
                        alt="霸王别姬" class="board-img" />
        </a>
        <div class="board-item-main">
                <div class="board-item-content">
                        <div class="movie-item-info">
                                <p class="name"><a href="/films/1203" title="霸王别姬" data-act="boarditem-click"
                                                data-val="{movieId:1203}">霸王别姬</a></p>
                                <p class="star">
                                        主演:张国荣,张丰毅,巩俐
                                </p>
                                <p class="releasetime">上映时间:1993-01-01</p>
                        </div>
                        <div class="movie-item-number score-num">
                                <p class="score"><i class="integer">9.</i><i class="fraction">6</i></p>
                        </div>

                </div>
        </div>

</dd>

再来看看翻页会发生什么:
url 从 https://maoyan.com/board/4 变成了 https://maoyan.com/board/4?offset=10
嗯,多了一个 ?offset=10
再下一页,变成了?offset=20
原来,每翻一页offset就加10(一页显示刚好是10个条目,这很合理。)
其实我们甚至可以尝试把值改成0(首页),或任何在范围 [0, 100) 内的值。

设计

我们现在设计一个可以完成目标的爬虫程序。

这个爬虫应该有这几个部分:

  • 抓取页面(同时也要注意配合翻页的问题处理):可以用 requests.get() 来请求,最好再伪造一组headers。
  • 正则提取(匹配出 影片名称、主演、上映时间、上映地区、评分、图片):用 re.findall() 和适当的正则表达式来提取信息。
  • 写入文件(用JSON格式去保存信息):涉及到 json.dumps() 与 文件写入

现在,我们还需要设计尤为关键的正则表达式:

'<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)@.*?title="(.*?)".*?主演:(.*?)\s*</p>.*?上映时间:(.*?)</p>.*?integer">(.*?)</i>.*?fraction">(.*?)</i></p>'
匹配到的顺序是:(排名, 图片地址, 名称, 主演, 上映时间, 评分整数部分, 评分小数部分)
需要使用 re.S

实现

我们现在来按照设计实现第一个版本的程序:

import re
import json
import time

import requests

url = 'https://maoyan.com/board/4'
filename = './movies.txt'
pattern = r'<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)@.*?title="(.*?)".*?主演:(.*?)\s*</p>.*?上映时间:(.*?)</p>.*?integer">(.*?)</i>.*?fraction">(.*?)</i></p>'

headers = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15',
        'Accept-Language': 'zh-cn'
        }

def get_page(url):   # 抓取页面,返回html字符串
    print('\tGetting...')
    try:
        response = requests.get(url, headers=headers)
        return response.text
    except Exception as e:
        print('[Error]', e)
        return ''


def extract(html):  # 正则提取,返回结果dict的list
    print('\tExtracting...')
    raws = re.findall(pattern, html, re.S)   # [(排名, 图片地址, 名称, 主演, 上映时间, 评分整数部分, 评分小数部分), ...]
    result = []
    for raw in raws:
        dc = {                      # 在这里调整了顺序
                'index': raw[0],
                'title': raw[2],
                'stars': raw[3],
                'otime': raw[4],
                'score': raw[5] + raw[6],   # 合并整数、小数
                'image': raw[1]
                }
        result.append(dc)

    return result
    

def save(data):      # 写入文件
    print('\tSaving...')
    with open(filename, 'a', encoding='utf-8') as f:
        for i in data:
            f.write(json.dumps(i, ensure_ascii=False) + '\n')


if __name__ == '__main__':
    for i in range(0, 100, 10):     # 翻页
        target = url + '?offset=' + str(i)
        print('[%s%%](%s)' % (i, target))
        page = get_page(target)
        data = extract(page)
        save(data)
        time.sleep(0.5)     # 防制请求过密集被封

    print('[100%] All Finished.\n Results in', filename)

调试

运行程序,如果一切顺利,我们将得到结果:

{"index": "1", "title": "霸王别姬", "stars": "张国荣,张丰毅,巩俐", "otime": "1993-01-01", "score": "9.6", "image": "https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg"}
{"index": "2", "title": "肖申克的救赎", "stars": "蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿", "otime": "1994-10-14(美国)", "score": "9.5", "image": "https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg"}
{"index": "3", "title": "罗马假日", "stars": "格利高里·派克,奥黛丽·赫本,埃迪·艾伯特", "otime": "1953-09-02(美国)", "score": "9.1", "image": "https://p0.meituan.net/movie/54617769d96807e4d81804284ffe2a27239007.jpg"}
{"index": "4", "title": "这个杀手不太冷", "stars": "让·雷诺,加里·奥德曼,娜塔莉·波特曼", "otime": "1994-09-14(法国)", "score": "9.5", "image": "https://p0.meituan.net/movie/e55ec5d18ccc83ba7db68caae54f165f95924.jpg"}
{"index": "5", "title": "泰坦尼克号", "stars": "莱昂纳多·迪卡普里奥,凯特·温丝莱特,比利·赞恩", "otime": "1998-04-03", "score": "9.6", "image": "https://p1.meituan.net/movie/0699ac97c82cf01638aa5023562d6134351277.jpg"}

乍一看好像没有问题了,我们需要的结果都得到了。
但是,仔细查看,发现还是会有几个问题存在:

  1. 主演(stars)一般都有几个人,我们最好把他们分开用一个list或是tuple来安置。
  2. 信息每一个条目直接相互分离,不便于传输与取用。

要解决第一个问题,我们可以在 extract 中增加一部分来处理这个问题,而第二个问题则需要我们将所有页都读取完成,放到指定位置,再统一写入文件。

修改源程序:

新增 函数,处理主演信息,得到一个list:

def stars_split(st):
    return st.split(',')

修改 extract(),在其中添加 stars_split 的调用:

def extract(html):  # 正则提取,返回结果dict的list
    print('\tExtracting...')
    raws = re.findall(pattern, html, re.S)   # [(排名, 图片地址, 名称, 主演, 上映时间, 评分整数部分, 评分小数部分), ...]
    result = []
    for raw in raws:
        dc = {                      # 在这里调整了顺序
                'index': raw[0],
                'title': raw[2],
                'stars': stars_split(raw[3]),   # 【修改】:分离主演
                'otime': raw[4],
                'score': raw[5] + raw[6],       # 合并整数、小数
                'image': raw[1]
                }
        result.append(dc)

    return result

新增 一个全局变量、函数,实现结果的整合:

result = {'top movies': []}

def merge(data):
    print('\tMerging...')
    result['top movies'] += data

修改 save:

def save(data):      # 写入文件
    print('Saving...')
    with open(filename, 'a', encoding='utf-8') as f:
        f.write(json.dumps(data, ensure_ascii=False))

修改程序框架:

if __name__ == '__main__':
    for i in range(0, 100, 10):     # 翻页
        target = url + '?offset=' + str(i)
        print('[%s%%](%s)' % (i, target))
        page = get_page(target)
        data = extract(page)
        merge(data)
        time.sleep(0.5)     # 防制请求过密集被封
        
    save(result)
    print('[100%] All Finished.\n Results in', filename)

整合代码:

import re
import json
import time

import requests

url = 'https://maoyan.com/board/4'
result = {'top movies': []}
filename = './movies.json'      # (最好把保存的文件名改一下,否则会添加到上次运行结果的后面)

pattern = r'<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)@.*?title="(.*?)".*?主演:(.*?)\s*</p>.*?上映时间:(.*?)</p>.*?integer">(.*?)</i>.*?fraction">(.*?)</i></p>'

headers = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15',
        'Accept-Language': 'zh-cn'
        }


def get_page(url):   # 抓取页面,返回html字符串
    print('\tGetting...')
    try:
        response = requests.get(url, headers=headers)
        return response.text
    except Exception as e:
        print('[Error]', e)
        return ''


def stars_split(st):
    return st.split(',')


def extract(html):  # 正则提取,返回结果dict的list
    print('\tExtracting...')
    raws = re.findall(pattern, html, re.S)   # [(排名, 图片地址, 名称, 主演, 上映时间, 评分整数部分, 评分小数部分), ...]
    result = []
    for raw in raws:
        dc = {                      # 在这里调整了顺序
                'index': raw[0],
                'title': raw[2],
                'stars': stars_split(raw[3]),   # 分离主演
                'otime': raw[4],
                'score': raw[5] + raw[6],       # 合并整数、小数
                'image': raw[1]
                }
        result.append(dc)

    return result
    

def merge(data):
    print('\tMerging...')
    result['top movies'] += data
    
    
def save(data):      # 写入文件
    print('Saving...')
    with open(filename, 'a', encoding='utf-8') as f:
        f.write(json.dumps(data, ensure_ascii=False))


if __name__ == '__main__':
    for i in range(0, 100, 10):     # 翻页
        target = url + '?offset=' + str(i)
        print('[%s%%](%s)' % (i, target))
        page = get_page(target)
        data = extract(page)
        merge(data)
        time.sleep(0.5)     # 防制请求过密集被封
        
    save(result)
    print('[100%] All Finished.\n Results in', filename)

运行修改完毕的程序,得到新的结果:

{"top movies": [{"index": "1", "title": "霸王别姬", "stars": ["张国荣", "张丰毅", "巩俐"], "otime": "1993-01-01", "score": "9.6", "image": "https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg"}, {"index": "2", "title": "肖申克的救赎", "stars": ["蒂姆·罗宾斯", "摩根·弗里曼", "鲍勃·冈顿"], "otime": "1994-10-14(美国)", "score": "9.5", "image": "https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg"}, ..., {"index": "100", "title": "龙猫", "stars": ["秦岚", "糸井重里", "岛本须美"], "otime": "2018-12-14", "score": "9.2", "image": "https://p0.meituan.net/movie/c304c687e287c7c2f9e22cf78257872d277201.jpg"}]}

这样就比较理想了。

完成

该爬虫项目结束了。
总结一下,我们主要用了 requests.get() 完成请求,还伪造了 headers;用 re.findall() 正则解析结果,然后调整了信息的顺序;用 json 格式化保存结果。

其实,这个项目只要稍作修改,我们就可以用来爬取其他很多种电影排行榜,如我们其实还实现了一个爬取豆瓣top250的程序,真的只是稍微改动,非常容易了。

我们展现了这个项目开发的过程,从目标到最后完成,一步一步进行,这个开发顺序适用于很多项目,并且富有哲理,我们认为值得感悟与践行。

Ajax 数据爬取

很多网站都使用有 Ajax,下面再介绍一下如何爬取 Ajax 渲染的网页。

Ajax 简介

Ajax,全称为 Asynchronous JavaScript and XML,即异步的 JavaScript 和 XML。

Ajax 是一种利用 JavaScript 在保证页面不被刷新、页面链接不改变的情况下与服务器交换数据并更新部分网页的技术。

发送 Ajax 请求到网页更新的这个过程可以简单分为三步:

  • 发送请求
  • 解析内容
  • 渲染网页

发送请求

var xmlhttp;
if (window.XMLHttpRequest) {
    // code for IE7+, Firefox, Chrome, Opera, Safari
    xmlhttp=new XMLHttpRequest();
} else {// code for IE6, IE5
    xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function() {
    if (xmlhttp.readyState==4 && xmlhttp.status==200) {
        document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
    }
}
xmlhttp.open("POST","/ajax/",true);
xmlhttp.send();

JavaScript 对 Ajax 最底层的实现,
实际上就是新建了 XMLHttpRequest 对象,
然后调用了onreadystatechange 属性设置了监听,
然后调用 open() 和 send() 方法向某个链接也就是服务器发送了一个请求,
当服务器返回响应时,onreadystatechange 对应的方法便会被触发,
然后在这个方法里面解析响应内容。

解析内容

得到响应之后,onreadystatechange 属性对应的方法便会被触发,此时利用 xmlhttp 的 responseText 属性便可以取到响应的内容。

返回内容可能是 HTML,可能是 Json,接下来只需要在方法中用 JavaScript 进一步处理即可。比如如果是 Json 的话,可以进行解析和转化。

渲染网页

DOM 操作,即对 Document网页文档进行操作,如更改、删除等。

Ajax 分析方法

在浏览器开发者工具的Network中,Ajax的请求类型是 xhr

在Request Headers中有一项 X-Requested-With: XMLHttpRequest 标记了该请求为 Ajax。

在 Preview 中可以看到响应的内容。

有了Request URL、Request Headers、Response Headers、Response Body等内容,就可以模拟发送Ajax请求了。

Python 模拟 Ajax 请求

爬取【人民日报】的微博:

import requests
from bs4 import BeautifulSoup

url_base = 'https://m.weibo.cn/api/container/getIndex?type=uid&value=2803301701&containerid=1076032803301701'

headers = {
        'Accept': 'application/json, text/plain, */*',
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15',
        'X-Requested-With': 'XMLHttpRequest',
        'MWeibo-Pwa': '1'
        }

def get_page(basicUrl, headers, page):
    url = basicUrl + '&page=%s' % page
    try:
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            return response.json()      # Would return a dict
        else:
            raise RuntimeError('Response Status Code != 200')
    except Exception as e:
        print('Get Page False:', e)
        return None


def parse_html(html):
    soup = BeautifulSoup(html, 'lxml')
    return soup.get_text()
    

def get_content(data):
    result = []
    if data and data.get('data').get('cards'):
        for item in data.get('data').get('cards'):
            useful = {}
            useful['source'] = item.get('mblog').get('source')
            useful['text'] = parse_html(item.get('mblog').get('text'))

            result.append(useful)

    return result


def save_data(data):    # 这里不保存了,只是把它打印出来
    for i in data:
        print(i)


if __name__ == '__main__':
    for page in range(1, 3):    # 记得调整需要的页数。
        r = get_page(url_base, headers, page)
        d = get_content(r)
        save_data(d)
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!