[关闭]
@wade123 2019-03-26T07:40:45.000000Z 字数 5243 阅读 1505

实战项目 4:AJAX 多页图片爬取,以澎湃网美数课信息图为例

Python入门爬虫与数据分析


摘要: 以澎湃「美数课」信息图栏目文章为例,介绍爬取 Ajax 多页图片。

前几篇爬虫文章的网站有一个特点,即翻页时 URL 参数会跟着变化,这种网页在爬取多页时递归参数容易构造,爬取一页就能爬多页,没有太大难度。

还有一类常见的网页在翻页时,URL 不会显示页码变化,网页中也没有「下一页」按钮,取而代之的是「加载更多」或者下拉会自动刷新内容。这类网页通常采用的是 Ajax 技术,提取网页内容和以前的方法不同,需另辟蹊径。下面以澎湃「美数课」栏目信息图为例,爬取该栏目从 2015 年至今全部 437 篇文章图片。

URL:https://www.thepaper.cn/list_25635

本文知识点:

1. Ajax 知识

尝试在上面的网页中不断下拉,会加载出更多新的文章,网址 URL 也保持不变,这种形式的网页很常见,它们普遍采用了Ajax 技术。

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

传统网页如果想更新内容,需要刷新整个页面,使用 Ajax 更新内容只需刷新网页部分内容。这个过程的背后实际上是后台与服务器进行了数据交互,获取到数据之后利用 JavaScript 改变网页,实现网页内容的更新 。

了解 Ajax 技术后,下面就来实战爬取该网页的所有文章图片。

主页右键点击检查,按f5刷新会弹出很多链接文件,回到第一个文件:list_25635ctrl+f搜索第一篇文章的标题:"娃娃机生意经"是否在这个文件中,可以看到顺利找到对应名称,意味着文章的源代码很可能是在这个文件里。

下拉鼠标更新新的文章,第一篇文章标题含有「金砖峰会」,再重复搜索一次看是否还能够在文件中找到源代码,经尝试搜不到对应内容,意味着更新后的文章源代码不在这个文件中了。所以在这个文件中只能爬取前面的信息,后续 Ajax 刷新出来的内容爬取不到。

2. 爬虫实战

Network选项卡下勾选 ·XHR,这样只查看使用了 Ajax 的网页,可以看到只有 4 个结果,经尝试发现第 3 个链接中的Response中存放着网页文章信息,比如第一篇文章标题:娃娃机生意经|有没有好奇过抓娃娃机怎么又重新火起来了?。切换到 headers 选项卡,复制Request URL后面的链接到浏览器中,可以看到显示一部分文章的标题和图片内容。数一下的话,可以发现一共有 20 个文章标题,也就是对应着网页最初的 20 篇文章。


接下来,尝试下拉鼠标,同时关注Name列内容,会发现不断弹出新的链接,这些链接很可能隐藏着 Ajax 加载的文章信息。选中第一个load_index开头的链接,查看Response中的源代码,尝试搜索十年金砖峰这篇文章内容,果然在源代码中能够找到这篇文章。

再次复制Request URL后面的链接到浏览器,可以看到包含了更新的 20 文章信息。

到这儿就能确定了,每个链接存放着网页中的 20 文章信息,只要爬取每个 URL 链接中的图片,就能够下载该栏目下所有文章的图片。

下面,我们来分析一下这些 URL,看看能不能找到规律,如果能够找到规律,就可以简单的够构造递增参数,用 for 循环批量下载每个 URL 中的图片,前 3 个链接如下:

  1. https://www.thepaper.cn/load_index.jsp?nodeids=25635&topCids=&pageidx=2&isList=true&lastTime=1533169319712
  2. https://www.thepaper.cn/load_index.jsp?nodeids=25635&topCids=&pageidx=3&isList=true&lastTime=1528625875167
  3. https://www.thepaper.cn/load_index.jsp?nodeids=25635&topCids=&pageidx=4&isList=true&lastTime=1525499007926

发现pageidx参数值很有规律,以自然数递增,而 lastTime 参数值没有规律,需要测试一下这个参数有没有影响,复制第一个 URL,删掉&lastTime=1533169319712这一串字符,会发现网页一样能够正常打开,就说明着一对参数不影响网页内容,可以删除掉。这样所有 url 的区别就只是 pageidx参数不同了,构造 for 循环就可以生成所有 URL:

  1. https://www.thepaper.cn/load_index.jsp?nodeids=25635&pageidx=2
  2. https://www.thepaper.cn/load_index.jsp?nodeids=25635&pageidx=3
  3. https://www.thepaper.cn/load_index.jsp?nodeids=25635&pageidx=4

同时,需要确定最后一页的数值是多少。很简单,尽可能改一个较大的数值,再打开链接看是否有内容即可。比如改为 **10 **,打开发现有内容显示,再改为 30 就没有内容,尝试几次后确定最后一页是 25 页。

接下来就可以爬取全部页的图片,代码编写思路是先爬取一篇文章中的图片(这个爬取过程,上一篇一篇爬取网易的文章已经介绍过),然后爬取一个 URL 中的 20 篇文章图片,最后循环 25 页爬取所有图片,完整代码如下:

3. 完整代码

  1. import requests
  2. from bs4 import BeautifulSoup
  3. import re
  4. import os
  5. from hashlib import md5
  6. from requests.exceptions import RequestException
  7. from multiprocessing import Pool
  8. from urllib.parse import urlencode
  9. headers = {
  10. 'user-agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36'
  11. }
  12. # 1 获取索引界面网页内容
  13. def get_page_index(i):
  14. # 下载 1 页
  15. # url = 'https://www.thepaper.cn/newsDetail_forward_2370041'
  16. # 2 下载多页,构造 url
  17. paras = {
  18. 'nodeids': 25635,
  19. 'pageidx': i
  20. }
  21. url = 'https://www.thepaper.cn/load_index.jsp?' + urlencode(paras)
  22. response = requests.get(url,headers = headers)
  23. if response.status_code == 200:
  24. return response.text
  25. # print(response.text) # 测试网页内容是否提取成功 ok
  26. # 2 解析索引界面网页内容
  27. def parse_page_index(html):
  28. soup = BeautifulSoup(html,'lxml')
  29. # 获取每页文章数
  30. num = soup.find_all(name = 'div',class_='news_li')
  31. for i in range(len(num)):
  32. yield{
  33. # 获取 title
  34. 'title':soup.select('h2 a')[i].get_text(),
  35. # 获取图片 url,需加前缀
  36. 'url':'https://www.thepaper.cn/' + soup.select('h2 a')[i].attrs['href']
  37. # print(url) # 测试图片链接
  38. }
  39. # 3 获取每条文章的详情页内容
  40. def get_page_detail(item):
  41. url = item.get('url')
  42. # 增加异常捕获语句
  43. try:
  44. response = requests.get(url,headers = headers)
  45. if response.status_code == 200:
  46. return response.text
  47. # print(response.text) # 测试网页内容是否提取成功
  48. except RequestException:
  49. print('网页请求失败')
  50. return None
  51. # 4 解析每条文章的详情页内容
  52. def parse_page_detail(html):
  53. soup = BeautifulSoup(html,'lxml')
  54. # 获取 title
  55. if soup.h1: #有的网页没有 h1 节点,因此必须要增加判断,否则会报错
  56. title = soup.h1.string
  57. # 每个网页只能拥有一个<H1>标签,因此唯一
  58. items = soup.find_all(name='img',width =['100%','600'])
  59. # 有的图片节点用 width='100%'表示,有的用 600 表示,因此用 list 合并选择
  60. # https://blog.csdn.net/w_xuechun/article/details/76093950
  61. # print(items) # 测试返回的 img 节点 ok
  62. for i in range(len(items)):
  63. pic = items[i].attrs['src']
  64. # print(pic) #测试图片链接 ok
  65. yield{
  66. 'title':title,
  67. 'pic':pic,
  68. 'num':i # 图片添加编号顺序
  69. }
  70. # 5 下载图片
  71. def save_pic(pic):
  72. title = pic.get('title')
  73. # 标题规范命名:去掉符号非法字符| 等
  74. title = re.sub('[\/:*?"<>|]','-',title).strip()
  75. url = pic.get('pic')
  76. # 设置图片编号顺序
  77. num = pic.get('num')
  78. if not os.path.exists(title):
  79. os.mkdir(title)
  80. # 获取图片 url 网页信息
  81. response = requests.get(url,headers = headers)
  82. try:
  83. # 建立图片存放地址
  84. if response.status_code == 200:
  85. file_path = '{0}\{1}.{2}' .format(title,num,'jpg')
  86. # 文件名采用编号方便按顺序查看,而未采用哈希值 md5(response.content).hexdigest()
  87. if not os.path.exists(file_path):
  88. # 开始下载图片
  89. with open(file_path,'wb') as f:
  90. f.write(response.content)
  91. print('文章"{0}"的第{1}张图片下载完成' .format(title,num))
  92. else:
  93. print('该图片%s 已下载' %title)
  94. except RequestException as e:
  95. print(e,'图片获取失败')
  96. return None
  97. def main(i):
  98. # get_page_index(i) # 测试索引界面网页内容是否获取成功 ok
  99. html = get_page_index(i)
  100. data = parse_page_index(html) # 测试索引界面 url 是否获取成功 ok
  101. for item in data:
  102. # print(item) #测试返回的 dict
  103. html = get_page_detail(item)
  104. data = parse_page_detail(html)
  105. for pic in data:
  106. save_pic(pic)
  107. # 单进程
  108. if __name__ == '__main__':
  109. for i in range(1,26):
  110. main(i)
  111. # 多进程
  112. if __name__ == '__main__':
  113. pool = Pool()
  114. pool.map(main,[i for i in range(1,26)])
  115. pool.close()
  116. pool.join()

运行过程如下:

最后,所有图片都按照文件夹下载好了。

文章代码素材,可在下方链接中得到:

https://github.com/makcyun/web_scraping_with_python/tree/master/Python%E7%88%AC%E8%99%AB(4)%EF%BC%9A%E5%9B%BE%E7%89%87%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BD-%E4%BB%A5%E6%BE%8E%E6%B9%83%E7%BD%91%E4%BF%A1%E6%81%AF%E5%9B%BE%E4%B8%BA%E4%BE%8B

到这里,我们用了两篇文章练习如何批量爬取图片,希望你能够掌握。下一篇文章,我们使用 Selenium 自动爬虫。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注