利用爬虫建立自己的图片软件(一)

vue文档已经看了一遍又一遍,一直想拿个项目练练手。想了又想不如就写个图片站吧,复杂的也搞不来~~嘿嘿。图片站需要图片支撑,自己上传怕是不可能,直接爬吧。百度了几个图片网站然后开干。

预期功能

  • 图组浏览
  • 图组收藏,点赞
  • 用户个人中心
  • 图组自动更新
  • 图组管理

技术栈

后端:

  • blade:轻量级微服务web框架
  • jsoup:Java Html文档解析框架
  • anima:轻量级ActionRecord模式的数据库框架

前端:

  • vue
  • vuex:vue状态管理插件
  • vue-router:vue路由组件
  • axios:js网络请求库
  • muse-ui:vue前端框架

数据库:

  • sqlite

项目开发(后端部分)

一、数据库设计

根据图片站结构,定义一下三个数据表:

  • 图片类型表:
@EqualsAndHashCode(callSuper = false)
@Data
@ToString
@Table(name = "img_type")
public class ImgType extends Model {
    private Integer id;
    @JsonIgnore
    private String host;   // 分类所属网站 *
    @JsonIgnore
    private String uri;    // 分类标题 英文 *
    private String name;   // 分类名 *

    @JsonIgnore
    private String  pageUriReg; // 分页打开规则如: "list_1_$.html" $: 页码 *
    private Integer totalPages; // 网站上的 该分类下总页数 *
    private Integer pageSize;   // 网站上的 分页尺寸
    private Integer pageNum;    // 已经加载的页码 默认0 *
    private String  time;

    /**
     * 获取本页url
     * @param page 页码
     */
    public String getUrl(int page) {
        if (page > totalPages) return null;
        if (page > 1) {
            return host + uri + pageUriReg.replace("$", page + "");
        }
        return host + uri;
    }
}
  • 图组表
@EqualsAndHashCode(callSuper = false)
@Data
@Table(name = "img_group", pk = "uuid")
@ToString
public class ImgGroup extends Model {
    private String  uuid;  // not null
    @JsonIgnore
    private String  host;   // 来源网站
    @JsonIgnore
    private String  uri;    // 网络图片组路径
    private Integer typeId; // 图片组类型id
    private String  type;   // 图片组类型名
    private String  title;  // 图片组标题

    @JsonIgnore
    private String  coverNetPath;  // 图片组封面网络地址 not null
    private String  coverUuid;// 封面图片的uuid
    private String  time;     // 图片组添加时间
    private Integer views;    // 图片组查看次数
    private Integer total;    // 该图片组图片数

    public String getUrl() {
        return host + uri;
    }
}
  • 图片表
@EqualsAndHashCode(callSuper = false)
@Data
@Table(name = "img_info", pk = "uuid")
public class ImgInfo extends Model {
    private String uuid;
    private String groupId;  // 图片组id,图片组的uuid

    @Ignore
    private ImgGroup group;
    private String   name;   // 图片名
    @JsonIgnore
    private String netPath;  // 图片网络地址

    @JsonIgnore
    private String  path;    // 图片本地地址
    private String  time;
    private Integer views;
}

二、确定爬取目标,编写爬虫接口

之前就爬过妹子图的图片,这次并不打算只爬一个网站,这样图片量太少。因此就要有一个统一的爬虫接口,以便于将不同网站整合在一起。

/**
 * @AUTHOR soft
 * @DATE 2018/9/20 23:01
 * @DESCRIBE 图片爬取器基础接口
 */
public interface ImgCrawlerBase {

    /**
     * 获取目标网站地址
     */
    String getHost();
    
    /**
     * 获取网站下所有图片分类
     */
    List<ImgType> getTypes();

    /**
     * 获取图片组
     * 页码要是对应网站的分页形式的页码
     * @param type 图片组类型
     * @param page 页码
     */
    List<ImgGroup> getGroups(@NonNull ImgType type, int page);

    /**
     * 分页获取图片,降低响应时间
     * 根据图片组获取指定页码的图片
     * @param group 图片组
     */
    List<ImgInfo> getImages(@NonNull ImgGroup group, int page);

    /**
     * 图片搜索
     * @param keyword 搜索关键字
     */
    ImgSearch search(String keyword, int page);

    /**
     * 下载图片
     * @param webPath 图片网络地址
     */
    static String downImage(String webPath) {
        Logger log = LoggerFactory.getLogger(ImgCrawlerBase.class);
        Optional<Connection.Response> execute = RequestKit.of().execute(webPath);
        if (execute.isPresent()) {
            Connection.Response response = execute.get();
            String filename = FileUtils.getFilenameByNetPath(webPath);
            byte[] bytes    = response.bodyAsBytes();
            try {
                return Files.write(new File(Const.TEMP, filename).toPath(), bytes)
                        .toAbsolutePath().toString();
            } catch (IOException e) {
                log.error("保存文件失败! {}", e.getMessage());
            }
        }
        return null;
    }
}

定义一个爬虫程序获取工具

/**
 * @AUTHOR soft
 * @DATE 2018/9/21 1:08
 * @DESCRIBE 根据网址获取对应的爬取工具
 */
@Slf4j
public class CrawlerFactory {
    private static List<ImgCrawlerBase> crawlers = new ArrayList<>();

    static {
        crawlers = getImgCrawlerClasses().stream().map(cls -> {
            try {
                return (ImgCrawlerBase) cls.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                log.error("获取实例失败! {}, 返回默认实例: Mzitu", e.getMessage());
                return new Mzitu();
            }
        }).collect(Collectors.toList());
    }

    public static List<ImgCrawlerBase> getImgCrawlers() {
        return crawlers;
    }

    public static ImgCrawlerBase getImgCrawlerRandom() {
        int i = new Random().nextInt(3);
        return crawlers.get(i);
    }

    public static ImgCrawlerBase getImgCrawlerByRemove(List<ImgCrawlerBase> crawlers) {
        int i = new Random().nextInt(crawlers.size());
        return crawlers.remove(i);
    }

    /**
     * 获取对应网站的爬取工具
     * @param host 网址
     */
    public static ImgCrawlerBase getImgCrawler(String host) {
        Optional<ImgCrawlerBase> first = getImgCrawlers().stream().filter(crawler -> {
            return host.equals(crawler.getHost());
        }).findFirst();
        return first.orElse(null);
    }

    private static List<Class> getImgCrawlerClasses() { 
        return Arrays.asList(Mzitu.class, I99mm.class, Mm131.class);
    }
}

三、为app提供接口

提供json格式数据,便于前端展示

/**
 * @AUTHOR soft
 * @DATE 2018/9/20 21:13
 * @DESCRIBE 接口
 */
@Path(value = "/api", restful = true)
public class ApiController {
    @Inject
    private ImgTypeService typeService;
    @Inject
    private ImgGroupService groupService;
    @Inject
    private ImgInfoService infoService;

    @Route("/titles")
    public RestResponse title(Request request) {
        List<ImgType> list = typeService.findList();
        if (list != null && !list.isEmpty()) {
            return RestResponse.ok(list);
        }
        return RestResponse.fail();
    }

    @Route("/group")
    public RestResponse group(@Param(defaultValue = "1") Integer page,
                              @Param(defaultValue = "10") Integer size,
                              @Param(defaultValue = "性感美女") String type) {
        Page<ImgGroup> page1 = groupService.page(page, size, type);
        if (page1 != null) {
            return RestResponse.ok(page1);
        }
        return RestResponse.fail();
    }

    @Route("/images")
    public RestResponse images(@Param(defaultValue = "1") Integer page,
                               @Param(defaultValue = "10") Integer size,
                               @Param String gid) {
        Page<ImgInfo> page1 = infoService.page(page, size, gid);
        if (page1 != null) {
            return RestResponse.ok(page1);
        }
        return RestResponse.fail();
    }

    @Route("/search")
    public RestResponse search(@Param String key,
                               @Param(defaultValue = "1") Integer page,
                               @Param(defaultValue = "10") Integer size) {
        Page<ImgGroup> search = groupService.search(key, page, size);
        if (search != null) {
            return RestResponse.ok(search);
        }
        return RestResponse.fail();
    }
}

后端基本框架就是这样。未完待续...
最终效果图

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342