改出个豆瓣书籍导出工具

微信图片_20220318004337

1. 属主

多少年以来,就不断传说豆瓣要倒掉了,大家都担心里面辛苦记录的看过哪本书看过哪部电影,就都化为飞灰。所以我也关注过豆坟这样的工具,能把你走过的路导出来。后来,听说这些工具被封的被查的,各种说法,也都是传说。只是豆瓣还没有倒掉,也许正如比萨斜塔,倒掉的过程漫长到不知不觉它已经变了,早已低于你一次次定下的底线,而你居然仍能接受。这应该不算忘却初心,算与时俱进吧。

虽说,只有忘掉了以后还在深刻影响你的,才是真正的修养。然而,当你准备吹牛说某某书在你成长的过程中曾经让你喝了三天大酒每天早晨逃课,但是那位作家那本书的名字就在嘴边却偏偏想不起来了。”就是那本,你知道的,那本特有名的,叫什么什么,哎,一时没想起来。”说服力大打折扣,真是尴尬。那个令你夜不能寐泪湿枕套的人,居然忘了名字,说起来谁能信有过这几滴泪呢。

再说,你走过的路,你标过的电影和书,说到底,它是谁的呢。在饭店结账,有一次我不知怎么犯了混——经常有,每次不同,其他的以后再讲给你——非想把小票拿走。起因可能就是对方那句”这个我们要留着”,我就一定要拿走。他一句我们有规定,又一句这个得入账,又一句给你个礼物,又一句你可以拍照,还有啥来着。他的每一句后面,我都只有一个问题,”这是你的还是我的?”

你标过的电影和书,说到底,它是谁的。

教师上课,领导不必打招呼随便进课堂检查,我也挺想知道,这课堂是谁的。谁,才应该是主人,谁,才有责任有主人翁意识。我就猜有人会说,课堂是学生的,学生才是主人。那么,你经过主人同意了么。得这么请示,”某同学,受我的上级领导委派,明天我不得不去旁听您所在班级的某门课,就是教师小那谁那堂。仰慕已久,正好可以当堂观摩您当堂指出教师授课问题时的风彩。您看是否合适?”如果有闲空,也可以对教师指出,”小那谁,明天本大人要去听你的课啊,提前耳提面命令尔知哓,是莫大的恩情呐!”我等即应”着”。如果没有闲空,发个通知,”本座可能突然莅临监听尔等任何一人任何一堂,望周知”,即可。甚至不必等”着”这个确认。要知道如果你有了”着”的责任,就意味着还有不”着”的权利。鲁迅先生所说的有钱人对我喊完”滚”,可是无须我表达听到或同意的。多么威风。

微信图片_20220318004330

当然,一切都是为了属主。属主喜欢什么就是什么,喜欢谁就是谁。

鉴权系统中有个概念,叫做 属主。属主真是说删就删,如果只读,我就把你的只读改掉还是要删。但是,得是属主。属主成天担心标过的书记过的电影正上着的课,这能算属主么。

非单身的同学请注意,你的收入可不是你一个人的。即,你并非属主。根据法律规定,你的收入归你和你的配偶共同所有。共同所有,你并非唯一的属主,因此也就不是属主。并非独占,即并非拥有。

出于对于并非独占而自以为拥有的战战兢心理吧,所以,爬豆瓣,把属于自己的数据取回来,这样的工具、方法、操作层出不穷。也是奇观。说起来,爬数据违法,爬自己的数据违法不?

微信图片_20220318004333

2. 爬

怎么爬呢。

正则表达式?这绝对是个好办法。正则表达式,等价于乔姆斯基3型文法,描述能力相当于有穷自动机。那是相当强悍。也算小众,不然乔姆斯基应该已经封杀了。说起来,如果乔姆斯基封杀了,正则表达式是不是不株连不足以平民愤呢。

不过,有比正则表达式更适合于 爬 这个具体需求的。杀鸡焉用牛刀,图灵机肯定行,图灵机干啥都行。但是回答”如果发贴子”,你不能说”用图灵机”,得用浏览器区别于微信,才好。

从网页或者类似的结果数据,而不是理解为可执行的代码,从结果数据中抽取文本的时候,匹配的特征描述可以用xpath获取dom节点。其关键思想是在 内容(如html中的字符串) 与 形式 (如css的字体)分离 的情况下,通过形式 (css符合哪个class,什么颜色,第几个li标签或者第几个p标签)找到内容 (一段文字)。

我们可以 (随便什么方法) 写个爬虫。然后

第一步,读到网页。

第二步,用xpath分析dom,从中提取到想要的信息,比如 我读过的书目列表,从中取出 书名、作者-出版社-出版时间-码洋、标注时间。记录这些信息到本地。或者放在你的脑子里,总之,你确定无疑地具有属主权限的地方。

不要放在云端!童话故事里恶魔把自己的心脏交给善良的小媳妇保管,坏人总是有那么一刻心软了,唉。后来你看到有几本书消失了,或者成了”未知书籍”,就像有的”我的网盘”上的东西突然没了,我一按键盘,那不是”我的”网盘么?难道,这就像”我的理发师””我的老师””我的母校”那种,其实它并不像”我的缺点””我的负债””我未完成的任务”那样是”我的”?这书没了的时候,你却在吹牛的时候刚好想不起来那几本对你影响深远的书的名字是什么,那就是单纯吹牛了。

第三步,还是刚才那个网页,用xpath分析dom,找到”下一页”那个链接。

第四步,http get “下一步” 的 url。

第五步,跳转到第一步。直到找不到”下一页”了,那么就是到了最后一页。

无标题也可以到这时,把前面每一页的每个你想保留的字段存储到本地。不要放在云端!

3. 工具

以上步骤,可以用 python,也可以用世界上最好的语言 php。凡是支持 http get的,并且支持 xpath dom 的,一般也都支持写入本地,都可以。

我前两天翻 firefox 优秀插件 tampermonkey 的脚本时,发现了一个好东西,豆瓣电影标注的导出工具。名字是 豆瓣电影导出工具,[https://kisexu.com/]这位大侠写的,发布在这里 [https://openuserjs.org/scripts/KiseXu/豆瓣电影导出工具]。

我想,有导出电影的,肯定还有导出书籍的吧。没找到。都这么懒呢。后来到了每日三省吾身的时候想,圣人日,行有不得 (并、不是这个意思),反求诸己 。对啊,找不到东西,自己写啊。正好有这么像的,动手改啊。我就改了一个。

Kisexu大侠的代码流畅,容易读懂,与上述如何爬的思路一致,不必修改。kisexu大侠用了 jquery 求 xpath。电影和书籍页面的xpath不一样,我得每页看看,改改。对 jquery 语法不熟悉,还顺便复习/预习了一下。在这里学的,[https://www.w3school.com.cn/jquery/jquery_ref_traversing.asp]。

微信图片_20220318004336

写完了跑了一下,效果还不错。刚好1234本书,有意思的数,算是个纪念。

4. 使用

需要用 firefox 浏览器,安装 tampermonkey 插件。

在 tampermonkey 的 dashboard 中新增脚本,删除原有示例代码(ctrl-a del),然后把以下代码贴进去。保存。

接着在访问豆瓣时,”我的豆瓣”,我读 的右边除了 在读、想读、读过、书单,多了个”导出读过书籍”链接。

微信图片_20220318004343点这个链接,firefox 会翻你标记了读过的书,每页15本,一页页翻下去。全翻完以后,产生一个excel (后缀csv)请你下载。这个excel就是你读过的书籍了。
微信图片_20220318004339

微信图片_20220318004341

tampermonkey 脚本如下。再次感谢kisexu大侠。

// ==UserScript==
// @name         豆瓣已读书籍导出工具
// @namespace    younggift.net
// @version      0.1
// @description  将豆瓣已读图书导出为csv文件。启用本脚本,进入豆瓣个人页面后,在『我读』部分会有一链接『导出读过书籍』,点击即可。无需登录,支持导出任意用户已读书籍。
// @author       younggift
// @copyright 2022, younggift; 2018, KiseXu (https://kisexu.com)
// @license MIT
// @match        https://book.douban.com/people/*/collect*
// @match        https://www.douban.com/people/*
// @require      https://unpkg.com/dexie@latest/dist/dexie.js
// @grant        none
// ==/UserScript==

// ==OpenUserJs==
// @author KiseXu
// ==/OpenUserJs==

(function() {
    'use strict';

    // 页面触发部分
    if (location.href.indexOf('//www.douban.com/') > -1) {
        // 加入导出按钮
        var people = location.href.slice(location.href.indexOf('/people') + 8, -1);
        var export_link = 'https://book.douban.com/people/' + people + '/collect?sort=time&start=0&filter=all&mode=grid&tags_sort=count&export=1'; //列表URL 及'&export=1'标记
        $('#book h2:first .pl a:last').after('&nbsp;·&nbsp;<a href="'+export_link+'">导出读过书籍</a>') //根据css定位
    }

    if (location.href.indexOf('//book.douban.com/') > -1 && location.href.indexOf('export=1') > -1) {
        // 开始导出
        getPage();
    }


    // 获取当前页数据
    function getCurrentPageList() {
        var items = [];
        $('li.subject-item').each(function(index) {
            items[index] = {
                title: $(this).find('h2').find('a').text().trim(),
                pub: $(this).find('.pub').text().trim(),
                mark_date: $(this).find('.date').text().replace(/读过/, '').trim(),
                link: $(this).find('h2').find('a').attr('href').trim(),
                // 学习了 https://www.w3school.com.cn/jquery/jquery_ref_traversing.asp
            };
//             alert("here");
//             alert(items[index].title);
//             alert(items[index].pub);
//             alert(items[index].mark_date);
//             alert(items[index].link);
    });

        return items;

    }

    // 采集当前页数据,保存到indexedDB
    function getPage() {
        const db = new Dexie('db_export');
        db.version(1).stores({
            items: `++id, title, pub, mark_date, link`
        });
        var items = getCurrentPageList();
        db.items.bulkAdd(items).then (function(){
            console.log('保存成功');
            // 获取下一页链接
            var next_link = $('span.next a').attr('href');  //todo 是否正确?
            if (next_link) {
                next_link = next_link + '&export=1';

                window.location.href = next_link;
            } else {
                exportAll()
            }
        }).catch(function(error) {
            console.log("Ooops: " + error);
        });

    }

    // 导出所有数据到CSV
    function exportAll() {
        const db = new Dexie('db_export');
        db.version(1).stores({
            items: `++id, title, pub, mark_date, link`
        });
        db.items.orderBy('mark_date').toArray().then(function(all){
            all = all.map(function(item,index,array){
                delete item.id;
                return item;
            })

            JSonToCSV.setDataConver({
                data: all,
                fileName: 'book',
                columns: {
                    title: ['书名', '出版信息', '标记时间','链接'],
                    key: ['title', 'pub', 'mark_date', 'link']
                }
            });
            db.delete();
        });
    }

    //以下未修改 todo
    // 导出CSV函数
    // https://github.com/liqingzheng/pc/blob/master/JsonExportToCSV.js
    var JSonToCSV = {
    /*
     * obj是一个对象,其中包含有:
     * ## data 是导出的具体数据
     * ## fileName 是导出时保存的文件名称 是string格式
     * ## showLabel 表示是否显示表头 默认显示 是布尔格式
     * ## columns 是表头对象,且title和key必须一一对应,包含有
          title:[], // 表头展示的文字
          key:[], // 获取数据的Key
          formatter: function() // 自定义设置当前数据的 传入(key, value)
     */
    setDataConver: function(obj) {
      var bw = this.browser();
      if(bw['ie'] < 9) return; // IE9以下的
      var data = obj['data'],
          ShowLabel = typeof obj['showLabel'] === 'undefined' ? true : obj['showLabel'],
          fileName = (obj['fileName'] || 'UserExport') + '.csv',
          columns = obj['columns'] || {
              title: [],
              key: [],
              formatter: undefined
          };
      ShowLabel = typeof ShowLabel === 'undefined' ? true : ShowLabel;
      var row = "", CSV = '', key;
      // 如果要现实表头文字
      if (ShowLabel) {
          // 如果有传入自定义的表头文字
          if (columns.title.length) {
              columns.title.map(function(n) {
                  row += n + ',';
              });
          } else {
              // 如果没有,就直接取数据第一条的对象的属性
              for (key in data[0]) row += key + ',';
          }
          row = row.slice(0, -1); // 删除最后一个,号,即a,b, => a,b
          CSV += row + '\r\n'; // 添加换行符号
      }
      // 具体的数据处理
      data.map(function(n) {
          row = '';
          // 如果存在自定义key值
          if (columns.key.length) {
              columns.key.map(function(m) {
                  row += '"' + (typeof columns.formatter === 'function' ? columns.formatter(m, n[m]) || n[m] : n[m]) + '",';
              });
          } else {
              for (key in n) {
                  row += '"' + (typeof columns.formatter === 'function' ? columns.formatter(key, n[key]) || n[key] : n[key]) + '",';
              }
          }
          row.slice(0, row.length - 1); // 删除最后一个,
          CSV += row + '\r\n'; // 添加换行符号
      });
      if(!CSV) return;
      this.SaveAs(fileName, CSV);
    },
    SaveAs: function(fileName, csvData) {
      var bw = this.browser();
      if(!bw['edge'] || !bw['ie']) {
        var alink = document.createElement("a");
        alink.id = "linkDwnldLink";
        alink.href = this.getDownloadUrl(csvData);
        document.body.appendChild(alink);
        var linkDom = document.getElementById('linkDwnldLink');
        linkDom.setAttribute('download', fileName);
        linkDom.click();
        document.body.removeChild(linkDom);
      }
      else if(bw['ie'] >= 10 || bw['edge'] == 'edge') {
        var _utf = "\uFEFF";
        var _csvData = new Blob([_utf + csvData], {
            type: 'text/csv'
        });
        navigator.msSaveBlob(_csvData, fileName);
      }
      else {
        var oWin = window.top.open("about:blank", "_blank");
        oWin.document.write('sep=,\r\n' + csvData);
        oWin.document.close();
        oWin.document.execCommand('SaveAs', true, fileName);
        oWin.close();
      }
    },
    getDownloadUrl: function(csvData) {
      var _utf = "\uFEFF"; // 为了使Excel以utf-8的编码模式,同时也是解决中文乱码的问题
      if (window.Blob && window.URL && window.URL.createObjectURL) {
          csvData = new Blob([_utf + csvData], {
              type: 'text/csv'
          });
          return URL.createObjectURL(csvData);
      }
      // return 'data:attachment/csv;charset=utf-8,' + _utf + encodeURIComponent(csvData);
    },
    browser: function() {
      var Sys = {};
      var ua = navigator.userAgent.toLowerCase();
      var s;
      (s = ua.indexOf('edge') !== - 1 ? Sys.edge = 'edge' : ua.match(/rv:([\d.]+)\) like gecko/)) ? Sys.ie = s[1]:
          (s = ua.match(/msie ([\d.]+)/)) ? Sys.ie = s[1] :
          (s = ua.match(/firefox\/([\d.]+)/)) ? Sys.firefox = s[1] :
          (s = ua.match(/chrome\/([\d.]+)/)) ? Sys.chrome = s[1] :
          (s = ua.match(/opera.([\d.]+)/)) ? Sys.opera = s[1] :
          (s = ua.match(/version\/([\d.]+).*safari/)) ? Sys.safari = s[1] : 0;
      return Sys;
    }
  };

})();

Leave a Reply

Your email address will not be published.