自学内容网 自学内容网

AjaX 数据爬取

有时我们用 requests 抓取页面得到的结果,可能和在浏览器上看到的不一样;在浏览器中可以看到正常显示的数据,而使用 requests 得到的结果中并没有这些数据。这是因为 requests 获取的都是原始 HTML 文档, 而浏览器中的页面是 JavaScript 处理数据后生成的结果, 这些数据有多种来源; 可能是通过 Ajax 加载的, 可能是包含在 HTML 文档中的, 也可能是经过 JavaScript 和特定的算法计算后生成的

对于第一种来源, 数据加载是一种异步加载方式, 原始页面最初不会包含某些数据,当原始页面加载完后,会再向服务器请求某个接口获取数据,然后经过处理,才会呈现再网页上,这请示就是发送了一个 Ajax 请求

什么是 Ajax

Ajax 全称: Asynchronous JavaScript and XML , 即异步的 JavaScript  和 XML, 它不是一门编程语言,而是利用 JavaScript 在保证页面不被刷新,页面链接不改变的情况下与服务器交换数据,并更新部分网页内容的技术

对于传统网页,如果想要更新内容,就必须刷新整个页面, 但有了 Ajax ,可以在页面不被全部刷新的情况下更新。这个过程实际是页面在后台与服务器进行了数据交互,获取数据后,在利用 JavaScript 改变网页,这样网页内容就会更新了

可以到 AJAX - XMLHttpRequest (w3school.com.cn) 体验几个实例

基本原理

浏览网页的时候我们会发现很多网页都有“下滑查看更多”的选项。

网页的更行可以简单分为: 1. 发送请求  2. 解析内容 3. 渲染网页

发送请求

var xmlhttp

if (window.XMLHttpRequest) {

        xmlhttp = new XMLHttpRequest();

} else {//conde for IE6 IE5

        xmlhttp = new ActiveXOBject("Micorsoft.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 对象 xmlhttp, 然后调用 onreadystatechange 属性设置监听, 最后调用 open 和 send 方法向某个链接(也就是服务器)发送请求。千米那用 python 实现发送请求之后,可以得到响应结果, 但这里的请求发送由 JavaScript 完成。由于设置了监听, 所以当服务器返回响应时, onreadystatechange 对应的方法便会被触发, 然后在这个方法里面解析响应内容即可

解析内容

服务器返回响应之后, onreadystatechange 属性对应的方法就被触发了, 此时利用 xmlhttp 的 responseText 属性便可得到响应内容。 这类似于 python 中利用 requests 向服务器发起请求, 然后得到响应的过程。返回内容可能是 HTML ,可能是 JSON , 接下来只需在方法中用 JavaScript 进一步处理即可。 如果是 JSON 的化, 可以进行解析和转化

渲染网页

JavaScript 有改变网页内容的能力, 因此解析完响应内容之后, 就可以调用 JavaScript 来基于解析完的内容对网页进行下一步的处理了。 例如, 通过 document.getElementById().innnerHTML 操作, 可以更改某个元素内的源代码, 这样网页显示的内容就改变了。 这种操作也被称为 DOM 操作, 即对网页文档进行操作,如更改,删除等

上面 “发送请求’ 部分,代码里的 document.getElementById("myDiv").innerHTML = xmlhttp.responseText 便是将 ID 为 myDiv 的节点内容的 HTML 代码更改为了服务器返回的内容,这样 myDiv 元素内部便会呈现服务器返回的新数据,对应的网页内容看上去就更新了

我们观察到,网页更新的 3 个步骤其实都由 JavaScript 完成的,它完成了整个请求,解析和渲染的过程

因此我们知道,真实的网页数据其实是一次次向服务器发送 Ajax 请求得到的,要想抓取这些数据,需要知道 Ajax 请求到底是怎么发送的, 发往哪里,发了哪些参数。

Ajax 分析方法

分析案例

这里以 Chrome 浏览器 打开微博 https://m.weibo.cn/u/2830678474 为例

打开网站 ---右键---检查

这里展示的就是页面的加载过程中,浏览器与服务器之间发送请求和接收响应的所有记录

事实上 Ajax 有其特殊的请求类型, 叫作 xhr 。 

上图中有一个以 getIndex 开头的请求, 其 Type 就为 xhr ,意味着这就是一个 Ajax 请求。用鼠标单击这个请求,可以看到其详细信息

从上图中观察这个 Ajax 请求的 Request Headers, URL 和 Response Headers 等信息。其中Request Headers 中有一个信息为 X-Rquested-With: XMLHttpRequest , 这就标记了此请求是 Ajax 请求

随后单击以下 Preview , 就能看到响应内容。

这些内容是 JSON 格式的,这里 Chrome 为我们做了自动解析, 单击右侧箭头可以展开收起相应的内容

另外也可以切换到 Response 选项卡,从中观察真实返回的数据

接下来返回第一个请求,观察它的 Response

这是最原始的链接 返回的结果, 其代码不到50行, 结构也非常简单,只是执行了一些 JavaScript 语句

所以说微博页面呈现给我们的真实数据并不是最原始的页面返回的,而是执行 JavaScript 后再次向后台发送 Ajax 请求, 浏览器拿到服务器返回的数据后进一步渲染得到

过滤请求

利用 Chrome 开发这工具的筛选功能筛选出所有的 Ajax 请求、

在请求上方点击 带有 XHR 的选项,然后不断向下滑动微博页面,就可以看到有更多的 Ajax 请求被刷新出来了,得到这些请求,就可以使用 python 来获取对应的信息了

Ajax 分析与爬虫实战

import requests

url = 'https://spa1.scrape.center/'
html = requests.get(url).text
print(html

<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>Scrape | Movie</title><link href=/css/chunk-700f70e1.1126d090.css rel=prefetch><link href=/css/chunk-d1db5eda.0ff76b36.css rel=prefetch><link href=/js/chunk-700f70e1.0548e2b4.js rel=prefetch><link href=/js/chunk-d1db5eda.b564504d.js rel=prefetch><link href=/css/app.ea9d802a.css rel=preload as=style><link href=/js/app.17b3aaa5.js rel=preload as=script><link href=/js/chunk-vendors.683ca77c.js rel=preload as=script><link href=/css/app.ea9d802a.css rel=stylesheet></head><body><noscript><strong>We're sorry but portal doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.683ca77c.js></script><script src=/js/app.17b3aaa5.js></script></body></html>

可以看到只有一点内容

在HTML 中, 我们只能看到源码引用了一些 JavaScript 和 CSS 文件, 并没有观察到电影数据

遇到这种情况,说明我们整个页面都是 JavaScript 渲染的, 浏览器执行了HTML 中引用的 JavaScript 文件, JavaScript 通过调用一些数据加载和页面渲染的方法,才呈现了主页的效果。这些数据一般都是通过 Ajax 加载的, JavaScript 在后台调用 Ajax 数据接口,得到数据之后,将数据进行解析并渲染出来

爬取列表页

首先分析列表页的 Ajax 接口逻辑, 打开浏览器开发者工具, 切换到 Network 面板,勾选 PreserveLog  并切换到 XHR 选项卡,接着重新刷新页面,再单击第2页 , 第3页,等按钮,这时可以看到不仅页面上的数据发生了变化, 开发这工具下方页监听到了几个 Ajax 请求

这里我们点开了最后一个结果,观察到其 Ajax 接口的请求 URL 为 https://spa1.scrape.center/api/movie/?limit=10&offset=40

这里有两个参数,一个是 limit, 这里是 10 , 一个是 offset 这里是 40 .

观察多个 Ajax 接口参数, 我们可以总结出这么一个规律, limit 一直为 10 ,正好对应每页 10 条数据, offset 在依次变大, 页数每加 1 , offset 就加 10 , 因此其 代表页面数据偏移量。例如第2页的 offset 为  10 就代表跳过了 10 条数据, 返回从 11 条数据开始的内容, 再加上 limit 的限制,最终页面呈现的是第 11 条到第 20 条数据

我们再观察以下响应内容, 切换到 Preview 选项卡

可以看到就是一些 JSON 数据, 其中一个 results 字段,是一个列表, 列表中每一个元素都是一个字典。观察一下字典内容, 里面正好可以看到对应电影数据的字段, 如 name , alias, cover, categories。 对比一下浏览器页面中的真实数据, 会发现各项内容完全一致, 而且这些数据已经非常结构化了, 完全就是我们想要爬取的数据

这样,我们只需要构造出所有页面的 Ajax 接口, 就可以获取到数据了

import requests
import logging

logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s: %(message)s')
INDEX_URL = 'https://spa1.scrape.center/api/movie/?limit={limit}&offset={offset}'

这里我们引入了 requests 和 logging 库, 并定义了 logging 的基本配置, 接着定义了 INDEX_URL , 这里把 limit 和 offset 预留了出来变成占位符, 可以动态传入参数构造一个完整的列表页URL。

下面实现以下详情页的爬取

def scrape_api(url):
    logging.info('scraping %s...', url)
    try:
        response = requests.get(url)
        if response.status_code==200:
            return response.json()
        logging.error('get invalid status code %s while scraping %s', response.status_code, url)
    except requests.RequestException:
        logging.error('error occurred while scraping %s', url, exc_info=True)

这里我们定义了一个 scrape_api 方法, 和之前不同的是, 这个方法专门用来处理 JSON 接口,。最后的 response 调用的是 json ,它可以解析响应内容并将其转化成 JSON 字符串。

接着在这个基础上,定义一个爬取列表的方法

LIMIT = 10
def scrape_index(page):
    url = INDEX_URL.format(limit=LIMIT, offset=LIMIT * (page - 1))
    return scrape_api(url)

这里我们定义了一个 scrape_index 方法, 它接收一个参数 page , 该参数代表列表页的页码。这里  limit 就直接使用了全局变量 LIMIT 的值; offset 则是动态计算的, 计算方法是页码数减一再乘以 limit , 例如第一页的 offset 就是 0 , 第二页的 offset 就是 10 , 依次类推。 构造号 url 后,直接调用 scrape_api 方法并返回结果即可

这样我们就完成了列表页的爬取,每次发送 Ajax 请求都会得到 10 部电影的数据信息

由于这里爬取的数据已经是 JSON 类型了, 所以无需取解析 HTML 代码提取数据,爬到的数据已经是我们想要的结构化数据, 因此解析这一步可以省略

爬取详情页

虽然我们已经可以拿到每一页的电影数据,但是这些数据实际上还缺少一些我们想要的信息,如剧情简介等信息,所以要进一步进入详情页来获取这些内容

单击任意一部电影进入详情页,可以发现此时的页面 URL 已经变成了

可以发现此时的页面 URL 已经变成了 

https://spa1.scrape.center/detail/40

页面页成功展示了 《教父》 的详情信息

另外,我们也可以观察到开发着工具有出现了一个 Ajax 请求, 其 URL 为

通过 Preview 选项卡也能看到 Ajax 请求对应的信息

稍加观察可以看出,Ajax 请求的 URL 后面有一个参数是可变的, 这个参数是电影的 id , 这里是 40 , 对应教父这部电影

如果我们想要获取 id 为 50 的电影,只需要把 URL 最后的参数改成 50 即可, 即

https://spa1.scrape.center/detail/50

请求这个新URL 就可以获取 id 为 50 的电影对应的数据了

现在,详情页的数据提取逻辑分析完了, 怎么和列表页关联起来呢,电影的 id 从哪里来?我们回过头来看看列表页的接口返回的数据

可以看到。列表页原本的返回数据中就带有 id 这个字段, 所以只需要拿到列表页结果中的 id 来构造详情页的 Ajax 请求的 URL 就好了

接着我们定义要给详情页的爬取逻辑

DETAIL_URL = 'https://spa1.scrape.center/api/movie/{id}'
def scrape_detail(id):
    url = DETAIL_URL.format(id=id)
    return scrape_api(url)

这里定义了一个 scrape_detail 方法, 它接收一个参数 id , 这里的实现页非常简单, 先根据定义好的 DETAIL_URL 加 id 构造一个真实的详情页 Ajax 请求的 URL , 再直接调用 scrape_api 方法传入这个 url 即可

最后我们定义一个 总调用方法, 对以上方法串联调用

TOTAL_PAGE = 10 

def main():
    for page in range(1, TOTAL_PAGE + 1):
        index_data = scrape_index(page)
        for item in index_data.get('results'):
            id = item.get('id')
            detail_data = scrape_detail(id)
            logging.info('detail data %s', detail_data)
if __name__== '__main__':
    main()

这里输出的内容有很多,所以如果为了节省时间,可以适当减少输出量

我们定义了一个 main 方法, 该方法首先遍历获取页码 page , 然后把 page 当作参数传递给 scrape_index 方法, 得到列表数据,接着遍历列表页的每个结果, 获取每部电影的 id , 之后把id 当作参数传递给 scrape_detail 方法来爬取每部电影的详情数据, 并将此数据赋值为 detail_data ,最后输出 detail_data即可

保存数据

数据成功提取后,这里我们把数据保存在 mongoDB中

在保存之前,首先确定自己有一个可以正常连接和使用的 MongoDB 数据库, 这里我们以本地 localhost 的 mongoDB 数据库为例进行操作,其运行在 27017 端口上, 无用户名和密码

将数据导入 MongoDB 需要 PyMongo 这个库

MONGO_CONNECTION_STRING = 'mongodb://localhost:27017'
MONGO_DB_NAME = 'movies'
MONGO_COLLECTION_NAME = 'movies'

import pymongo
client = pymongo.MongoClient(MONGO_CONNECTION_STRING)
db = client['movies']
collection = db['movies']

这里我们先声明了几个变量

MONGO_CONNECTION_STRING: MongoDB 的连接字符串, 里面定义的是  MongoDB 的基本连接信息, 这里是 host ,port 还可以定义用户名,密码等内容

MONGO_DB_NAME: MongoDB数据库名称

MONG_COLLECTION_NAME: MongoDB 的集合名称

然后用 MongoClient 声明了一个连接对象 client , 并依次声明了存储数据的数据库和集合

接下来,再实现一个将数据保存到 MongoDB 数据库的方法

def save_data(data):
    collection.update_one({'name': data.get('name')},{'$set': data},upsert=True)

这里我们顶一个 save_data 方法,它接收一个参数 data , 也就是上一节提取的电影详情信息,这个方法里面,我们调用了 updata_one 方法, 其第一个参数是查询条件, 即根据 name 进行查询,第二个参数是 data 对象本身,就是所有的数据,这里我们用 $set 操作符表示更新操作; 第三个参数很关键,这里实际上是 upsert 参数,如果把它设置为 True , 就可以实现存在即更新,不存在即插入的功能, 更新时会参照第一个参数设置的 name 字段, 所以这样可以防止数据库中出现同名电影数据

注意: 实际上电影有可能存在同名现象, 但此处场景下的爬取数据没用同名情况, 当然这里更重要的是实现MongoDB 的去重操作

接下来稍微修改以下 main 方法

TOTAL_PAGE = 10 
def main():
    for page in range(1, TOTAL_PAGE + 1):
        index_data = scrape_index(page)
        for item in index_data.get('results'):
            id = item.get('id')
            detail_data = scrape_detail(id)
            logging.info('detail data %s', detail_data)
            save_data(detail_data)
            logging.info("data saved successfully")
if __name__== '__main__':
    main()

注意点: 1. 记得启动 mongoDB 数据库 2 . 可能会遇到内存不足的情况


原文地址:https://blog.csdn.net/qq_39217312/article/details/140665302

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!