smarth 发表于 2021-9-15 11:16

[原创][python爬虫] 利用多线程爬取漫画实例及简单反反爬

本帖最后由 smarth 于 2021-9-15 12:26 编辑

## 概述

利用python爬取[海贼王漫画彩色版](https://ww8.readonepiece.com/manga/one-piece-digital-colored-comics/)

## 库介绍

1. requests
2. beautifulSoup
3. re (正则表达式)
4. (https://github.com/VeNoMouS/cloudscraper)
    第三方库,用来突破cloudflare限制
5. threading
6. 其他

## 网页分析

### 获取章节页面

章节页面URL很简单 基本样式如下`https://ww8.readonepiece.com/chapter/one-piece-digital-colored-comics-chapter-001/`
末尾是章节序号

### 获取图片URL

根据之前获得的章节页面获取每章图片的URL,下面是关键代码解释

```python
res = BeautifulSoup(html, "lxml").find(attrs={"class":"js-pages-container"})
```

利用 class="js-pages-container" 这个唯一属性获取图片url父标签

```python
ls = []
    for img in res.find_all(name="img"):
      url = img.attrs["src"]
      if check.search(url) != None:
            ls.append(url)
```

将url父标签中所有img标签提取出来,它的"src"属性就是需要的图片url

将所有的图片url放到列表中,返回主函数

### 获取图片

```python
def getPng(pictureName, url):
    if os.path.exists(pictureName):
            return True
    content = getPictureContent(url)
    if content == None:
      return False
    else:
      with open(pictureName, "wb") as fout:
            fout.write(content)
      return True
```

`getPng(pictureName, url)` 函数的两个参数分别表示当前下载图片的文件名和url,其中文件名包括路径 `示例: ./chapter-001/1.png`

如果图片成功下载则返回 `True` 否则 `False`

### 流程

1. 先判断之前是否下载过该图片 `os.path.exists()`,如果下载过,直接返回 `True` (这个主要调试时用到,避免重复下载,节约时间。正常运行代码不会出现重复文件)
2. 调用自定义的 `getPictureContent()` 函数, 返回二进制,即`requests.get().content`, 如果返回 `None` 则未能获取图片,返回 `False`
3. 保存成功下载的图片, 返回`True`

### 反反爬

网站使用cloudflare进行反爬,直接 `requests.get()`返回状态码403,并附以下内容
```html
<head>
<title>Access denied | img.mghubcdn.com used Cloudflare to restrict access</title>
...</head>
<body>...</body>
```

通过 第三方库 cloudscrapy 可突破限制,项目地址在上面

## 下载失败处理

1. 所有下载失败的图片将会以`'path':'url'\n` 的格式保存在 `./faliure/chapter-xxx.txt`文件,其中 `xxx`为章节序列
2. 手动运行 `reDownload.py` 将下载 `failure`目录下所有文件中图片
3. 下载每个章节前都会首先生成`./failure/chapter-xxx.txt` 文件,当本章节下载文成后如果该文件为空则删除该文件。下载`failure`目录下同理。

## 多线程

1. 不使用多线程下载速度大概是一分钟一章且失败概率较低。(测试了前10章漫画,只有第4章失败了3次)
2. 使用多线程每次下载n章,~~但失败率提高~~ [见下方更新]
3. 多线程设计不完善,后续将尝试对图片采用多线程下载而非仅仅章节。

## 完整代码结构

```
|--structure
|--reDownload.py下载之前失败的图片
    |--main()主函数
      |--getFileList() 先获取 failure 目录下所有文件名
      |--reDownload() 将文件名传入(包括路径)此函数,开始下载
      |--getPng() 调用 spider 中的 getPng 函数
          |--log() 每下载完一个文件里的所有url输出一次日志
|--spider.py 爬虫文件
    |--threadManager() 多线程下载管理
      |--class downloadThread 下载线程类,实例化此类获得下载线程
      |--main() 主函数
          |--Makedir() 根据传入的路径创建文件夹, 只能创建一层, 这里创建了 failure 文件夹
          |--getChapter() 单个章节下载
            |--Makedir() 创建对应章节的文件夹
            |--getHtmlText() 获取章节页面
            |--download() 调用 download 获取章节页面
            |--getPng() 获取图片
            |--getPictureContent() 调用 getPictureContent 函数获得图片内容
                |--download() 调用download 获取图片内容
                  |--csDownload() scDownload 在 download 中调用以应对cloudflare防御, 上面不写是因为 getHtmlText 不会触发反爬机制
            |--parseHtml() 解析 获得的章节页面 Html 以获得图片 url
```

## 注
1. 本爬虫在外网环境运行良好
2. `parseHtml()` 方法中有我临时加入的去除推广片段,可能不适用于所有页面.不适用原因(图片url格式改变)
   ```python
         check = re.compile(r"https://img.mghubcdn.com/file/imghub/one-piece-colored/")
         ...
             if check.search(url) != None:
                           ls.append(url)
         ```
3. 直接运行示例代码所有文件会保存在`./test01/`目录下,可通过更改main函数中的 `basePath` 变量更改。
4. 更改 `threadManager` 中的 begin,end 变量控制下载的章节

##<span id="jump">更新

刚才重新测试了一波,失败率和网络情况有关,无关是否开启多线程。

开启40个线程,5分钟内下完了200章,速度相当可观

## 完整代码
```python
# spider.py

import os
import time
import threading
from bs4 import BeautifulSoup
import re
import requests
import datetime
import cloudscraper


def log(msg):
    time = datetime.datetime.now()
    print('['+time.strftime('%Y.%m.%d-%H:%M:%S')+']:'+msg)

def csDownload(url):
    scraper = cloudscraper.create_scraper()
    try:
      res = scraper.get(url)
    except:
      res = None
    return res

def download(url, head = None):
    header = {
      "referer": "https://ww8.readonepiece.com/",
      "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36"
    }
    if head != None:
      header.update(head)
    try:
      res = requests.get(url, headers=header, timeout=5)
    except:
      return None
    if res.status_code == 403 and res.text.find("Cloudflare"):
      res = csDownload(url)
    if res == None:
      return res
    return res if res.status_code == 200 else None

def getHtmlText(url):
    res = download(url)
    return res.text if res != None else res

def getPictureContent(url, head = None):
    res = download(url, head)
    return res.content if res != None else res

def getPng(pictureName, url):
    if os.path.exists(pictureName):
            return True
    content = getPictureContent(url)
    if content == None:
      return False
    else:
      with open(pictureName, "wb") as fout:
            fout.write(content)
      return True

def parseHtml(html):
    check = re.compile(r"https://img.mghubcdn.com/file/imghub/one-piece-colored/")
    res = BeautifulSoup(html, "lxml").find(attrs={"class":"js-pages-container"})
    ls = []
    for img in res.find_all(name="img"):
      url = img.attrs["src"]
      if check.search(url) != None:
            ls.append(url)
    return ls

def Makedir(path):
    try:
      os.mkdir(path)
    except:
      pass

def getChapter(url, chapter, basePath = './'):
    html = None
    while html == None:
      html = getHtmlText(url)
      time.sleep(1)
    fName = f"{basePath}failure/{chapter}.txt"
    failure = open(fName, "w+")
    srcList = parseHtml(html)
    Makedir(basePath+chapter)
    path = basePath + chapter + '/'
    for pictureUrl in srcList:
      pictureName = path + re.search('\d+\.\w+', pictureUrl).group()
      if not getPng(pictureName, pictureUrl):
            failure.write(f"'{pictureName}':'{pictureUrl}'"+'\n')
      time.sleep(1)
    failure.close()
    if not os.path.getsize(fName):
      os.remove(fName)

def main(start, end):
    basePath = "./test01/"
    Makedir(basePath)
    baseURL = 'https://ww8.readonepiece.com/chapter/one-piece-digital-colored-comics-chapter-'
    Makedir(basePath+"/failure")
    for num in range(start, end):
      chapter = "chapter-{:>03d}".format(num)
      getChapter("{}{:>03d}/".format(baseURL, num), chapter, basePath=basePath)
      log("第 {:>03d} 章完成!".format(num))

class downloadThread(threading.Thread):   
    def __init__(self, begin, end):
      threading.Thread.__init__(self)
      self.begin = begin
      self.end = end

    def run(self):                  
      main(self.begin, self.end)   

def threadManager():
    begin = 1
    end = 100
    for stp in range(begin, end + 1, 10):
      downloadThread(stp, stp+10).start()

if __name__ == '__main__':
    threadManager()
```

```python
# reDownload.py

import os
import re
import time
from spider import getPng, log

def getFileList(path):
    lst = os.listdir(path)
    return lst

def readURL(name):
    res = dict()
    with open(name, "r") as fin:
      for line in fin.readlines():
            key = re.search("(?<=').*(?=':)", line).group()
            value = re.search("(?<=:').*(?=')", line).group()
            res = value
    os.remove(name)
    return res

def reDownload(path):
    if not os.path.getsize(path):
      return
    res= readURL(path)
    fout = open(path, "w+")
    for key, value in res.items():
      if not getPng(key, value):
            fout.write("'{}':'{}'".format(key, value)+'\n')
      time.sleep(1)
    fout.close()
    if not os.path.getsize(path):
      os.remove(path)

def main():
    path = './test01/failure/'
    fileList = getFileList(path)
    for file in fileList:
      reDownload(path + file)
      log(f"{file} 已完成")

if __name__ == '__main__':
    main()
```

AndresG 发表于 2021-9-15 11:31

学习一下

地狱猫 发表于 2021-9-29 19:57

你开发业务系统吗?现在网上有很多零代码搭建系统的平台,我们想借助这样的平台搭建业务系统,然后布置到我们自己的服务器。
页: [1]
查看完整版本: [原创][python爬虫] 利用多线程爬取漫画实例及简单反反爬