python数据采集

爬虫(crawler)也经常被称为网络蜘蛛(spider),是按照一定的规则自动浏览网站并获取所需信息的机器人程序(自动化脚本代码),被广泛的应用于互联网搜索引擎和数据采集。使用过互联网和浏览器的人都知道,网页中除了供用户阅读的文字信息之外,还包含一些超链接,网络爬虫正是通过网页中的超链接信息,不断获得网络上其它页面的地址,然后持续的进行数据采集。正因如此,网络数据采集的过程就像一个爬虫或者蜘蛛在网络上漫游,所以才被形象的称为爬虫或者网络蜘蛛。

主要的应用领域:

  1. 搜索引擎
  2. 新闻聚合
  3. 社交应用
  4. 舆情监控
  5. 行业数据

主要的实用库,以及前期的页面调研

对于数据页面或者数据接口的研究是第一步,你需要掌握html结构,页面渲染方式(ajax、iframe或者其他懒加载机制),以及安全验证(登录),session 保持、auth 等。 
  1. httpie, 一个命令行的页面请求工具,可以用于前期的page页面结构观察。
    doc文档

  2. buildwith 获取网站使用技术

    1
    pip install buildwith 
  3. requests 库
    doc文档
    这是一个HTTP库,可以用于发送HTTP请求,获取响应,解析响应,获取数据。

    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
    import requests
    r = requests.get('https://www.baidu.com')
    r = requests.post('https://httpbin.org/post', data={'key': 'value'})
    r = requests.put('https://httpbin.org/put', data={'key': 'value'})
    r = requests.delete('https://httpbin.org/delete')
    r = requests.head('https://httpbin.org/get')
    r = requests.options('https://httpbin.org/get')
    payload = {'key1': 'value1', 'key2': 'value2'}
    r = requests.get('https://httpbin.org/get', params=payload)

    r.text() # 获取响应文本
    r.json() # 获取响应json
    r.raw() # 获取响应原始数据
    r.status_code() # 获取响应状态码
    r.headers() # 获取响应头
    r.cookies() # 获取响应cookies
    r.history() # 获取响应历史记录
    r.elapsed() # 获取响应时间
    r.encoding() # 获取响应编码
    r.content() # 获取响应内容
    r.request() # 获取请求对象
    r.url() # 获取响应url
    r.raise_for_status() # 获取响应状态码, 200情况下 是None
    r.close()

    # 获取简单流与原始流
    r = requests.get('https://api.github.com/events', stream=True)
    r.raw
    r.raw.read(10)

    #获取响应流 带buff,需要注意的与 raw不同,1 需要关闭流,2 默认已经decode gzip 和默认的压缩格式
    with open(filename, 'wb') as fd:
    for chunk in r.iter_content(chunk_size=128):
    fd.write(chunk)

    #文件上传
    files = {'file': open('report.xls', 'rb')}
    r = requests.post('https://httpbin.org/post', files=files)
    r.text()


爬取数据的一个流程

最佳实践

1. 一个最原生的requests + re 爬取网页的例子

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

import random
import re
import time
import requests
import requests.adapters

# 通过正则表达式获取class属性为title且标签体不以&开头的span标签并用捕获组提取标签内容
pattern1 = re.compile(r'<span class="title">([^&]*?)</span>')
# 通过正则表达式获取class属性为rating_num的span标签并用捕获组提取标签内容
pattern2 = re.compile(r'<span class="rating_num".*?>(.*?)</span>')
# 获取豆瓣电影top250
for p in range(1,11):
resp = requests.get(
url =f'https://movie.douban.com/top250?start={(p - 1) * 25}',
# 设置头,有些网站必须设置头才可以访问. 这里页可模拟baiduspider的请求头
headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36'}
)
titles = pattern1.findall(resp.text)
ranks = pattern2.findall(resp.text)
# 使用zip压缩两个列表,循环遍历所有的电影标题和评分
for title, rank in zip(titles, ranks):
print(title, rank)
# 随机休眠1-5秒,避免爬取页面过于频繁
time.sleep(random.random() * 4 + 1)



  1. 使用代理

    一般网站基于自身安全和带宽容量的浪费,在网关或者入口处都会有防爬和流量控制,所以需要使用代理来绕过网关,防止被封禁。

    这里建议使用商业代理(免费往往不太稳定),比如:蘑菇代理、芝麻代理、快代理等
    这里需要去他们的官网注册来得到代理ip。一般上面的代理服务商都会提供完善的demo给我们。

  2. 其他常用的html解析库

    不过这里个人提倡原生方式,类似xpath或者bef这类其实是将text生成一个内存的dom树,然后通过xpath语法提取数据,这将消耗较多内存

    解析方式 对应的模块 速度 使用难度
    正则表达式解析 re 困难
    XPath 解析 lxml 一般
    CSS 选择器解析 bs4pyquery 不确定 简单
    • BeautifulSoup

      关于 BeautifulSoup ,可以参考它的官方文档

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      # 创建BeautifulSoup对象
      soup = bs4.BeautifulSoup(resp.text, 'lxml')
      # 通过CSS选择器从页面中提取包含电影标题的span标签
      title_spans = soup.select('div.info > div.hd > a > span:nth-child(1)')
      # 通过CSS选择器从页面中提取包含电影评分的span标签
      rank_spans = soup.select('div.info > div.bd > div > span.rating_num')
      for title_span, rank_span in zip(title_spans, rank_spans):
      print(title_span.text, rank_span.text)

    • lxml 一个基于xpath的解析dom库,对于xpath 需要了解相关知识.

      1
      2
      3
      4
      5
      6
      7
      8
      tree = etree.HTML(resp.text)
      # 通过XPath语法从页面中提取电影标题
      title_spans = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]')
      # 通过XPath语法从页面中提取电影评分
      rank_spans = tree.xpath('//*[@id="content"]/div/div[1]/ol/li[1]/div/div[2]/div[2]/div/span[2]')
      for title_span, rank_span in zip(title_spans, rank_spans):
      print(title_span.text, rank_span.text)

  3. 并发在抓取中的使用

    数据采集(爬虫)在python并发情形中是最符合 大量io操作的,所以使用线程和异步IO是在爬虫中比较常用的。尤其是深度采集的需求时.

    • 线程池

      主要片段:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
        import os
      from concurrent.futures import ThreadPoolExecutor

      with ThreadPoolExecutor(max_workers=16) as pool:
      for page in range(3):
      resp = requests.get(f'https://image.so.com/zjl?ch=beauty&sn={page * 30}')
      if resp.status_code == 200:
      pic_dict_list = resp.json()['list']
      for pic_dict in pic_dict_list:
      pool.submit(download_picture, pic_dict['qhimg_url'])
      ```

      * 异步IO

      我们使用`aiohttp`将上面的代码修改为异步 I/O 的版本。为了以异步 I/O 的方式实现网络资源的获取和写文件操作,我们首先得安装三方库`aiohttp`和`aiofile`,命令如下所示。
      ``` shell
      pip install aiohttp aiofile
      下面是一个完整的demo:
      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
      import json
      import os

      import aiofile
      import aiohttp

      async def download_picture(session, url):
      filename = url[url.rfind('/') + 1:]
      async with session.get(url, ssl=False) as resp:
      if resp.status == 200:
      data = await resp.read()
      async with aiofile.async_open(f'images/beauty/{filename}', 'wb') as file:
      await file.write(data)


      async def fetch_json():
      async with aiohttp.ClientSession() as session:
      for page in range(3):
      async with session.get(
      url=f'https://image.so.com/zjl?ch=beauty&sn={page * 30}',
      ssl=False
      ) as resp:
      if resp.status == 200:
      json_str = await resp.text()
      result = json.loads(json_str)
      for pic_dict in result['list']:
      await download_picture(session, pic_dict['qhimg_url'])


      def main():
      if not os.path.exists('images/beauty'):
      os.makedirs('images/beauty')
      loop = asyncio.get_event_loop()
      loop.run_until_complete(fetch_json())
      loop.close()


      if __name__ == '__main__':
      main()
  4. Selenium with Python

    selenium 是一个用于自动化测试的开源工具,可以驱动浏览器,模拟用户操作,实现自动化测试。 随着css和js技术发展,很多站点数据都是由前端的js渲染或者异步加载的,这个时候如果需要提取数据即可通过selenium来完成。 通过selenium可以模拟js执行,按钮点击等大多数浏览器事件。 更多详细内容可以参考官方文档
    python-selenium文档

    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
    import os
    import time
    from concurrent.futures import ThreadPoolExecutor

    import requests
    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.common.keys import Keys

    DOWNLOAD_PATH = 'images/'


    def download_picture(picture_url: str):
    """
    下载保存图片
    :param picture_url: 图片的URL
    """
    filename = picture_url[picture_url.rfind('/') + 1:]
    resp = requests.get(picture_url)
    with open(os.path.join(DOWNLOAD_PATH, filename), 'wb') as file:
    file.write(resp.content)


    if not os.path.exists(DOWNLOAD_PATH):
    os.makedirs(DOWNLOAD_PATH)
    browser = webdriver.Chrome()
    browser.get('https://image.so.com/z?ch=beauty')
    browser.implicitly_wait(10)
    kw_input = browser.find_element(By.CSS_SELECTOR, 'input[name=q]')
    kw_input.send_keys('苍老师')
    kw_input.send_keys(Keys.ENTER)
    for _ in range(10):
    browser.execute_script(
    'document.documentElement.scrollTop = document.documentElement.scrollHeight'
    )
    time.sleep(1)
    imgs = browser.find_elements(By.CSS_SELECTOR, 'div.waterfall img')
    with ThreadPoolExecutor(max_workers=32) as pool:
    for img in imgs:
    pic_url = img.get_attribute('src')
    pool.submit(download_picture, pic_url)

  5. 爬虫框架Scrapy

    scrapy是一个基于python的爬虫框架,可以快速实现爬虫,并且可以方便的进行数据存储。scrapy的安装可以参考官方文档

    doc2

下面是一个简单的scrapydemo,未涉及部分可查看官方doc.

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# 创建项目
scrapy startproject scrapydemo
# 创建爬虫主入口
scrapy genspider doubanbook book.douban.com

# 1. 创建采集数据的对象orm
class DoubanBookItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
score = scrapy.Field()
motto = scrapy.Field()



# 2. 采集逻辑编写
from typing import Iterable
import scrapy
from scrapydemo.items import DoubanBookItem


class DoubanbookSpider(scrapy.Spider):
name = "doubanbook"
allowed_domains = ["book.douban.com"]
# 固定urls
#start_urls = ["https://book.douban.com/top250?start=0"]

def start_requests(self):
for page in range(10):
yield scrapy.Request(url=f"https://book.douban.com/top250?start={page*25}")

def parse(self, response):
# print(response) # <200 http://www.4399.com/flash/>
# print(response.text) # 打印页面源代码
# response.xpath() # 通过xpath解析数据
# response.css() # 通过css解析数据

# 获取名称
# txt = response.xpath('//ul[@class="n-game cf"]/li/a/b/text()')
# txt 列表中的每一项是一个Selector:
# <Selector query='//ul[@class="n-game cf"]/li/a/b/text()' data='逃离克莱蒙特城堡'>]
# 要通过extract()方法拿到data中的内容
# print(txt)

# txt = response.xpath('//ul[@class="n-game cf"]/li/a/b/text()').extract()
# print(txt) #

# 也可以先拿到每个li,然后再提取名字
# lis = response.xpath('//ul[@class="n-game cf"]/li')
book_items = response.css('#content > div > div.article > div > table')
#print(len(book_items))
for i in book_items:
item = DoubanBookItem()
item['title'] = i.css(' tr > td:nth-child(2) > div.pl2 > a::text').extract_first().strip()
item['score'] = i.css(' tr > td:nth-child(2) > div.star.clearfix > span.rating_nums::text').extract_first()
item['img'] = i.css(' tr > td:nth-child(1) > a > img::attr("src")').extract_first()
item['motto'] = i.css(' tr > td:nth-child(2) > p.quote > span::text').extract_first()
yield item

# 页面下的分页url全部提取出来
#hrefs = response.css('#content > div > div.article > div.paginator > a::attr("href")')
#for href in hrefs:
# full_url = response.urljoin(href.extract())
# yield scrapy.Request(url=full_url)



# 3. 存储数据
from itemadapter import ItemAdapter
import pymysql


class ScrapydemoPipeline:
def process_item(self, item, spider):
return item

# 存储到mysql的pipline
class Doubanbook2DbPipeline(object):
def open_spider(self, spider):
print('爬虫开始运行')
self.conn = pymysql.connect(host='localhost', port=3306, user='root', passwd='123456', db='doubanbook', charset='utf8')
self.cursor = self.conn.cursor()
def __init__(self):
self.conn = None
self.cursor = None

def process_item(self, item, spider):
sql = 'insert into book(title, score, motto, img) values(%s, %s, %s, %s)'
self.cursor.execute(sql, (item['title'], item['score'], item['motto'], item['img']))


class Doubanbook2Pipeline(object):
def __init__(self):
self.f = open('doubanbook.txt', 'w', encoding='utf-8')

def process_item(self, item, spider):
self.f.write(item['title'] + ' ' + item['score'] + ' ' + item['motto'] + ' ' + item['img'] + '\n')
return item

def close_spider(self, spider):
self.f.close()

最后setting.py中需要配置pipeline ,以及对请求的header和浏览器的设置,防止403错误。

USER_AGENT

DEFAULT_REQUEST_HEADERS = {
“Accept”: “text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8”,
“Accept-Language”: “en”,
}

  1. 反爬的一些应对方式

    1. 构造合理的HTTP请求头。

      • Accept

      • User-Agent

      • Referer

      • Accept-Encoding

      • Accept-Language

    2. 检查网站生成的Cookie。

      • 有用的插件:EditThisCookie
      • 如何处理脚本动态生成的Cookie
    3. 抓取动态内容。

      • Selenium + WebDriver
      • Chrome / Firefox - Driver
    4. 限制爬取的速度。

    5. 处理表单中的隐藏域。

      • 在读取到隐藏域之前不要提交表单
      • 用RoboBrowser这样的工具辅助提交表单
    6. 处理表单中的验证码。

      • OCR(Tesseract) - 商业项目一般不考虑

      • 专业识别平台 - 超级鹰 / 云打码

        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
        from hashlib import md5

        class ChaoClient(object):

        def __init__(self, username, password, soft_id):
        self.username = username
        password = password.encode('utf-8')
        self.password = md5(password).hexdigest()
        self.soft_id = soft_id
        self.base_params = {
        'user': self.username,
        'pass2': self.password,
        'softid': self.soft_id,
        }
        self.headers = {
        'Connection': 'Keep-Alive',
        'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
        }

        def post_pic(self, im, codetype):
        params = {
        'codetype': codetype,
        }
        params.update(self.base_params)
        files = {'userfile': ('captcha.jpg', im)}
        r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files, headers=self.headers)
        return r.json()


        if __name__ == '__main__':
        client = ChaoClient('用户名', '密码', '软件ID')
        with open('captcha.jpg', 'rb') as file:
        print(client.post_pic(file, 1902))
    7. 绕开“陷阱”。

      • 网页上有诱使爬虫爬取的爬取的隐藏链接(陷阱或蜜罐)
      • 通过Selenium+WebDriver+Chrome判断链接是否可见或在可视区域
    8. 隐藏身份。

总结

python 在数据采集方面,有非常多的工具,如requests,selenium,scrapy等,但是对于一些简单的爬虫,还是使用requests比较方便。
另外,对于一些需要登录的网站,使用selenium可以模拟登录,但是对于一些需要验证码的网站,selenium可能无法解决。