环境配置

apache
1
conda create --name spider python=3.8

第一个爬虫程序

  • Python urllib 库为Python的自带库,用于操作网页 URL,并对网页的内容进行抓取处理。
python
1
2
3
4
5
6
from urllib.request import urlopen

url = "http://www.baidu.com"
resp = urlopen(url)

print(resp.read().decode("utf-8"))
  • 保存到html文件中
python
1
2
3
4
5
6
7
8
9
10
11
12
from urllib.request import urlopen

url = "http://www.baidu.com"
resp = urlopen(url)

## with open("mybaidu.html", mode="w") as f:
## f.write(resp.read().decode("utf-8"))

with open("mybaidu.html", mode="wb") as f:
f.write(resp.read())

print("over!")

Web请求过程

  • 服务器渲染:在服务器那边直接把数据和html整合在一起,统一返回给浏览器

    在页面原代码中可以看到数据

  • 客户端渲染:第一次请求只要一个html框架,第二次请求拿到数据进行数据展示

    在页面原代码中看不到数据

豆瓣/电影/排行榜/喜剧中,查看网页原代码,里面是没有数据的,虽然网页上显示了憨豆先生,但在原代码里面查找不到。这种渲染是客户端渲染。

熟练使用浏览器抓包工具

在网页界面按F12打开开发者工具,点击网络(network),刷新一次页面,在网络下面可以看到网页渲染的全部请求,这些请求里面包括 对数据的请求。

image-20220708161953506

image-20220708162548893

HTTP协议

协议就是两个计算机之间为了能够流畅地进行沟通而设置的一个君子协定。常见的协议有TCP/IP,SOAP协议,HTTP协议 ,SMTP协议等等

HTTP协议,即超文本传输协议(Hyper Text Transfer Protocol,HTTP),是一个简单的请求-响应协议。它是从万维网(WWW——World Wide Web)服务器传输超文本到本地浏览器的传送协议。直白点就是浏览器和服务器之间的传输网页原代码需要遵守的协议。

HTTP协议把一条消息分为三大块内容,无论是请求还是响应都是三块内容

  • 请求
clean
1
2
3
4
请求行 -> 请求方式 请求url地址 协议
请求标头 -> 放一些服务器要使用的附加信息

请求体 -> 一般放一些请求参数
  • 响应
clean
1
2
3
4
状态行 -> 协议 状态码(200,404,500,302等)
响应标头 -> 放一些客户端要使用的一些附加信息

响应体 -> 服务器返回的真正客户端要用的内容(HTML,json)等

image-20220708171126927

请求标头中常见的一些重要内容(爬虫需要):

  • User-Agent:请求载体的身份标识(用什么发送的请求)
  • Referer:防盗链(这次请求时从哪个页面来的?反爬会用)
  • Cookie:本地字符串数据信息(用户登录信息,反爬的token)

响应标头中的一些重要的内容:

  • Cookie:本地字符串数据信息(用户登录信息,反爬的token)
  • 各种神奇的莫名其妙的字符串(这个需要经验,一般都是token字样,防止各种攻击和反爬)

请求方式:

  • GET:显式的提交
  • POST:隐式的提交

Request库入门

Request库并不是Python自带的模块

Request库的安装

python
1
2
pip install requsets
conda list requests ## 检验是否安装成功

案例1——搜狗周杰伦

  • 在搜狗中搜索周杰伦,获得周杰伦的url链接

    注意:直接输入“周杰伦”回车得到的长串链接只需要保留...query=周杰伦即可,后面的部分可以删除

python
1
2
3
4
5
import requests

url = 'https://www.sogou.com/web?query=周杰伦'
resp = requests.get(url)
print(resp.text)
  • 返回的信息中提示需要验证
html
1
2
3
...
<p class="p2">用户您好,我们的系统检测到您网络中存在异常访问请求。<br>此验证码用于确认这些请求是您的正常行为而不是自动程序发出的,需要您协助验证。</p>
...
  • 右键检查或者F12进入调试界面

    在请求头中获取User-Agent,它是用来描述当前的请求是通过什么设备发出的

image-20220725154820088

html
1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
  • 在爬虫程序中加一个标头

    User-Agent加上一个双引号,以及冒号后面的内容也加上一个双引号

    python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import requests

    url = 'https://www.sogou.com/web?query=周杰伦'
    request_header = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"
    }
    resp = requests.get(url, headers=request_header)

    print(resp.text)

案例二——百度翻译

  • 进入百度翻译的网页并打开调试界面

    点开网络,清除无关的请求

    使用英文输入法输入dog,出现4个下拉菜单

    在网络的请求中找到sug,点开预览,可以看到4个下拉菜单的内容

image-20220725164828439

  • Headers->General中可以获得url

    传入的参数dog被保存在字段kw

image-20220725201504424

  • 这里的传入参数并不像上一个周杰伦的例子,保存在url中,而是在data里面

    python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import requests

    url = "https://fanyi.baidu.com/sug"
    ## 这里的传入参数并不像上一个周杰伦的例子,保存在url中,而是在data里面
    s = input("请输入你要翻译的英文单词: ")
    keyword = {
    "kw" : s
    }

    ## 发送post请求,发送的数据必须放在字典中,通过data参数进行传递
    resp = requests.post(url, data = keyword)
    ## 将服务器返回的内容直接处理成json() => dict
    print(resp.json())

案例三——豆瓣电影

  • 进入网页豆瓣->电影->排行榜->分类排行榜->喜剧
  • 获得对应内容的url

image-20220725211304730

  • 这个url中,问号?前面的部分是链接,后面的部分是参数

    html
    1
    Request URL: https://movie.douban.com/j/chart/top_list?type=24&interval_id=100%3A90&action=&start=0&limit=20
  • 对应的参数在payload中

image-20220725211803238

  • 通过Request Method: GET可以知道它的请求方式是GET

  • 重新封装参数

    python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import requests
    ## 去掉?后的参数部分,将其封装在param中
    url = "https://movie.douban.com/j/chart/top_list"
    param = {
    "type": "24",
    "interval_id": "100:90",
    "action": "",
    "start": 0,
    "limit": 20,
    }
    resp = requests.get(url = url, params = param)
    print(resp.url)
    python
    1
    2
    ## 返回的链接和获取的Request URL一样
    https://movie.douban.com/j/chart/top_list?type=24&interval_id=100%3A90&action=&start=0&limit=20
  • 直接print(resp.url)什么都不返回,原因是被反爬了

  • 先尝试修改User-Agent

    • 输出headers,查看Python默认的User-Agent是什么
    python
    1
    print(resp.request.headers)
    • 可以看到Python默认的User-Agent'python-requests/2.28.1'
    python
    1
    {'User-Agent': 'python-requests/2.28.1', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
    • 去浏览器调试界面Request Headers下找到User-Agent,将其添加到爬虫代码中

      python
      1
      2
      3
      headers = {
      "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"
      }
  • 最后代码

    python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import requests

    url = "https://movie.douban.com/j/chart/top_list"

    ## 重新封装参数
    param = {
    "type": 24,
    "interval_id": "100:90",
    "action": "",
    "start": 0,
    "limit": 20,
    }

    ## 设置headers,浏览器标识UA
    headers = {
    "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"
    }

    resp = requests.get(url = url, params = param, headers = headers)

    print(resp.json())
    resp.close()
  • 在访问完服务器之后记得关闭访问,如果不关则会和服务器一直保持连接,同时访问多个服务器可能会报错

    python
    1
    resp.close() ## 关掉resp
  • 参数start的值每20每20地往上加,修改参数start的值可以爬取其它的内容

image-20220725215900543

Re解析

正则表达式

Regular Expression

  • 常用元字符:

    python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    .    匹配除换行符以为的任意字符
    \w 匹配字母数字下划线
    \s 匹配任意空白符
    \d 匹配数字
    \n 匹配一个换行符
    \t 匹配一个制表符

    ^ 匹配字符串的开头
    $ 匹配字符串的结尾

    \W 匹配非字母数字下划线
    \D 匹配非数字
    \S 匹配非空白符
    | 逻辑或
    () 表示组
    [...] 匹配[]中的所有字符
    [^...] 匹配除了[^]中的所有字符
  • 量词:

    python
    1
    2
    3
    4
    5
    6
    *    重复零次或者更多次
    + 重复一次或者更多次
    ? 重复零次或者一次
    {n} 重复n次
    {n,} 重复n次或者更多次
    {m,n}重复n次到m次
  • 贪婪匹配和惰性匹配

    python
    1
    2
    .*    贪婪匹配——匹配尽可能长的字符串
    .*? 惰性匹配——匹配尽可能短的字符串

正则表达式快速替换

  • 从网页上复制粘贴来的参数是这样,需要给它们加上引号和逗号
python
1
2
3
4
5
6
7
8
9
10
11
12
param = {
action: list_ex
begin: 0
count: 5
fakeid: MzIwMzUxNjgzNw==
type: 9
query:
token: 773287030
lang: en_US
f: json
ajax: 1
}
  • 可以使用正则表达式快速替换
python
1
2
匹配:(\w+?): (\w+)
替换:'$1': '$2',
  • 替换后结果如下
python
1
2
3
4
5
6
7
8
9
10
11
12
param = {
'action': 'list_ex',
'begin': '0',
'count': '5',
'fakeid': 'MzIwMzUxNjgzNw==',
'type': '9',
'query': '',
'token': '773287030',
'lang': 'en_US',
'f': 'json',
'ajax': '1',
}

re模块

  • 导入模块

    python
    1
    import re
  • findall

    findall返回一个列表

    python
    1
    2
    lst = re.findall(r"\d+", "移动客服10086,电信客服10000")
    print(lst, type(lst))
    python
    1
    2
    ## 返回一个列表
    ['10086', '10000'] <class 'list'>
  • finditer

    finditer返回一个迭代器,从迭代器中拿数据需要用.group()

    python
    1
    2
    itr = re.finditer(r"\d+", "移动客服10086,电信客服10000")
    print(itr, type(itr))
    python
    1
    2
    ## 返回一个迭代器
    <callable_iterator object at 0x000001C373DF2B20> <class 'callable_iterator'>
    python
    1
    2
    3
    4
    5
    for i in iter:
    print(i.group())
    ## 输出
    10086
    10000
  • search

    search返回的结果是match对象,拿数据需要.group()

    search全文匹配,并且只返回找到的第一个结果

    python
    1
    2
    sch = re.search(r"\d+", "移动客服10086,电信客服10000")
    print(sch.group())
    python
    1
    10086
  • match

    match是从头开始匹配的,默认在正则前加了一个^,不是常用方法

    python
    1
    2
    3
    4
    mch = re.match(r"\d+", "移动客服10086,电信客服10000")
    print(mch.group())

    ## 输出会报错,因为mch并没有匹配到
  • 预加载正则表达式

    预加载正则表达式可以稍稍提高一点程序运行的效率,并且可以多次使用

    python
    1
    2
    3
    4
    obj = re.compile(r"\d+")

    lst = obj.findall(r"\d+", "移动客服10086,电信客服10000")
    itr = obj.finditer(r"\d+", "移动客服10086,电信客服10000")
  • re.S.能匹配换行符

    python
    1
    obj = re.compile(r"xxx", re.S)
  • 使用组

    使用?P<group_name>可以给组起名

    python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import re

    s = """
    移动客服:10086;
    电信客服:10000;
    联通客服:10010。
    """

    obj = re.compile(r"^(?P<Name>.*客服).(?P<Number>\d{5}).$",re.M)

    itr = obj.finditer(s)

    for i in itr:
    print(i.group("Name"))
    print(i.group("Number"))
    python
    1
    2
    3
    4
    5
    6
    7
    ## 输出
    移动客服
    10086
    电信客服
    10000
    联通客服
    10010

实战——豆瓣Top250

https://movie.douban.com/top250

  • 可以先打开源代码,查看源代码中有没有我们想要爬取的内容

image-20220726152039173

  • 豆瓣Top205的原代码中已经体现了电影的排名、名称和评分等信息
  • 那么爬取这个网站的步骤就是1.通过requests模块先拿到网页源代码2.再通过re模块提取有效信息

获得网页源代码

  • 通过requests模块获取网页源代码
python
1
2
3
4
5
6
7
8
9
10
11
12
13
import requests

url = "https://movie.douban.com/top250"
header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"
}

resp = requests.get(url, headers = header)

with open("douban250.html", mode="w", encoding="utf-8") as f:
f.write(resp.text)

resp.close()
  • 奇怪的地方是直接在终端中输出resp.text并没有显示完整的html源代码,但将其保存到本地文件中却是完整的代码

    python
    1
    print(resp.text) ## 并不输出完整代码

匹配关键信息

  • 先观察源代码,观察需要匹配信息的特征

xskdjs

  • 编写正则表达式

    正则表达式在线验证工具:https://regex101.com/

    可以在网站上验证自己的正则表达式是否正确的

    注意要设置单行匹配,否则.不能匹配换行符

    python
    1
    <li>.*?<em class="">(?P<ranking>\d+).*?<span class="title">(?P<title>.*?)<\/span>.*?<span class="rating_num" property="v:average">(?P<score>[\d\.]+)<\/span>.*?(?P<Number>\d+)人评价'
  • 得到目标信息

    python
    1
    2
    3
    obj = re.compile(r'<li>.*?<em class="">(?P<ranking>\d+).*?<span class="title">(?P<title>.*?)<\/span>.*?<span class="rating_num" property="v:average">(?P<score>[\d\.]+)<\/span>.*?(?P<number>\d+)人评价', re.S)

    itr = obj.finditer(resp.text)
  • 输出目标信息

    python
    1
    2
    3
    4
    5
    6
    for i in itr:
    print("ranking : " + i.group("ranking"))
    print("title : " + i.group("title"))
    print("score : " + i.group("score"))
    print("number : " + i.group("number"))
    print("---------\n")
    python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ranking : 1
    title : 肖申克的救赎
    score : 9.7
    number : 2659667
    ---------
    ...

    ranking : 25
    title : 怦然心动
    score : 9.1
    number : 1694301
    ---------
  • 整个代码

    python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import requests
    import re

    url = "https://movie.douban.com/top250"
    header = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"
    }

    resp = requests.get(url, headers = header)


    obj = re.compile(r'<li>.*?<em class="">(?P<ranking>\d+).*?<span class="title">(?P<title>.*?)<\/span>.*?<span class="rating_num" property="v:average">(?P<score>[\d\.]+)<\/span>.*?(?P<number>\d+)人评价', re.S)
    itr = obj.finditer(resp.text)

    for i in itr:
    print("ranking : " + i.group("ranking"))
    print("title : " + i.group("title"))
    print("score : " + i.group("score"))
    print("number : " + i.group("number"))
    print("---------\n")

将数据存入csv文件

python
1
2
3
4
5
6
7
8
import csv
...
f = open("douban250.csv", mode="w")
csvwriter = csv.writer(f)

for i in itr:
dic = i.groupdict()
csvwriter.writerow(dic.values())
python
1
2
3
4
5
6
7
1,肖申克的救赎,9.7,2659674

2,霸王别姬,9.6,1974596

...

25,怦然心动,9.1,1694301

修改参数

  • 不同页数url对应的参数不一样,修改这个参数可以爬取不同页面的数据
  • 第一页的参数是start=0,第二页的参数是start=25,以此类推

image-20220726183122932