python爬虫学习路径(后面那个有点难,烦请大佬进行技术指点)

python爬虫

基本概念:编写一段程序,我们把相关数据爬取下来的含义

怎么使用呢?我们需要网络编程的基础,request实际上也是一个爬虫,理论基础在网络编程,可以见我的demo

但是爬虫虽然方便,但是有一些网站为了安全会设置反爬虫机制。具体怎么操作,后面我们慢慢说。

1. 网页的介绍

这里是属于前端的知识,可以去看一下p3前端的内容,才进行了入门

1.1 Chorme调试工具

网页源代码爬取:在页面右键单击,在菜单里面找到显示网页源代码。前端的内容前端看叭。

1.2 Elements

调试工具:chorme检查:右键单击,点击检查,要换至element面板,里面全是html代码,可以看到里面的内容。

1.3 DOM

表示节点,前端知识。

2. 静态网页爬虫基础

我们以王者荣耀封面为例

2.1 分析

我们大致可以把爬虫逻辑分为四部分:

1.人工查找承载信息的网站

2.分析网页问题:静态 or 动态

静态网页:服务端渲染出HTML返回给客户端

动态网页:客户端(浏览器)通过JavaScript渲染出HTML

3.使用网页解析器(正则表达式)进行分析

4.提取需要信息。

静态网页:发现网页里面内容可以在源代码里面找到的东西。

首先开一个源码:

<li rel="44587" rel2="1" style="display:none">
  <a href="http://news.4399.com/gonglue/wzlm/yingxiong/ck/m/557118.html"
    ><img
      lz_src="http://newsimg.5054399.com/uploads/userup/1705/2615530535Z.jpg"
      alt="王者荣耀红拂"
    /><span>红拂</span></a
  >
  <div class="cf">
    <a href="#" class="yxtxt-no" οnclick="sm();return false;">英雄解析</a
    ><a href="#" class="yxvideo-no" οnclick="sm();return false;">英雄视频</a>
  </div>
</li>

很多英雄是包裹在同一个网页里面的

我们需要详细分析

他们包裹在同一个ul标签里面,里面全是英雄节点

<ul class="rolelist cf" id="hreo_list">
  <li>...鲁班大师...</li>
  ...
  <li>...红拂...</li>
</ul>

2.2 静态网页爬虫实现

思路:1.用requests库获取源码

import requests
response = requests.get('http://news.4399.com/gonglue/wzlm/yingxiong/')
print(response.text)

我们发现里面有乱码,我们需要在网页编码方式里面找到编码方式:head里面找到第一行,前端里面有charset这个玩意,就是编码方式,复制过来。代码如下:

import requests
response = requests.get('http://news.4399.com/gonglue/wzlm/yingxiong/')
response.encoding = 'gb2312'
print(response.text)

2.获取相应区域的代码:

我们在提取相应代码之前要采用正则表达式:

先选出相应区域的代码,匹配正则表达式:

r'<ul\sclass="rolelist\scf"\sid="hreo_list">([\s\S]*?)<\/ul>'`

为了控制不冗余,用非贪婪匹配即可

代码如下:

import requests
import re
response = requests.get('http://news.4399.com/gonglue/wzlm/yingxiong/')
obj = re.search(r'<ul\sclass="rolelist\scf"\sid="hreo_list">([\s\S]*?)<\/ul>', response.text)
print(obj.group(1)) # 使用group(1)提取第一个组内容

后面我们要分割内容,因为太烦了

我们可以采用split()函数进行分割。在里面输入条件就可以分割了。

我们现在来分割一下:

import requests
import re
response = requests.get('http://news.4399.com/gonglue/wzlm/yingxiong/')
response.encoding = 'gb2312'
obj = re.search(r'<ul\sclass="rolelist\scf"\sid="hreo_list">([\s\S]*?)<\/ul>', response.text)
heros = obj.group(1).split('</li>') # 根据</li>分割
print(len(heros) - 1) # 打印英雄个数,最后一个</li>分割后会出现一个空,所以减1

因为最后一个元素分割无效,去除采用pop()函数。

import requests
import re
response = requests.get('http://news.4399.com/gonglue/wzlm/yingxiong/')
response.encoding = 'gb2312'
obj = re.search(r'<ul\sclass="rolelist\scf"\sid="hreo_list">([\s\S]*?)<\/ul>',
                response.text)
heros = obj.group(1).split('</li>')  # 根据</li>分割
heros.pop()
for hero in heros:
  banner = re.search(r'\slz_src="(.*?)"\s', hero)
  name = re.search(r'<span>(.*?)</span>', hero)
  # 我们不需要先存储results了,在这里直接下载即可
  imgResult = requests.get(banner.group(1))
  with open('imgs/{}.jpg'.format(name.group(1)), 'wb') as file:
    print(name.group(1) + ' 下载完成!')
    file.write(imgResult.content)

代码解释

我们还是一如既往的获取图片内容。

'imgs/{}.jpg'.format(name.group(1))

是一个字符串模板方法,用name.group(1)把内容填入大括号里面。

3 Beautiful Soup问题

由于正则表达式难度较大,其解析器对正则表达式有较高的要求。

所以我们在这里引入Beautiful Soup来解决问题

这是一个可以从HTML文档或者xml文件里面提取数据的python库,功能可以通过你喜欢的转换器实现文档导航,查找与修改

我们还需要安装环境,具体安装线路见链接:https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/#id8

我们有安装办法:

pip install beautifulsoup4

使用方法如下:

rom bs4 import BeautifulSoup # 1. 引入BeautifulSoup解析库

html = """
<html>
  <head>
    <title>优课达</title>
  </head>
  <body>
    <a href="https://www.youkeda.com" alt="学得比别人好一点">优课达</a>
    <ul>
      <li><a href="https://www.youkeda.com">问吧</a></li>
      <li><a href="https://www.youkeda.com/academy/java">研发学院</a></li>
      <li><a href="https://www.youkeda.com/academy/python/P2">Python学院</a></li>
      <li><a href="https://www.youkeda.com/app">APP下载</a></li>
    </ul>
  </body>
</html>
"""

# 2. 用html.parser的规则解析html
result = BeautifulSoup(html, 'html.parser')

# 3. 打印网页的title
print(result.title)

分部执行方法:1. 引用公共库

​ 2.使用其解析器进行解析

​ 3.再打印你所需要的信息,解析出来的结果.内容

获取里面内容,不想要标签怎么办?

用get_text()方法进行操作。

这个方法获取到tag中包含的所有文版内容包括子孙tag中的内容,并将结果作为Unicode字符串返回:

markup = '<a href="http://example.com/">\nI linked to <i>example.com</i>\n</a>'
soup = BeautifulSoup(markup)

soup.get_text()
u'\nI linked to example.com\n'
soup.i.get_text()
u'example.com'

看了文档发现会很烦,我们具体实例看看:

result = BeautifulSoup(html, 'html.parser')
print(result.title.get_text())

这样直接加这个方法就好了

如果你想获得第一个标签,就soup.body.a或soup.a

要想获取多个,就要soup.find_all(标签名)

如果要解析HTML里面的属性,就要用标签名加中括号[属性名]方法,操作方法与字典一模一样

3.1 find_all方法

我们重点是查询、筛选、获取信息

本节方法主要是查询所有节点

3.1.1 案例1:查询所有a标签
from bs4 import BeautifulSoup

html = """
<html>
  <head>
    <title>优课达</title>
  </head>
  <body>
    <a href="https://www.youkeda.com" alt="学得比别人好一点">优课达</a>
    <ul>
      <li><a href="https://www.youkeda.com">问吧</a></li>
      <li class="info"><a href="https://www.youkeda.com/academy/java">研发学院</a></li>
      <li class="info"><a href="https://www.youkeda.com/academy/python/P2">Python学院</a></li>
      <li class="info last"><a class="download" href="https://www.youkeda.com/app">APP下载</a></li>
    </ul>
  </body>
</html>
"""
result = BeautifulSoup(html, 'html.parser')

# 查询所有的a标签
aList = result.find_all('a')
print(aList)

这个会帮助我们查找所有的a标签,帮助我们查找,最终返回的是一个列表

3.1.2 查询为info的li标签
# 省略前面相同的代码
……

# 查询所有class为info的li标签
liList = result.find_all('li', 'info')
print(liList)

前一个参数是标签名,后一个参数是属性。

3.1.3 查询href为xxx的标签
# 省略前面相同的代码
……

# 查询所有的href为https://www.youkeda.com的a标签
aList = result.find_all('a', href='https://www.youkeda.com')
print(aList)

参数1是标签名,参数二是链接

3.1.4 嵌套查询

查询所有的 class 为 “info” 的 li 标签,并且查询 li 标签内部 class 为 download 的 a 元素的文字内容

首先我们先查询class为info的li标签

liList = result.find_all('li', 'info')

如何进行第二步呢?

再一次在筛选的结果里面再一次遍历

liList = result.find_all('li', 'info')
# 查询到的结果是一个列表,可以遍历
for li in liList:
  # 每个元素可以继续在此基础上进行查询
  temp = li.find_all('a', 'download')

  # 并不一定会有结果,需要判断查询到的个数是否为空
  if len(temp) > 0:
    print(temp[0].get_text())

由于查询结果是一个列表,所以可以遍历

3.2 select()

这个方法是通过css选择器来进行的,这个是前端内容,前端可以看笔记哦。当然我们也不需要精通前端,我们可以看开发者选项

怎么查看?

我们可以把相关部分写在html文件里面,然后用chorme浏览器打开,Copy > Copy Selector,会得到

body > ul > li.info.last > a

这个关系表示继承关系,.表示class名称

li.info.last意思是取class为info和last的li元素

要想更深一步了解,可以参考:https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Selectors

使用方法:

# 省略前面代码
……
# 根据selectors筛选
aApps = result.select('body > ul > li.info.last > a.download')
for item in aApps:
  print(item.get_text())

你会发现代码更加简洁相对于嵌套查询,select返回的是列表,可以遍历

3.3 综合案例

我们在上一节用王者荣耀案例进行分析了,现在我需要进行优化,代码如下:

import requests
from bs4 import BeautifulSoup
response = requests.get('http://news.4399.com/gonglue/wzlm/yingxiong/')
response.encoding = 'gb2312'

result = BeautifulSoup(response.text, 'html.parser')

# 第一步筛选英雄列表下面的li
heroliList = result.select('ul.rolelist.cf > li')
for heroli in heroliList:

  # 查找唯一一个img 和 span
  img = heroli.img
  name = heroli.span

  print('{}-{}'.format(name.get_text(), img['lz_src']);

首先我们这样可以不怎么用正则表达式进行了

补充前端知识:通用选择器:*+内容,相当于选择所有元素

​ 类型选择器:直接类名即可,input匹配的任何input元素

​ 类选择器:.类名,class属性匹元素

​ ID选择器:#idname,id属性匹配元素

​ 属性选择器[属性名字],选择所有该属性所有元素

4 豆瓣综合案例

首先我们看网址:

第一页:https://book.douban.com/top250?start=0
第二页:https://book.douban.com/top250?start=25
第三页:https://book.douban.com/top250?start=50

他们前面一模一样,后面start不一样,代表

第一页的数据是0开始到24结束
第二页的数据是25开始到49结束
第三页的数据是50开始到75结束

怎么获取呢?

我们还是一如既往的查找

import requests
from bs4 import BeautifulSoup

response = requests.get('https://book.douban.com/top250?start=0')

# 同样我们可以根据meta charset获取到页面的编码方式
response.encoding = 'utf-8'

print(response)
print(response.text)

你会发现打不开,是因为这个网页有防爬功能,为了可以正常爬取,我们要进行user_agent设置

import requests
from bs4 import BeautifulSoup

user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'
response = requests.get('https://book.douban.com/top250?start=0', headers={'User-Agent': user_agent})

# 同样我们可以根据 meta charset 获取到页面的编码方式
response.encoding = 'utf-8'

print(response)
print(response.text)

这样就正常了

user-agent找法:在开发者工具里面network里面点击查找

获得第几个table:

'#content > div > div.article > div > table:nth-child(2)'

这里是获取第二个table

要想获得全部table,就要:

'#content > div > div.article > div > table'

我们综合打一下:

import requests
from bs4 import BeautifulSoup

user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'
response = requests.get(
    'https://book.douban.com/top250?start=0',
    headers={'User-Agent': user_agent})

# 同样我们可以根据 meta charset 获取到页面的编码方式
response.encoding = 'utf-8'

result = BeautifulSoup(response.text, 'html.parser')
books = result.select('#content > div > div.article > div > table')
print(len(books))

若想获取图书名称:

就要在开发者选项里面看到他的对应状态

'#content > div > div.article > div > table:nth-child(2) > tbody > tr > td:nth-child(2) > div.pl2 > a'

当然,我们也可以采用相对路径

'tr > td:nth-child(2) > div.pl2 > a'

相应代码如下:

#...忽略上面重复的代码

result = BeautifulSoup(response.text, 'html.parser')
books = result.select('#content > div > div.article > div > table')
for book in books:
  nameDoms = book.select('tr > td:nth-child(2) > div.pl2 > a')
  name = ''
  if len(nameDoms) > 0:
    name = nameDoms[0].get_text()

  rawNameDoms = book.select('tr > td:nth-child(2) > div.pl2 > span')
  rawName = ''
  if len(rawNameDoms) > 0:
    rawName = rawNameDoms[0].get_text()

  authorDoms = book.select('tr > td:nth-child(2) > p.pl')
  author = ''
  if len(authorDoms) > 0:
    author = authorDoms[0].get_text()

  print('{} {} {}'.format(name, rawName, author))

我们发现有很多空白,处理办法:

import re

def removeWhite(content):
  content = re.sub(r'\s', '', content)
  return content

结果如下:

import requests
from bs4 import BeautifulSoup
import re

def removeWhite(content):
  content = re.sub(r'\s', '', content)
  return content
  
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'
response = requests.get(
    'https://book.douban.com/top250?start=0',
    headers={'User-Agent': user_agent})

# 同样我们可以根据 meta charset 获取到页面的编码方式
response.encoding = 'utf-8'

result = BeautifulSoup(response.text, 'html.parser')
books = result.select('#content > div > div.article > div > table')
for book in books:
  nameDoms = book.select('tr > td:nth-child(2) > div.pl2 > a')
  name = ''
  if len(nameDoms) > 0:
    name = nameDoms[0].get_text()

  rawNameDoms = book.select('tr > td:nth-child(2) > div.pl2 > span')
  rawName = ''
  if len(rawNameDoms) > 0:
    rawName = rawNameDoms[0].get_text()

  authorDoms = book.select('tr > td:nth-child(2) > p.pl')
  author = ''
  if len(authorDoms) > 0:
    author = authorDoms[0].get_text()

  print('{} {} {}'.format(removeWhite(name), removeWhite(rawName), removeWhite(author)))

我们进一步优化

authorDoms = book.select('tr > td:nth-child(2) > p.pl')
  author = ''
  if len(authorDoms) > 0:
    author = authorDoms[0].get_text()
    # 下面是处理方法
    author = author.split('/')[0]

但是你发现,我们要获取多个信息,就要进行循环遍历,可见我们循环遍历充满困难。所以,我们把爬取过程抽象一个方法:

import requests
from bs4 import BeautifulSoup
import re


def replaceContent(content):
  content = re.sub(r'\s', '', content)
  return content


# 处理一个startSpider方法,并且接收一个url作为网页地址
def startSpider(url):
  user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'
  response = requests.get(url, headers={'User-Agent': user_agent})

  # 同样我们可以根据 meta charset 获取到页面的编码方式
  response.encoding = 'utf-8'

  result = BeautifulSoup(response.text, 'html.parser')
  books = result.select('#content > div > div.article > div > table')
  for book in books:
    nameDoms = book.select('tr > td:nth-child(2) > div.pl2 > a')
    name = ''
    if len(nameDoms) > 0:
      name = nameDoms[0].get_text()

    rawNameDoms = book.select('tr > td:nth-child(2) > div.pl2 > span')
    rawName = ''
    if len(rawNameDoms) > 0:
      rawName = rawNameDoms[0].get_text()

    authorDoms = book.select('tr > td:nth-child(2) > p.pl')
    author = ''
    if len(authorDoms) > 0:
      author = authorDoms[0].get_text()
      author = author.split('/')[0]

    print('{} {} {}'.format(
        replaceContent(name), replaceContent(rawName), replaceContent(author)))

# 通过函数调用的方式启动爬虫
startSpider('https://book.douban.com/top250?start=0')

再加上for循环:

for i in range(10):
  startSpider('https://book.douban.com/top250?start={}'.format(i * 25))

总结:我们爬取的过程如下:

先确定要爬取的部分

找到对应代码区域

我们要抽离函数进行解析

解析后进行遍历

解析步骤:先找到网页进行分析请求,要找到user-agent和网址

​ 再使用beautiful soup进行解析

​ 然后再使用相关查找方法进行提取,找到其信息点

​ 最后进行格式处理就可以了

import requests
from bs4 import BeautifulSoup
import re

def getBill(film):
  billDoms = film.select('div.pic > a > img')
  if len(billDoms) > 0:
    return billDoms[0].attrs['src']
  return ''

def getNames(film):
  nameBoxDom = film.select('div.info > div.hd > a')[0]

  # 查询到所有的名称DOM
  nameDoms = nameBoxDom.find_all("span")
  names = []
  for nameDom in nameDoms:
    # 将奇怪字符串替换掉
    name = re.sub('\xa0', '', nameDom.get_text())

    # 得到的名称可能本身还包含多个名称,使用 '/' 分割
    if name.find('/') != -1:
      tempNames = name.split('/')
      for tempName in tempNames:

        # 去除左右的空格
        tempName = tempName.strip()
        if tempName != '':
          names.append(tempName)
    else:
      names.append(name)
  return names


# 获取导演,年份,类别,地区信息
def getOtherInfo(film):
  infoDom = film.select('div.info > div.bd > p:nth-child(1)')[0]
  info = infoDom.get_text().strip()
  temps = info.split('\n')

  # temp[0] 是导演和主演信息,先找到主演的位置,然后用字符串分割出导演内容
  endIndex = temps[0].find('主演:')
  director = temps[0][3:endIndex].strip()

  # temp[1] 包含的年份,类别,地区信息
  others = temps[1].split('/')
  year = others[0].strip()

  # areas types 为数组形式,需要用空格分割原始字符串
  areas = others[1].strip().split(' ')
  types = others[2].strip().split(' ')

  return (director, year, areas, types)


# 获取评分
def getRate(film):
  return film.select('div.info > div.bd > div > span.rating_num')[0].get_text()


resultFilms = []


def startSpider(url):
  user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'
  response = requests.get(url, headers={'User-Agent': user_agent})

  # 同样我们可以根据 meta charset 获取到页面的编码方式
  response.encoding = 'utf-8'

  result = BeautifulSoup(response.text, 'html.parser')
  films = result.select('#content > div > div.article > ol > li')
  for film in films:
    bill = getBill(film)
    names = getNames(film)
    (director, year, areas, types) = getOtherInfo(film)
    rate = getRate(film)
    resultFilms.append({
        'names': names,
        'bill': bill,
        'director': director,
        'year': year,
        'areas': areas,
        'types': types,
        'rate': rate
    })


for i in range(10):
  startSpider('https://movie.douban.com/top250?start={}'.format(i * 25))

print(resultFilms)
    

补充知识:字符串替换:

content = 'abc'
content = re.sub('a', '', content)
print(content) # 结果为 'bc',也就是把'a'这个字符串替换为空

左右空格去除:

content = ' The Shawshank Redemption '
print(content.strip()) # 结果将会是 'The Shawshank Redemption'

strip()方法

字符串包含:find()函数

content1 = '月黑高飞(港)  /  刺激1995(台)'
index1 = content1.find('/')
print(index1)  # 结果为9, 表示'/'在字符串中的下标位置

content2 = '肖生克的救赎'
index2 = content2.find('/')
print(index2) # 结果为 -1, 表示未找到

-1表示未找到

5 动态网页

数据有浏览器渲染出来,由JavaScript发起请求,通过浏览器渲染出来,在chorme的network里面渲染出来

5.1 寻找network

我们在preview里面可以查看返回数据,可以得到请求地址,Header》general》request url

5.2 API

我们找到api以后,发现里面东西很长,很烦人。

https://www.zhihu.com/api/v4/questions/341554416/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cis_labeled%2Cis_recognized%2Cpaid_info%2Cpaid_info_content%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%2A%5D.topics&limit=5&offset=5&platform=desktop&sort_by=default

协议一大堆,我们慢慢看

首先我们先看看接口地址:

https://www.zhihu.com/api/v4/questions/341554416/answers

参数如下:

include后面一大串,我们不看

limit(这个打五星):5 表示每页5个

offset(这个打五星):5 偏移量

plate:desktop 平台

sort_by: default 排序规则

offset是从第几条开始的数据

import requests
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'


def start(offset=0, limit=20):
  result = requests.get(
      url='https://www.zhihu.com/api/v4/questions/341554416/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cis_labeled%2Cis_recognized%2Cpaid_info%2Cpaid_info_content%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%2A%5D.topics&limit={}&offset={}&platform=desktop&sort_by=default'
      .format(20, offset),
      headers={'User-Agent': user_agent})
  data = result.json()['data']
  for answer in data:
    text = answer['content']
    author = answer['author']['name']
    print(author)

start()

20可以换成offset

解析其中元素:

import requests
from bs4 import BeautifulSoup
import re

user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'

def getTotal(id):
  result = requests.get(url='https://www.zhihu.com/question/{}'.format(id), headers={'User-Agent': user_agent})
  soup = BeautifulSoup(result.text, "html.parser")
  totalDom = soup.find(class_='List-headerText')
  totalText = totalDom.select('span')[0].text
  # 解析获取这个数字,并转换成int类型
  total = int(re.split('\s', totalText)[0].replace(',', ''))
  return total

def start(id, offset = 0, limit = 20):
  print('--------------' + str(offset))
  result = requests.get(url='https://www.zhihu.com/api/v4/questions/{}/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cis_labeled%2Cis_recognized%2Cpaid_info%2Cpaid_info_content%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%2A%5D.topics&limit={}&offset={}&platform=desktop&sort_by=default'.format(id, 20, offset), headers={'User-Agent': user_agent})
  data = result.json()['data']
  for answer in data:
    # 通过字典获取到需要的字段
    text = answer['content']
    author = answer['author']['name']
    print(author)

id = '341554416'
total = getTotal(id)
for offset in range(0, total, 20):
  start(id, offset, 20)

原理:在api里面找到相应字段进行爬取即可

post接口参数:在request playload里面找

上面这段代码你们肯定看的头大

所以我拿一个例子来解释

import requests
from bs4 import BeautifulSoup
import re

user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'
header = {'User-Agent':user_agent}
url = 'https://movie.douban.com/j/search_subjects?type=movie&tag=%E7%83%AD%E9%97%A8&page_limit=50&page_start=0'
movies = []

def start():
  result = requests.get(url,headers=header)
  subjects = result.json()['subjects']
  for subject in subjects:
    movies.append({
      'bill':subject['url'],
      'name':subject['title'],
      'rate':subject['rate']
    })
    
start()
print(movies)

json里面可以直接get到数据,注意查看preview里面所要的路径来get相关的数据,我们可以减少beautiful soup的调用来管理

我们再看一个复杂的案例:

import requests

user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'
header = {'User-Agent':user_agent,'X-Agent':'Juejin/Web'}
articles = []
url = 'https://web-api.juejin.im/query'

def start(after):
  data = {
      "operationName": "",
      "query": "",
      "variables": {
          "first": 100,
          "after": after,
          "order": "POPULAR"
      },
      "extensions": {
          "query": {
              "id": "21207e9ddb1de777adeaca7a2fb38030"
          }
      }
  }
  print('-------------' + after)
  result = requests.post(
      "https://web-api.juejin.im/query",
      json=data,
      headers={
          'User-Agent': user_agent,
          'X-Agent': 'Juejin/Web'
      })
  edges = result.json()['data']['articleFeed']['items']['edges']
  pageInfo = result.json()['data']['articleFeed']['items']['pageInfo']

  for edge in edges:
    articles.append(edge['node']['title'])
  return pageInfo['endCursor']
  
  after = ""
  for i in range(1,6):
    after=start(after)
  print(articles)

掘金爬取,post,我们需要json来传data,在request playload里面

header请求:我们爬取过程里面

会遇到爬不到的问题,会有拦截,找到相应agent进行解决

再同上找到preview来找相应数据

6 pyppeteer

新爬虫神器,是一个自动化测试工具,来源于puppeteer

puppeteer:是谷歌chorme 团队官方的无界面工具。,是一个node库

可以完全模拟人操作浏览器行为,我们还是要安装相关

python3 install pyppeteer

我们看看案例:

6.1 网页截图案例:

from pyppeteer import launch
import asyncio

async def spider():
  # 1. 启动一个浏览器
  browser = await launch({'args': ['--no-sandbox', '--disable-setuid-sandbox'})
  # 2. 新开浏览器的一个网页标签页
  page = await browser.newPage()
  # 3. 在新开的网页标签页中输入掘金地址
  await page.goto('https://www.juejin.im')
  # 4. 输入地址以后,等待2秒钟,等待页面渲染完成
  await page.waitFor(2000)
  # 5. 截图,截图地址为juejin.png
  await page.screenshot({'path': 'juejin.png'})
  # 6. 关闭浏览器
  await browser.close()


asyncio.get_event_loop().run_until_complete(spider())

async/await:python协程,解决异步回调问题

# 此处的 await 意思是阻塞在此处,直到launch成功再继续往下面执行
browser = await launch({'args': ['--no-sandbox', '--disable-setuid-sandbox'})

# 此处的 await 意思是阻塞在此处,直到浏览器打开新标签再继续
page = await browser.newPage()

await里面都有耗时操作,目的是等待这个操作完成,继续往下执行。

所有带await里面的东西,必须放在async上面进行

要调用这个方法,要调用asyncio库:

asyncio.get_event_loop().run_until_complete(spider())

其他方法:goto()到访问某个网页

​ waitFor(miliseconds)等待多少毫秒

​ screenshot({‘path’:xxx})截图保存xxx

6.2 生成PDF

把后面screenshot变成

await page.pdf({'path': 'juejin.pdf'})

环境安装:https://blog.csdn.net/weixin_40844416/article/details/80889165

6.3 获取页面信息

方法:querySelector(selector:str)用路径访问方法进行查询第一个满足的元素

querySelectorAll()查询所有满足selector元素

案例:

from pyppeteer import launch
import asyncio

async def spider():
  # 1. 启动一个浏览器
  browser = await launch({'args': ['--no-sandbox', '--disable-setuid-sandbox']})
  # 2. 新开浏览器的一个网页标签页
  page = await browser.newPage()
  # 3. 在新开的网页标签页中输入掘金地址
  await page.goto('https://www.juejin.im')

  # 4. await page.waitFor(2000) 用页面中某个元素显示作为判断条件
  await page.waitForSelector('ul.entry-list')
  # 5. 查询到某个DOM节点
  result = await page.querySelector(
      '#juejin main div.feed.welcome__feed li div.info-row.title-row > a')
  # 6. 获取这个节点的内容对象
  domText = await result.getProperty('innerText')
  # 7. 内容需要通过jsonValue()提取出来
  title = await domText.jsonValue()
  print(title)
  await browser.close()


asyncio.get_event_loop().run_until_complete(spider())

其中代码waitForSelector()我们要渲染页面会有不完全,乱码,所以这个方法就可以渲染那个节点东西

getProperty()获取节点里面对象

最后用jsonValue()是返回那个里面节点值

返回所有元素:

from pyppeteer import launch
import asyncio

async def spider():
  # 1. 启动一个浏览器
  browser = await launch({'args': ['--no-sandbox', '--disable-setuid-sandbox']})
  # 2. 新开浏览器的一个网页标签页
  page = await browser.newPage()
  # 3. 在新开的网页标签页中输入掘金地址
  await page.goto('https://www.juejin.im')

  # 4. await page.waitFor(2000) 用页面中某个元素显示作为判断条件
  await page.waitForSelector('ul.entry-list')
  # 5. 查询到某个DOM节点
  result = await page.querySelectorAll(
      '#juejin main div.feed.welcome__feed li div.info-row.title-row > a')
  for item in result:
    # 6. 获取这个节点的内容对象
    domText = await item.getProperty('innerText')
    # 7. 内容需要通过jsonValue()提取出来
    title = await domText.jsonValue()
    print(title)
  await browser.close()


asyncio.get_event_loop().run_until_complete(spider())

JS提取方法:

result = await page.evaluate(
  """
  () => {
    // 通过JS自带的查询
    const doms = document.querySelectorAll("#juejin main div.feed.welcome__feed li div.info-row.title-row > a")
    const result = [];
    // 遍历doms
    for(let i = 0; i < doms.length; i++){
      result.push(doms[i].innerText)
    }
    return result;
  }
  """
)

使用JavaScript方法进行遍历

6.4 更多方法

async def spider():
  # 1. 启动一个浏览器
  browser = await launch({'args': ['--no-sandbox', '--disable-setuid-sandbox']})
  # 2. 新开浏览器的一个网页标签页
  page = await browser.newPage()
  # 3. 在新开的网页标签页中输入掘金地址
  await page.goto('https://www.juejin.im')

  # 4. 点击登录按钮
  await page.click('span.login')
  # 5. 等待中间登录弹框出现
  await page.waitForSeletor('.auth-modal-box')
  # 6. 让loginPhoneOrEmail输入框获取焦点,此处使用的Selector的属性选择器
  await page.focus('input[name="loginPhoneOrEmail"]')
  # 7. 输入账号
  await page.keyboard.type('XXX', {'delay': 100})
  # 8. 让loginPassword获取焦点
  await page.focus('input[name="loginPassword"]')
  # 9. 输入密码
  await page.keyboard.type('XXX', {'delay': 100})
  # 10. 点击登录按钮
  await page.click('.auth-form .btn')
  await browser.close()

await page.click(‘span.login’) 表示点击

支持属性选择:await page.focus(‘input[name=“loginPassword”]’)

await page.keyboard.type(‘XXX’, {‘delay’: 100})再每次输入后延迟100ms

动态显示:

browser = await launch({
    'headless': False,
    'slowMo': 10, # 为了让过程更加明显,我们配置slowMo让每一步操作延迟10ms
    'args': ['--no-sandbox', '--disable-setuid-sandbox']
  })

6.5 滚动分页

调用js代码async可以这么做:

async () => {
  await new Promise((resolve, reject) => {
    // 1. 定义最大滚动高度,如果是0则表示无线滚动
    let maxHeight = 50000;

    // 2. 记录上一次scrollHeight,便于判断此次下拉操作有没有成功,从而提前结束下拉
    let scrollHeight = 0;
    // 3. maxTries : 有时候无法下拉可能是网速的原因
    let maxTries = 20;
    let tried = 0;

    // 4. 每隔100ms调用一次滚动,滚动到最底部
    var timer = setInterval(function() {
      // 5. 滚动以后高度没变化,无法再滚动,并且重试的次数已经超过maxTries
      if (document.body.scrollHeight === scrollHeight) {
        tried += 1;
        if (tried >= maxTries) {
          console.log('已经达到底部!');
          clearInterval(timer);
          resolve();
        } else {
          console.log('重试:' + tried);
        }
      } else {
        tried = 0;
      }

      // 6. 促使页面滚动
      scrollHeight = document.body.scrollHeight;
      console.log('滚动距离:' + scrollHeight);
      window.scrollBy(0, scrollHeight);
      window.scrollBy(0, -10);

      // 7. 如果达到最高高度,则返回
      if (maxHeight > 0 && maxHeight <= scrollHeight) {
        console.log('已经达到最大高度!');
        clearInterval(timer);
        resolve();
      }
    }, 100);
  });
};

js代码你们不用管,直接CTRL cv即可

代码完善:

async def spider():
  # 1. 启动一个浏览器
  browser = await launch({'args': ['--no-sandbox', '--disable-setuid-sandbox']})
  # 2. 新开浏览器的一个网页标签页
  page = await browser.newPage()
  # 3. 在新开的网页标签页中输入掘金地址
  await page.goto('https://www.juejin.im')

  await page.evaluate("""
    上面JS滚动逻辑
  """)

  entrys = await page.querySelectorAll('ul.entryList li a.title')
  titles = []
  for entry in entrys:
    text = await entry.getProperty('innerText')
    title = await text.jsonValue()
    titles.append(title)

  print(titles)
  await browser.close()

但是有的人有可能出现以下问题:

是由于python里面bug导致的

解决办法:

def patch_pyppeteer():
  import pyppeteer.connection
  original_method = pyppeteer.connection.websockets.client.connect

  def new_method(*args, **kwargs):
    kwargs['ping_interval'] = None
    kwargs['ping_timeout'] = None
    return original_method(*args, **kwargs)

  pyppeteer.connection.websockets.client.connect = new_method


async def spider():
  ...

# 先调用修复BUG
patch_pyppeteer()
asyncio.get_event_loop().run_until_complete(spider())

更改这个设置就可以自运行了

browser = await launch({
    'headless': False,
    'slowMo': 10,'
    'devtools': True, #自动打开Chrome开发者工具
    'args': ['--no-sandbox', '--disable-setuid-sandbox']})

最后pyppeteer那部分难度较大,感觉是复制粘贴的,如果大佬有什么好的意见或者建议都可以留言进行交流,或者发送邮箱到2455219437@qq.com给我,那部分有什么好的资料,大家可以相互交流

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值