返回 Python
Python
23 分钟阅读

Python 爬虫

「先观察页面规律,再写选择器」的思路

我所理解的爬虫,本质是针对网络中直接对外呈现的内容,梳理其数据传输链路与展示规律,进而解析提取其中核心价值数据的技术手段。

例子一:

  • 若我想快速掌握小红书每日的内容推送逻辑,可先查看其首页 —— 内容以列表形式呈现,每个卡片对应一条笔记内容。
  • 我可以通过爬虫获取每个卡片的核心信息(如标题 Title 和封面图 Cover Image),再结合大数据分析、深度学习等方法,挖掘这些内容的价值,或是反推自己在小红书平台的用户画像特征。

例子二:

  • 有时会遇到这类情况:某个网站的界面(UI)设计不符合使用习惯,但其承载的数据却无可替代;同时该网站未开放 API 接口,无法通过正规渠道获取数据源,也就无法自行搭建 Web 前端或移动应用,来更高效、直观地分析数据并辅助决策。
  • 这时就可以借助爬虫工具,在合规前提下抓取网站前端呈现的结构化、有规律的数据,并将其存储到本地介质(如文件、SQLite 数据库等)。
  • 后续可将这些数据接入自研的 Web 项目中(既可以是 Python Web + Jinja2 这类前后端不分离的架构,也可以基于 Spring Boot 封装为 Restful API,再对接 Vue/React 前端展示 —— 具体技术选型可灵活调整,并非核心)。
  • 结合自定义的 UI 设计,能大幅提升大脑对数据的分析效率,辅助做出更精准的决策。

那么针对这个工具,就是我们要使用的爬虫了。

Python 爬虫工作流:观察页面规律 → requests 发起请求 → BeautifulSoup 解析选择器 → 提取结构化数据 → 本地存储

为什么用Python

  • 从本质上看,爬虫是通过自动化方式模拟用户访问网站 / 移动应用,再按照预设的规则提取指定内容的程序(这是爬虫的基础定义;更前沿的爬虫会结合深度学习实现数据的自主学习与提取,这部分暂不展开)。
  • 从技术实现角度,任何编程语言都能完成爬虫开发,比如 Java、JavaScript 等均具备相应能力。
  • 但实际应用中存在关键差异:Java、JavaScript 等语言的爬虫生态,没有 Python 这么完善。Python 拥有 Beautiful Soup 这类功能成熟、易用性极高的爬虫专用库,能大幅降低开发成本。
  • 更重要的是,Python 语法简洁易懂:同样的功能,Java 可能需要数十行代码实现,而 Python 调用第三方库往往几行就能完成。且爬虫任务大多属于脚本级需求,而非大型工程化开发 —— 它通常无需搭建庞大的服务集群,对工程化规范的要求相对宽松,往往一段 50 行以内的代码就能完成完整的爬取、解析、存储流程。
  • 尽管爬虫的实现不局限于 Python,但凭借生态优势和开发效率,Python 成为了业界实现爬虫任务的主流选择。
  • 还有一点是:我在个人Profile上面提到过,我自身的原因:我先是一名Python开发者,最早学习的开发语言就是Python,后面才是Kotlin。出于个人的技术背景,写脚本的事情,我第一个想法一定是Python。

Python 爬虫经常使用的几个核心工具

ToolsEffectedScene
requests发起 HTTP/HTTPS 请求爬取静态网页、接口数据
BeautifulSoup4解析 HTML/XML 提取数据静态网页数据解析
lxml高性能 HTML/XML 解析复杂网页解析
Scrapy爬虫框架(整合请求 / 解析 / 存储)大规模、结构化爬取
Selenium/Playwright模拟浏览器行为爬取动态渲染网页
  • 基础的爬虫步骤,就是可以通过requests实现一个网络请求的内容。
    • 这里就需要注意一下网站方可能会做的一些tricks,比如你缺少Cookie、Content-Type、Auth等等内容,它会返回错误的状态给你,那么就需要去真实请求中,拿到这些数据,再通过Python模拟请求发送出去
    • 比如可以通过Edge、Chrome这些浏览器,去请求网页,然后通过F12查看每一个请求携带的Header,找到欠缺的内容,然后在Python上通过Header传递上去。
  • 拿到数据之后:
    • 如果是Restful API的内容,我们会得到一个JSON内容(通常这类的数据,就是交给分离式的前端项目去fetch,或者交给移动端通过Retrofit2去解析)
    • 而如果是直接呈现给用户观看到的Web内容,大抵就是一个html内容。这个时候,就可以去分析得到的这个HTML网页,如何按照一个规律性的角度,把核心的数据,拿出来,存起来,然后进一步去分析数据背后的商业逻辑。

实际案例

  • 之前看尤雨溪很经典的一个段子,就是很多人在那里喋喋不休讨论React比Vue更加好,然后尤雨溪忍不住回怼了一句:“你解决生理需求的那个网站Pornhub,都是用Vue去搭建的”。
  • 这是一个契机吧,然后我就顺着这一句话作为线索,去专门找文章寻找到了各种黄色网站,并且去看了一下他们的技术实现方案。
  • 在这个过程中,我发现很多黄色网站,他们的前端门户做得真的是很让人绷不住,简单到你很随意就能找到他的规律,然后去使用代码去自动化、流水线过程把他的内容获取出来,然后以新的交互形式去展示。

注意:实操此案例,需要保证网络可以访问Google,可以采用ShadowsRock、V2RayNG、ClashX等等方案,也可以参考路由器翻墙方案

我所选择要去爬取资源的网站是:JavBus

  • 警告⚠️:千万不要在公开场合打开上面的那个Link。

他们的url格式,真的很让人绷不住的简单,他们的格式就是:

link = "https://www.javbus.com/KBI-001"
# 规律如下
link = host_name + "/" + series + "-" + code
  • 这种格式,就是他们的域名,然后加上番号,就是一个资源链接了。

  • 它的Cover Image的区域,是在:

<div class="col-md-9 screencap">
    <a class="bigImage" href="/pics/cover/6qah_b.jpg">
      <img src="/pics/cover/6qah_b.jpg" title="KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ">
    </a>
</div>
  • 而针对他们的样品图片这个区域,则是:
<div id="sample-waterfall">
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-1.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_1.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 1">
    </div>
  </a>        
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-2.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_2.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 2">
    </div>
  </a>        
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-3.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_3.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 3">
    </div>
  </a>        
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-4.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_4.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 4">
    </div>
  </a>       
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-5.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_5.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 5">
    </div>
  </a>        
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-6.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_6.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 6">
    </div>
  </a>        
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-7.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_7.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 7">
    </div>
  </a>        
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-8.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_8.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 8">
    </div>
  </a>        
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-9.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_9.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 9">
    </div>
  </a>        
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-10.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_10.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 10">
    </div>
  </a>       
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-11.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_11.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 11">
    </div>
  </a>        
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-12.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_12.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 12">
    </div>
  </a>       
  <a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-13.jpg">
    <div class="photo-frame">
      <img src="/pics/sample/6qah_13.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 13">
    </div>
  </a>            
</div>

这种格式简直就是爬虫初学者的天堂了,都不需要使用Selenium/Playwright这两个东西了,直接一个requests + BeautifulSoup简单解析就行了。

所以,具体的Python代码如下所示

  • 配置文件
  • series_image_cover_config.py
series = "KBI"
start_code = 1
end_code = 100
  • 实际解析以及下载、程序主入口的内容
  • series_image_cover_download.py
import os
import asyncio
import aiohttp
from bs4 import BeautifulSoup
import urllib3
 
from series_image_cover_config import *
 
# 禁用 InsecureRequestWarning 警告
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
 
host = "https://www.javbus.com"
 
HEADERS = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "zh-CN,zh;q=0.9",
}
 
async def download_image_cover(session: aiohttp.ClientSession, name: str, series: str):
    URL = f"{host}/{name}"
    SAVE_DIR = f"./{series}"
    os.makedirs(SAVE_DIR, exist_ok=True)
 
    try:
        async with session.get(URL, timeout=aiohttp.ClientTimeout(total=10), ssl=False) as response:
            if response.status != 200:
                print(f"[{name}] 页面不存在,跳过")
                return
        
            html = await response.text()
            soup = BeautifulSoup(html, "html.parser")
 
            title = soup.find("title")
            cover_tag = soup.select_one("a.bigImage img")
 
            if not cover_tag:
                print(f"[{name}] 无封面,跳过")
                return
 
            cover_url = cover_tag["src"]
            if cover_url.startswith("/"):
                cover_url = host + cover_url
 
            ext = os.path.splitext(cover_url)[1]
            cover_path = os.path.join(SAVE_DIR, f"{name}{ext}")
 
            async with session.get(
                cover_url, 
                headers={"Referer": URL, **HEADERS}, 
                ssl=False
            ) as img_response:
                if img_response.status == 200:
                    with open(cover_path, "wb") as f:
                        f.write(await img_response.read())
                    
                    print(f"[{name}] 下载成功 → {cover_path}")
                    print(f"[{name}] 标题:{title.text.strip() if title else '无标题'}")
                else:
                    print(f"[{name}] 图片下载失败,状态码:{img_response.status}")
 
    except asyncio.TimeoutError:
        print(f"[{name}] 请求超时,跳过")
    except Exception as e:
        print(f"[{name}] 发生错误:{e}")
        return
 
async def start_to_download():
    names = [
        f"{SERIES}-{code:03d}" for code in range(max(1, START_CODE), END_CODE)
    ]
    
    connector = aiohttp.TCPConnector(limit=10)
    async with aiohttp.ClientSession(
        connector=connector,
        headers=HEADERS
    ) as session:
 
        tasks = [download_image_cover(session, name, SERIES) for name in names]
        
        batch_size = 10
        for i in range(0, len(tasks), batch_size):
            batch = tasks[i:i+batch_size]
            await asyncio.gather(*batch)
            await asyncio.sleep(0.5)  # 防止请求太快,被他们知道这是来自脚本的下载解析行为
 
if __name__ == "__main__":
    asyncio.run(start_to_download())

程序运行方式

python ./series_image_cover_download.py
  • 上面的爬取方式,是针对一个大系列的番号进行爬取的。

  • 番号这个概念,就是某某厂商的一个系列,有点像买手机,比如三星手机,它会有外折叠机、内折叠机、直屏机等等,一一对应到Galaxy Z-Fold系列、Galaxy Z-Flip系列和Galaxy S系列。

  • 那么相应地在这里,黄色电影本身就是一个三星手机概念,而番号则是如同手机系列的概念,比如KBI什么的,而具体的片号,则是有番号 + 数字组成,比如 KBI-002 这种格式。

  • 那么针对想下载某一个片的所有详情图,就是解析样品图片这个区域的内容,那么具体的代码如下所示:

  • special_image_detail_config.py

videos = [
  {
    "name" : "KBI-001",
  },
  {
    "name": "KBI-002",
  }
]
  • special_image_detail_download.py
import os
import requests
from bs4 import BeautifulSoup
 
host = "https://www.javbus.com"
 
def image_download(names, onlyFolderImage = False):
    for name in names:
        URL = host + "/" + name
        SAVE_DIR = "./{}".format(name)
 
        HEADERS = {
            "authority": "www.javbus.com",
            "method": "GET",
            "path": f"/{name}",
            "scheme": "https",
            "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
            "accept-encoding": "gzip, deflate, br, zstd",
            "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
            "cookie": "PHPSESSID=nnelcojatbmsjh2s02i4m0fno4; existmag=mag; _tea_utm_cache_10000007=undefined",
            "dnt": "1",
            "priority": "u=0, i",
            "sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Microsoft Edge";v="140"',
            "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": '"macOS"',
            "sec-fetch-dest": "document",
            "sec-fetch-mode": "navigate",
            "sec-fetch-site": "none",
            "sec-fetch-user": "?1",
            "upgrade-insecure-requests": "1",
            "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                        "AppleWebKit/537.36 (KHTML, like Gecko) "
                        "Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0"
        }
 
        os.makedirs(SAVE_DIR, exist_ok=True)
 
        session = requests.Session()
        response = session.get(URL, headers=HEADERS)
        soup = BeautifulSoup(response.text, "html.parser")
 
        title_tag = soup.find("title")
        title = title_tag.text.strip() if title_tag else "unknown_title"
        print("页面标题:", title)
 
        cover_tag = soup.select_one("a.bigImage img")
        if cover_tag:
            cover_url = cover_tag["src"]
            if cover_url.startswith("/"):
                cover_url = host + cover_url
            ext = os.path.splitext(cover_url)[1]
            cover_path = os.path.join(SAVE_DIR, f"cover{ext}")
            cover_path = cover_path.replace("cover.jpg", "folder.jpg")
            
            cover_headers = HEADERS.copy()
            cover_headers["referer"] = URL
            
            r = session.get(cover_url, headers=cover_headers, stream=True)
            with open(cover_path, "wb") as f:
                for chunk in r.iter_content(1024):
                    f.write(chunk)
            print("封面图已下载:", cover_path)
 
        if not onlyFolderImage:
            sample_tags = soup.select("a.sample-box")
            if not sample_tags:
                print("没有样本图")
            else:
                for idx, a_tag in enumerate(sample_tags, 1):
                    img_url = a_tag.get("href")
                    if not img_url:
                        continue
                    ext = os.path.splitext(img_url)[1]
                    img_path = os.path.join(SAVE_DIR, f"sample_{idx}{ext}")
                    r = session.get(img_url, headers=HEADERS, stream=True)
                    with open(img_path, "wb") as f:
                        for chunk in r.iter_content(1024):
                            f.write(chunk)
                    print(f"样本图 {idx} 已下载:", img_path) 
 
if __name__ == "__main__":
    from special_image_detail_config import videos
    
    image_download([video["name"] for video in videos])

程序运行方式

python ./special_image_detail_download.py
  • 对于电影的下载部分
    • 一般来说,类似像JavBus这些地方,都会提供一些磁力链接,一般来说直接可以使用Motrix去进行下载,但是Motrix也是存在很大的问题,特别是下载这种不太合法的影片的时候,他的下载速度堪忧。
    • 两种解决方案:
      • 其一是采用:
        • 夸克网盘或者迅雷的会员版 去下载,虽然他们的云盘也是会屏蔽这一类的影片资源,但是你下载你的本地,这是没有任何问题的。
      • 第二种方式是:
        • 很多人会去Miss AV这种聚合平台观看这一类的影片,Miss AV的影片一般是采用M3U8的格式输出的,于是 ->
        • 我提供了另一个工具,可以下载这一类的M3U8的视频,详细可以观看这一篇关于M3U下载的文章

更多玩法

  • 如果你也会写一下前端Web代码,甚至乎来说你都不需要会React或者Vue,就是简单的HTML就行,那么你可以顺便实现一个瀑布流照片墙,然后把爬下来的数据做成输入图片。 img.png

这个是我使用Python Flask + (CSS + HTML + JavaScript) 直接写的。

后面觉得闲着也是闲着,不如把自己所有做过的内容以及碎片化的想法,做得更完善一些,于是又写了一个Go + Gin + Bootstrap的小型Web项目,通过Vibe Coding的形式,把上面的那段Python代码迁移到了使用Go去实现,在Web层直接使用表格的形式去实现抓取数据,并新增了一个图库展示的页面。 img2.png img3.png

说明

    1. 别沉迷在这里面的黄色网站的实际内容上,这些内容只是猎奇形态的产物。以上只是作为技术文章分享一下针对他们网站的简单爬取行为,仅供学习使用,不做商业向的任何技术性行为分享。另外本人不对这里面的实际内容做任何负责,这也不代表我实际的价值观取向。技术是无罪的
    1. 最近在研究Android的逆向工程,怎么样去抓取一些来自Mobile端的API数据,并且实现拦截行为。
    • 这是一条很建议入门的路线,一些项目如果存在技术难题,可以去看竞品的解决方案。
    • 产品经理经常说,别人家的应用怎么比我们的应用稳定这么多呢?这个时候,可以爬出数据,直接放出来,大家都是讲道理的人,Talk is cheap, show you my data.
    1. 可以探索更多的玩法是:
    • 3.1 怎么样去爬取一些公众号里面优秀的文章,或者类似于小红书去爬取一些博主所分享的图文内容,然后保存下来,存进去本地中(可以是SQLite或者TXT都行,本地持久化存储方案)。
    • 3.2 然后这些文章作为原始数据,进行下一轮的训练,拟合出一个更加强大的AI形态,实现观点的快速输出以及信息的快速匹配。