Python 爬虫 101
在浏览器输入地址并按下回车时,其实是向对应的服务器发出了 HTTP 请求,接收到返回数据后进行显示。而爬虫是通过程序发出请求,获取到原始数据,方便进行后续操作。
网页分析#
在爬取特定网页之前,首先应该使用浏览器对原始网页进行查看与分析。
HTML#
在 Chrome 浏览器中打开开发者工具,在最上方可以看到一些面板,其中 Elements 面板是网页对应的 HTML 代码。我们可以右键网页中的任意元素来检查其在 HTML 中的对应位置,也可以在 HTML 中右键进行各种操作,比如复制元素对应的各种选择器,方便代码使用。
HTTP 请求#
定位至 Network 面板,刷新页面,我们可以看到发出的 HTTP 请求。点击请求的 URL,在 Headers 栏可以看到请求的详细内容:
- General:
- Request URL: 请求的网址
- Request Methods: 请求方式,最主要的有 GET 和 POST。其中 GET 是获取信息,而 POST 是提交信息。
- Status Code: 返回码,请求成功一般是 200,不同代码有不同的含义
- Request Headers: 请求头,在请求时发送至服务器的数据
- User-Agent: 表明发送请求的系统平台、浏览器等
- Cookie: 用于告诉服务器两个请求是否来自同一个浏览器,比如保持用户的登陆状态
- Response Headers: 响应头
在 Preview 栏可以看到响应内容:渲染后的 html、图片、格式化后的 js 和 css。
在 Response 栏可以看到响应内容的原始格式。
Example#
以 豆瓣电影 Top 250 为例,目标是爬取 TOP 250 的电影信息。
打开网页及开发者工具,定位至 Network 面板,刷新网页,点击最上方的 URL
查看 Headers 栏,发现返回文件类型为 text/html
查看 Preview 栏可以看到一个新的「网页」,可能和原始网页存在区别!,因为网页上的某些内容并不是由 HTML 决定的,此时需要分析其它请求
查看 Response 栏,即源格式 html,搜索找到需要的内容,并分析结构,如下
1
2
3
4
5
6
7
8
9
| <li>
...
<span class="title">肖申克的救赎</span>
...
<span class="rating_num" property="v:average">9.7</span>
...
<span class="inq">希望让人自由。</span>
...
<li>
|
总共有 10 页,每页存在 25 个上述条目。点击第二页可以发现 URL 变为了 https://movie.douban.com/top250?start=25&filter=
。可以猜测其中的 ?start=25&filter=
变量达到了翻页的效果,可以在浏览器更改 start
变量值进一步确认
设计程序框架:遍历请求 URL,使用 start
变量进行翻页,获取响应内容后通过上述结构找到需要的内容
Request#
Request 是一个模拟 HTTP 请求的 Python 库。一些常见用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import requests
# Get/Post Request
r = requests.get('https://api.github.com/events', params = {'key': 'value'})
r = requests.post('https://httpbin.org/post', data = {'key':'value'})
# Pretend to be a browser
r = requests.get("https://api.github.com/events", headers = {'User-Agent': '[value]'})
# Response
r.status_code # response status code
r.headers # response headers
# Response content
r.text # text
r.content # binary
r.json() # json
|
BeautifulSoup#
BeautifulSoup 是一个从 HTML/XML 文件提取数据的 Python 库。一些常见用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| from bs4 import BeautifulSoup
# Load html with lxml parser
soup = BeautifulSoup(r.text, 'lxml')
# Find an element by tag
soup.p # first element
soup.p.a
soup.find('p')
# Find all elements
soup.find_all('p')
soup.find_all(['p', 'a']) # pass list
# Find an element by attribute
soup.find(class_='abc') # use keyword argument
soup.find(href=re.compile('abc'), id='abc') # multiple attribute
soup.find('p', class_='abc') # together with tag
# Find by string
soup.find_all(string='abc')
# Find by css selector
soup.select("p > a")
# Get the string of an element
soup.p.string
|
Example#
使用 request 爬取内容,正则表达式提取信息。正则表达式可以在 regex 101 进行测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import requests
import re
url = 'https://movie.douban.com/top250'
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
pattern = re.compile(r'<li>.*?title">(.*?)<.*?average">([\.\d]+)<.*?inq">(.*?)<.*?<\/li>', re.S)
for page in range(10):
params = {'start': page*25}
r = requests.get(url, params, headers=headers)
r.raise_for_status()
items = re.findall(pattern, r.text)
for i, item in enumerate(items):
print(f'{page*25+i+1}: {item[0]}, {item[1]}, {item[2]}')
|
使用 BeautifulSoup:
1
2
3
4
5
6
7
8
9
10
11
12
| # ...
for page in range(10):
# ...
soup = BeautifulSoup(r.text, 'lxml')
movies = soup.find(class_='grid_view').find_all('li')
for movie in movies:
ind = movie.find('em').string
title = movie.find(class_='title').string
rating = movie.find(class_='rating_num').string
intro = '' if movie.find(class_='inq') is None else movie.find(class_='inq').string
print(f'{ind}: {title}, {rating}, {intro}')
|
Selenium#
Selenium 是一个用于浏览器的自动化测试工具,用来模拟人操作网页,提供 Python 的 API。一些常见用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| from selenium import webdriver
# Basics
driver = webdriver.Chrome() # Create Chrome instance
driver.get('http://www.python.org') # Open webpage
driver.close() # Close one tab
driver.quit() # Close the browser
# Properties
driver.current_url
driver.get_cookie('key')
driver.get_cookies()
driver.add_cookie(dict)
driver.page_source
# Move between windows and frames
driver.find_elements_by_tag_name('iframe') # find all frames
driver.switch_to.frame(int) # switch to frame
driver.switch_to_default_content() # swith back
windows = driver.window_handles # get all windows
driver.switch_to.window(windows[i]) # switch to window
# Find an element
driver.find_element_by_id('id')
driver.find_element_by_name('name')
driver.find_element_by_xpath('xpath')
driver.find_element_by_link_text('link text')
driver.find_element_by_partial_link_text('partial link text')
driver.find_element_by_tag_name('tag name')
driver.find_element_by_class_name('class name') # change space to .
driver.find_element_by_css_selector('css selector')
# Find all elements (return list)
driver.find_elements_by_ # ...
# Interaction
# Send texts/keys
from selenium.webdriver.common.keys import Keys
element.send_keys('text')
element.send_keys('text', Keys.RETURN) # Using "Keys" class
element.clear() # clear the contents of a text field
# Click
element.click()
# Waits
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# EC has some predefined conditions
wait = WebDriverWait(driver, timeout, poll_frequency=0.5, ignored_exceptions=None)
output = wait.until(EC.presence_of_element_located((By.ID, 'id')))
|
Example#
抓取豆瓣电影 TOP250 中未看名单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
| from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup
import re
import math
def get_movie():
wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'article')))
soup = BeautifulSoup(driver.page_source, 'lxml')
movies = soup.find(class_='grid_view').find_all('li')
for movie in movies:
ind = movie.find('em').string
title = movie.find(class_='title').string
rating = movie.find(class_='rating_num').string
intro = '' if movie.find(class_='inq') is None else movie.find(class_='inq').string
print(f'{ind}: {title}, {rating}, {intro}')
username = '' # put username here
password = '' # put password here
options = webdriver.ChromeOptions()
options.add_argument('headless')
driver = webdriver.Chrome(options=options)
driver.get("https://www.douban.com")
wait = WebDriverWait(driver, 10)
print('Logging in...')
# Swith login method
driver.switch_to.frame(0)
login_switch = driver.find_element_by_class_name('account-tab-account')
login_switch.click()
# Login
user_field = driver.find_element_by_class_name('account-form-input')
pass_field = driver.find_element_by_class_name('account-form-input.password')
user_field.send_keys(username)
pass_field.send_keys(password, Keys.RETURN)
print('Open Top 250 and filter unseen')
# Open movie page
movie = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="db-global-nav"]/div/div[4]/ul/li[3]/a')))
movie.click()
# Navigat to 排行榜->全部->我没看过的
windows = driver.window_handles
driver.switch_to.window(windows[1])
top = wait.until(EC.presence_of_element_located((By.LINK_TEXT, '排行榜')))
top.click()
top = wait.until(EC.presence_of_element_located((By.LINK_TEXT, '全部')))
top.click()
unseen = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="mine-selector"]/input')))
unseen.click()
total = driver.find_element_by_class_name('count').text
total = int(re.search(r'\d+', total)[0])
print(f'还有 {total} 部未看')
pages = math.ceil(total / 25)
get_movie()
for page in range(2, pages + 1):
next_page = driver.find_element_by_class_name('next')
next_page.click()
wait.until(EC.text_to_be_present_in_element((By.CLASS_NAME, 'thispage'), str(page)))
get_movie()
driver.quit()
|
Ajax#
浏览器渲染出的网页有可能包含 JavaScript 的数据,此时使用 requests 爬取下来的内容不会包含这一部分。
Ajax 是 JS 异步更新页面上的内容的一种方法。即没有刷新页面,但页面内容可以变换。
对于 JS 内容,在 Chrome 浏览器开发者工具上的 Network 面板,通过选取 XHR
进行过滤,在 preview 中查找到需要的内容后,对这一 URL 做一个单独的请求即可。
伪装操作#
大部分网站都做了反爬取的操作,以豆瓣为例,需要传递 User-Agent 信息假装是浏览器。因此在爬取时需要针对反爬取做一些伪装操作,不仅是为了成功爬取,同时也在保护自己的信息(ip 地址),并且不道德的爬取会给他人服务器带来巨大负担。
User-Agent#
构建一个常见浏览器的 User-Agent 列表,爬取时随机选取
IP 代理#
在使用 requests
库进行爬取时存在 IP 被封的风险,因此需要使用 IP 代理
1
2
| proxies = {'http': 'http://xx.xxx.xxx.xx'}
response = requests.get(url, proxies=proxies)
|
一般会构建一个 IP 池,需要用时从里面随机选取。IP 代理可以购买,也可以先爬取网上免费的 IP,检测可用后添加至 IP 池中。GitHub 上有一些构建 IP 池的爬取的项目,通过其提供的 API 可以很方便地得到可用的代理 IP
时间间隔#
在请求之间等待随机时间,一方面减缓爬虫的频率,另一方面也使得行为更像人类,避免被封
1
2
3
4
| import time
import random
time.sleep(random.random()*3 + 4) # 依据实际情况调整
|