携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
青年图摘
在个人的项目里面有一个对于图文素材进行获取的需求,于是需要在网络平台上获取图文素材。青年图摘就是其中一个,很不凑巧,该网站在2022年4月1日的时候停更了,在对网站进行分析的时候,发现图片使用的是新浪的图床,故在该网站服务停止运行的时候,有可能还能根据网址进行获取图片。所以本脚本是对图文相关数据进行存储。
分析
- 由于网站文章停止了运营,所以需求将是对网站过去的文章内容进行获取,不存在增量爬虫的设定
- 在网站资源url上,不遵循restful,也不支持显式分页,但是在每一篇文章上都拥有“上一页”超链接,故可以作为分页进行资源的获取
- 网站没有进行javascript进行渲染,requests包可直接获取源码
- 每一篇文章的主要内容位于 类选择器为
post-content的div标签下 - 在每一篇文章都拥有对图片的说明和图片的展示,分别在上述
div之中的p列表里
代码
组织架构
Crawler
|-- conf # 存放配置文件
|-- settings.py # 配置文件
|-- interial # 内部组织,存放store相关
|-- logger.py # 日志配置,使用loguru
|-- mysql.py # Mysq连接,使用pymysql
|-- redis.py # Redis连接,使用redis
|-- logs # 日志存放文件夹
|-- spider
|-- qntuzhai.py # 爬虫文件
爬虫实现
# filename: spider/qntuzhai.py
import json
import requests
from pyquery.pyquery import PyQuery as pq
from interial.redis import REDIS
from conf.settings import HEADERS
from interial.logger import logger, Secretary
def fetch(url, method='GET', **kwargs):
try:
rsp = requests.request(method, url, **kwargs)
if rsp.status_code != 200:
return False, rsp.text
return rsp, "请求成功"
except Exception as e:
return False, e
@Secretary
def get_page(url):
logger.info(f"Get Page: {url}")
rsp, msg = fetch(url, headers=HEADERS)
if not rsp:
logger.error(msg)
return
doc = pq(rsp.text)
ps = doc('div.post-content p').items()
for p in ps:
text = p.text().split("】")[-1]
image_src = p("img").attr("src")
item = {"title": text, "url": image_src}
REDIS.lpush("crawler:recreator:item", json.dumps(item, ensure_ascii=False))
logger.info(f"Push item: {item}")
next_page = doc("div.prev-post a").attr("href")
if next_page:
next_page = f"https://www.qingniantuzhai.com{next_page}"
return get_page(next_page)
return
if __name__ == '__main__':
url = "https://www.qingniantuzhai.com/qing-nian-tu-zhai-0331-4/"
get_page(url)
运行结果:
...
2022-08-09 20:16:14.805 | INFO | __main__:get_page:33 - Push item: {'title': '伤害不高,侮辱极强', 'url': 'https://wx4.sinaimg.cn/mw1024/0037UN1agy1gv59npxr2sg60990c04qw02.gif'}
2022-08-09 20:16:14.846 | INFO | __main__:get_page:33 - Push item: {'title': '做的不正宗了,狗狗都开始感兴趣了', 'url': 'https://wx1.sinaimg.cn/mw1024/002iRMxrly1gv6tyft68mg607v08s7wi02.gif'}
...
# 共计 9343条
其他文件
Redis
# filename: interial/redis.py
import redis
from conf.settings import REDIS_AUTH, REDIS_HOST, REDIS_PORT, REDIS_DB, USE_REDIS
REDIS_CONFIG = {
'host': REDIS_HOST or "127.0.0.1", # 数据库地址
'port': REDIS_PORT or 6379, # 端口号
'db': REDIS_DB or 0, # 数据库名称
'password': REDIS_AUTH or "", # 数据库密码
}
class RedisPool:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = object.__new__(cls)
return cls._instance
_redis_pool_data = redis.ConnectionPool(decode_responses=True, **REDIS_CONFIG)
# 实例化redis
@classmethod
def get_conn(cls):
conn = redis.Redis(connection_pool=cls._redis_pool_data)
return conn
REDIS = RedisPool.get_conn() if USE_REDIS else None
logger
import os
from datetime import datetime
from loguru import logger
from conf.settings import LOG_DIR
now = datetime.now()
file_name = os.path.join(LOG_DIR, f"crawler_{now.year}_{now.month}_{now.day}.log")
logger.add(file_name, format="{time} {level} {message}", level="INFO", rotation="10 MB", encoding="utf-8", enqueue=True)
class Secretary(object):
"""
黑盒日志记录器:数据数据 > (执行) > 数据输出
"""
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
res = self.func(*args, **kwargs)
logger.info(f'{{"func": "{self.func.__name__}", "params": "{args, kwargs}", "res": "{res}"}}')
依赖
async-timeout==4.0.2
certifi==2022.6.15
charset-normalizer==2.1.0
cssselect==1.1.0
Deprecated==1.2.13
fake-useragent==0.1.11
idna==3.3
jsonpath==0.82
loguru==0.6.0
lxml==4.9.1
packaging==21.3
py==1.11.0
PyMySQL==1.0.2
pyparsing==3.0.9
pyquery==1.4.3
redis==4.3.4
requests==2.28.1
urllib3==1.26.11
wrapt==1.14.1