Repository: dataabc/weiboSpider Branch: master Commit: 720d52a58aef Files: 80 Total size: 255.0 KB Directory structure: gitextract_3r6f4gjt/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ ├── failed.md │ │ ├── feature-request.md │ │ └── other.md │ ├── stale.yml │ └── workflows/ │ └── python-app.yml ├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── docs/ │ ├── FAQ.md │ ├── academic.md │ ├── automation.md │ ├── contributors.md │ ├── cookie.md │ ├── example.md │ ├── known_issues.md │ ├── settings.md │ └── userid.md ├── requirements.txt ├── setup.py ├── tests/ │ ├── __init__.py │ ├── test_downloader_async.py │ ├── test_parser/ │ │ ├── __init__.py │ │ ├── test_album_parser.py │ │ ├── test_comment_parser.py │ │ ├── test_index_parser.py │ │ ├── test_info_parser.py │ │ ├── test_mblog_picAll_parser.py │ │ ├── test_page_parser.py │ │ ├── test_photo_parser.py │ │ └── util.py │ └── testdata/ │ ├── 2f62165fa3ca1e85e0d398d385c377a068b76eb95765f7020ffffd3e.html │ ├── 4957814af5a123b82e974b5537dea736dfb34e48d8835203a45d2e67.html │ ├── 4d5ed0a3ebd0303cb45edd544dbc0ab5e86d43e103405f0c60515884.html │ ├── 63a98849ec82b2c87ec55bca03cbf5988f7eac233a23d86b4fdf5ffd.html │ ├── 76233b3f90394581aac6f19cfa5d674a610e8b442b1f83de7673ab49.html │ ├── a4437630f3bdfa2757bae1595186ac063fe5ec25cf2f98116ece83cb.html │ ├── b541fd1751117498b6d6f40d3321686ddf871651237c4ac854a5c3eb.html │ ├── ca5f2a555e8d62f728c66fa90afb2d54d19f8c898e164204a61bdf03.html │ ├── d486235d4a17dd0accb0f2cc77b3648abfa03580b9e0cdb61f1e618f.html │ ├── e4d541ecb02253c14abc1d52605fc00d91279df9ac4c1465c85b91b3.html │ ├── e97222acd5bc7d8d1bfbd3f352f8cad3e36fdd19e40b69e1c33fb3c3.html │ └── url_map.json └── weibo_spider/ ├── __init__.py ├── __main__.py ├── config_sample.json ├── config_util.py ├── datetime_util.py ├── downloader/ │ ├── __init__.py │ ├── avatar_picture_downloader.py │ ├── downloader.py │ ├── img_downloader.py │ ├── origin_picture_downloader.py │ ├── retweet_picture_downloader.py │ └── video_downloader.py ├── logging.conf ├── parser/ │ ├── __init__.py │ ├── album_parser.py │ ├── comment_parser.py │ ├── index_parser.py │ ├── info_parser.py │ ├── mblog_picAll_parser.py │ ├── page_parser.py │ ├── parser.py │ ├── photo_parser.py │ └── util.py ├── spider.py ├── user.py ├── user_id_list.txt ├── weibo.py └── writer/ ├── __init__.py ├── csv_writer.py ├── json_writer.py ├── kafka_writer.py ├── mongo_writer.py ├── mysql_writer.py ├── post_writer.py ├── sqlite_writer.py ├── txt_writer.py └── writer.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: Bug报修 about: 向程序开发者申报bug title: '' labels: bug assignees: '' --- 感谢您申报bug,为了表示感谢,如果bug确实存在,您将出现在本项目的贡献者列表里;如果您不但发现了bug,还提供了很好的解决方案,我们会邀请您以pull request的方式成为本项目的代码贡献者(Contributor);如果您多次提供很好的pull request,我们将邀请您成为本项目的协助者(Collaborator)。当然,是否提供解决方按都是自愿的。不管是否是真正的bug、是否提供解决方案,我们都感谢您对本项目的帮助。 - 问:请您指明哪个版本出了bug(github版/PyPi版/全部)? 答: - 问:您使用的是否是最新的程序(是/否)? 答: - 问:爬取任意用户都会复现此bug吗(是/否)? 答: - 问:若只有爬特定微博时才出bug,能否提供出错微博的weibo_id或url(非必填)? 答: - 问:若您已提供出错微博的weibo_id或url,可忽略此内容,否则能否提供出错账号的**user_id**及您配置的**since_date**,方便我们定位出错微博(非必填)? 答: - 问:如果方便,请您描述bug详情,如果代码报错,最好附上错误提示。 答: ================================================ FILE: .github/ISSUE_TEMPLATE/failed.md ================================================ --- name: 程序运行出错 about: 运行出错,需要帮助 title: '' labels: failed assignees: '' --- 为了更好的解决问题,请认真回答下面的问题。等到问题解决,请及时关闭本issue。 - 问:请您指明哪个版本运行出错(github版/PyPi版/全部)? 答: - 问:您使用的是否是最新的程序(是/否)? 答: - 问:爬取任意用户都会运行出错吗(是/否)? 答: - 问:若只有爬特定微博时才出错,能否提供出错微博的weibo_id或url(非必填)? 答: - 问:若您已提供出错微博的weibo_id或url,可忽略此内容,否则能否提供出错账号的**user_id**及您配置的**since_date**,方便我们定位出错微博(非必填)? 答: - 问:如果方便,请您描述出错详情,最好附上错误提示。 答: ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.md ================================================ --- name: 新需求或建议 about: 建议开发新功能,或虽然没有新需求但对本项目有其它建议 title: '' labels: 'feature' assignees: '' --- - 问:请说明需要什么新功能。 答: - 问:请说明添加该功能的意义。(非必填) 答: ================================================ FILE: .github/ISSUE_TEMPLATE/other.md ================================================ --- name: 其它问题 about: 其它想讨论的问题 title: '' labels: '' assignees: '' --- ================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an issue becomes stale daysUntilStale: 60 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - pinned - security - to do # Set to true to ignore issues with an assignee exemptAssignees: true # Label to use when marking an issue as stale staleLabel: wontfix # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: > Closing as stale, please reopen if you'd like to work on this further. # Limit to only `issues` or `pulls` only: issues ================================================ FILE: .github/workflows/python-app.yml ================================================ # This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python application on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.8 uses: actions/setup-python@v2 with: python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | pytest ================================================ FILE: .gitignore ================================================ .vscode *.pyc __pycache__ build/ dist/ *.egg-info config.json weibo/ weibo.db *.log .idea ================================================ FILE: CONTRIBUTING.md ================================================ # 为本项目做贡献 本项目使用**Python3**编写,感谢大家对项目的支持,也欢迎大家为开源项目做贡献。鉴于大家拥有不同的技能、经验、认知、时间等,每个人可以根据自身的情况为本项目贡献力量。我们不会因为贡献者写的代码少或者提的建议不好而失去感恩之心,每一个乐于奉献的人都值得并且应该被尊重。所以,如果您觉得自己的代码或建议不好,而不好意思去贡献,这样可能就让本项目失去了一次变得更好的机会。所以,如果您有好的想法、建议,或者发现了bug,欢迎通过issue提出来,这也是一种贡献方式。如果您想要为本项目贡献代码,我们也非常欢迎。最开始您可以通过pull request方式提交代码,如果我们发现您的代码质量非常高,或者非常有想法等,我们会邀请您请成为本项目的协作者([Collaborator](https://help.github.com/cn/github/setting-up-and-managing-your-github-user-account/permission-levels-for-a-user-account-repository#collaborator-access-on-a-repository-owned-by-a-user-account)),这样您就可以直接向本项目提交代码了。在您贡献代码之前,请先阅读下面的说明,这会让您更好的贡献代码。 ## 贡献代码之前 如果要开发新功能或者其它需要大量编写代码的修改,在开发之前最好发Issue说明一下。比如,“我准备开发xx新功能”或者“我想修改xx功能”之类的。因为要开发的功能不一定适合本项目,所以提前说明讨论,判断新功能或修改是否有必要。否则,费时费力写了很多代码,结果最后没有被采纳,可能会做一些无用功。 ## Python风格规范(建议Python新手阅读) 参考[Python风格规范](https://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/python_style_rules/) 或者[Python风格规范](https://github.com/zh-google-styleguide/zh-google-styleguide/blob/master/google-python-styleguide/python_style_rules.rst), 二者内容是一样的。 ## git提交规范 参考[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 或者[Git提交规范](https://zhuanlan.zhihu.com/p/67804026),commit描述中文英文皆可,只要符合规范就好。 ## git提交建议(可选) 本建议是可选的,如果你觉得不合理,可以按自己的方式编写代码。建议每次提交都是代码改动较少的提交,如果新功能需要大量修改代码,建议将新功能分成几个小模块,每个模块提交一次。原因是这样更容易管理代码。比如,一个新功能包含几个模块。其中大部分模块都写的很好,但是有一个模块有bug。分模块提交只需要单独处理出问题的模块,其他模块不受影响。 ## Python之linter 本项目使用flake8。 ## Python之formatter 本项目使用yapf。 ## 引号的使用 代码中**建议使用单引号**,只有在特殊情况下使用双引号,如类、方法、函数等开头的注释使用6个双引号包裹(注释左边三个双引号,右边三个双引号),或者字符串中中已经包含单引号了,则要用双引号包裹。 ## 避免过多的模块依赖 除非有必要,尽量少使用非内置的模块,因为会增加用户的安装成本,当然如果该模块能够为本项目或用户带来很多便利,则可以使用。 ================================================ FILE: README.md ================================================ [![Build Status](https://github.com/dataabc/weiboSpider/workflows/Python%20application/badge.svg)](https://badge.fury.io/py/weibo-spider) [![Python](https://img.shields.io/pypi/pyversions/weibo-spider)](https://badge.fury.io/py/weibo-spider) [![PyPI](https://badge.fury.io/py/weibo-spider.svg)](https://badge.fury.io/py/weibo-spider) # Weibo Spider 本程序可以连续爬取**一个**或**多个**新浪微博用户(如[胡歌](https://weibo.cn/u/1223178222)、[迪丽热巴](https://weibo.cn/u/1669879400)、[郭碧婷](https://weibo.cn/u/1729370543))的数据,并将结果信息写入**文件**或**数据库**。写入信息几乎包括用户微博的所有数据,包括**用户信息**和**微博信息**两大类。因为内容太多,这里不再赘述,详细内容见[获取到的字段](#获取到的字段)。如果只需要用户信息,可以通过设置实现只爬取微博用户信息的功能。本程序需设置cookie来获取微博访问权限,后面会讲解[如何获取cookie](#如何获取cookie)。如果不想设置cookie,可以使用[免cookie版](https://github.com/dataabc/weibo-crawler),二者功能类似。 爬取结果可写入文件和数据库,具体的写入文件类型如下: - **txt文件**(默认) - **csv文件**(默认) - **json文件**(可选) - **MySQL数据库**(可选) - **MongoDB数据库**(可选) - **SQLite数据库**(可选) 同时支持下载微博中的图片和视频,具体的可下载文件如下: - **原创**微博中的原始**图片**(可选) - **转发**微博中的原始**图片**(可选) - **原创**微博中的**视频**(可选) - **转发**微博中的**视频**(可选) - **原创**微博**Live Photo**中的**视频**([免cookie版](https://github.com/dataabc/weibo-crawler)特有) - **转发**微博**Live Photo**中的**视频**([免cookie版](https://github.com/dataabc/weibo-crawler)特有) ## 内容列表 [TOC] - [Weibo Spider](#weibo-spider) - [内容列表](#内容列表) - [获取到的字段](#获取到的字段) - [用户信息](#用户信息) - [微博信息](#微博信息) - [示例](#示例) - [运行环境](#运行环境) - [使用说明](#使用说明) - [0.版本](#0版本) - [1.安装程序](#1安装程序) - [源码安装](#源码安装) - [pip安装](#pip安装) - [2.程序设置](#2程序设置) - [3.运行程序](#3运行程序) - [个性化定制程序(可选)](#个性化定制程序可选) - [定期自动爬取微博(可选)](#定期自动爬取微博可选) - [如何获取cookie](#如何获取cookie) - [如何获取user_id](#如何获取user_id) - [常见问题](#常见问题) - [学术研究](#学术研究) - [相关项目](#相关项目) - [贡献](#贡献) - [贡献者](#贡献者) - [注意事项](#注意事项) ## 获取到的字段 本部分为爬取到的字段信息说明,为了与[免cookie版](https://github.com/dataabc/weibo-crawler)区分,下面将两者爬取到的信息都列出来。如果是免cookie版所特有的信息,会有免cookie标注,没有标注的为二者共有的信息。 ### 用户信息 - 用户id:微博用户id,如"1669879400",其实这个字段本来就是已知字段 - 昵称:用户昵称,如"Dear-迪丽热巴" - 性别:微博用户性别 - 生日:用户出生日期 - 所在地:用户所在地 - 学习经历:用户上学时学校的名字和时间 - 工作经历:用户所属公司名字和时间 - 阳光信用(免cookie版):用户的阳光信用 - 微博注册时间(免cookie版):用户微博注册日期 - 微博数:用户的全部微博数(转发微博+原创微博) - 关注数:用户关注的微博数量 - 粉丝数:用户的粉丝数 - 简介:用户简介 - 主页地址(免cookie版):微博移动版主页url - 头像url(免cookie版):用户头像url - 高清头像url(免cookie版):用户高清头像url - 微博等级(免cookie版):用户微博等级 - 会员等级(免cookie版):微博会员用户等级,普通用户该等级为0 - 是否认证(免cookie版):用户是否认证,为布尔类型 - 认证类型(免cookie版):用户认证类型,如个人认证、企业认证、政府认证等 - 认证信息:为认证用户特有,用户信息栏显示的认证信息 ### 微博信息 - 微博id:微博唯一标志 - 微博内容:微博正文 - 头条文章url:微博中头条文章的url,若微博中不存在头条文章,则值为'' - 原始图片url:原创微博图片和转发微博转发理由中图片的url,若某条微博存在多张图片,每个url以英文逗号分隔,若没有图片则值为"无" - 视频url: 微博中的视频url,若微博中没有视频,则值为"无" - 微博发布位置:位置微博中的发布位置 - 微博发布时间:微博发布时的时间,精确到分 - 点赞数:微博被赞的数量 - 转发数:微博被转发的数量 - 评论数:微博被评论的数量 - 微博发布工具:微博的发布工具,如iPhone客户端、HUAWEI Mate 20 Pro等 - 结果文件:保存在当前目录weibo文件夹下以用户昵称为名的文件夹里,名字为"user_id.csv"和"user_id.txt"的形式 - 微博图片:原创微博中的图片和转发微博转发理由中的图片,保存在以用户昵称为名的文件夹下的img文件夹里 - 微博视频:原创微博中的视频,保存在以用户昵称为名的文件夹下的video文件夹里 - 微博bid(免cookie版):为[免cookie版](https://github.com/dataabc/weibo-crawler)所特有,与本程序中的微博id是同一个值 - 话题(免cookie版):微博话题,即两个#中的内容,若存在多个话题,每个url以英文逗号分隔,若没有则值为'' - @用户(免cookie版):微博@的用户,若存在多个@用户,每个url以英文逗号分隔,若没有则值为'' - 原始微博(免cookie版):为转发微博所特有,是转发微博中那条被转发的微博,存储为字典形式,包含了上述微博信息中的所有内容,如微博id、微博内容等等 ## 示例 如果想要知道程序的具体运行结果,可以查看[示例文档](https://github.com/dataabc/weiboSpider/blob/master/docs/example.md),该文档介绍了爬取[迪丽热巴微博](https://weibo.cn/u/1669879400)的例子,并附有部分结果文件截图。 ## 运行环境 - 开发语言:python2/python3 - 系统: Windows/Linux/macOS ## 使用说明 ### 0.版本 本程序有两个版本,你现在看到的是python3版,另一个是python2版,python2版位于[python2分支](https://github.com/dataabc/weiboSpider/tree/python2)。目前主力开发python3版,包括新功能开发和bug修复;python2版仅支持bug修复。推荐python3用户使用当前版本,推荐python2用户使用[python2版](https://github.com/dataabc/weiboSpider/tree/python2),本使用说明是python3版的使用说明。 ### 1.安装程序 本程序提供两种安装方式,一种是**源码安装**,另一种是**pip安装**,二者功能完全相同。如果你需要修改源码,建议使用第一种方式,否则选哪种安装方式都可以。 #### 源码安装 ```bash $ git clone https://github.com/dataabc/weiboSpider.git $ cd weiboSpider $ pip install -r requirements.txt ``` #### pip安装 ```bash $ python3 -m pip install weibo-spider ``` ### 2.程序设置 要了解程序设置,请查看[程序设置文档](https://github.com/dataabc/weiboSpider/blob/master/docs/settings.md)。 ### 3.运行程序 **源码安装**的用户可以在weiboSpider目录运行如下命令,**pip安装**的用户可以在任意有写权限的目录运行如下命令 ```bash $ python3 -m weibo_spider ``` 第一次执行,会自动在当前目录创建config.json配置文件,配置好后执行同样的命令就可以获取微博了。 如果你已经有config.json文件了,也可以通过config_path参数配置config.json路径,运行程序,命令行如下: ```bash $ python3 -m weibo_spider --config_path="config.json" ``` 如果你想指定文件(csv、txt、json、图片、视频)保存路径,可以通过output_dir参数设定。假如你想把文件保存到/home/weibo/目录,可以运行如下命令: ```bash $ python3 -m weibo_spider --output_dir="/home/weibo/" ``` 如果你想通过命令行输入user_id,可以使用参数u,可以输入一个或多个user_id,每个user_id以英文逗号分开,如果这些user_id中有重复的user_id,程序会自动去重。命令行如下: ```bash $ python3 -m weibo_spider --u="1669879400,1223178222" ``` 程序会获取user_id分别为1669879400和1223178222的微博用户的微博,后面会讲[如何获取user_id](#如何获取user_id)。该方式的所有user_id使用config.json中的since_date和end_date设置,通过修改它们的值可以控制爬取的时间范围。若config.json中的user_id_list是文件路径,每个命令行中的user_id都会自动保存到该文件内,且自动更新since_date;若不是路径,user_id会保存在当前目录的user_id_list.txt内,且自动更新since_date,若当前目录下不存在user_id_list.txt,程序会自动创建它。 ## 个性化定制程序(可选) 本部分为可选部分,如果不需要个性化定制程序或添加新功能,可以忽略此部分。 本程序主体代码位于weibo_spider.py文件,程序主体是一个 Spider 类,上述所有功能都是通过在main函数调用 Spider 类实现的,默认的调用代码如下: ```python config = get_config() wb = Spider(config) wb.start() # 爬取微博信息 ``` 用户可以按照自己的需求调用或修改 Spider 类。通过执行本程序,我们可以得到很多信息。
点击查看详情 - wb.user['nickname']:用户昵称; - wb.user['gender']:用户性别; - wb.user['location']:用户所在地; - wb.user['birthday']:用户出生日期; - wb.user['description']:用户简介; - wb.user['verified_reason']:用户认证; - wb.user['talent']:用户标签; - wb.user['education']:用户学习经历; - wb.user['work']:用户工作经历; - wb.user['weibo_num']:微博数; - wb.user['following']:关注数; - wb.user['followers']:粉丝数;
**wb.weibo**:除不包含上述信息外,wb.weibo包含爬取到的所有微博信息,如**微博id**、**微博正文**、**原始图片url**、**发布位置**、**发布时间**、**发布工具**、**点赞数**、**转发数**、**评论数**等。如果爬的是全部微博(原创+转发),除上述信息之外,还包含被**转发微博原始图片url**、**是否为原创微博**等。wb.weibo是一个列表,包含了爬取的所有微博信息。wb.weibo[0]为爬取的第一条微博,wb.weibo[1]为爬取的第二条微博,以此类推。当filter=1时,wb.weibo[0]为爬取的第一条**原创**微博,以此类推。wb.weibo[0]['id']为第一条微博的id,wb.weibo[0]['content']为第一条微博的正文,wb.weibo[0]['publish_time']为第一条微博的发布时间,还有其它很多信息不在赘述,大家可以点击下面的"详情"查看具体用法。
详情 若目标微博用户存在微博,则: - id:存储微博id。如wb.weibo[0]['id']为最新一条微博的id; - content:存储微博正文。如wb.weibo[0]['content']为最新一条微博的正文; - article_url:存储微博中头条文章的url。如wb.weibo[0]['article_url']为最新一条微博的头条文章url,若微博中不存在头条文章,则值为''; - original_pictures:存储原创微博的原始图片url和转发微博转发理由中的图片url。如wb.weibo[0]['original_pictures']为最新一条微博的原始图片url,若该条微博有多张图片,则存储多个url,以英文逗号分割;若该微博没有图片,则值为"无"; - retweet_pictures:存储被转发微博中的原始图片url。当最新微博为原创微博或者为没有图片的转发微博时,则值为"无",否则为被转发微博的图片url。若有多张图片,则存储多个url,以英文逗号分割; - publish_place:存储微博的发布位置。如wb.weibo[0]['publish_place']为最新一条微博的发布位置,如果该条微博没有位置信息,则值为"无"; - publish_time:存储微博的发布时间。如wb.weibo[0]['publish_time']为最新一条微博的发布时间; - up_num:存储微博获得的点赞数。如wb.weibo[0]['up_num']为最新一条微博获得的点赞数; - retweet_num:存储微博获得的转发数。如wb.weibo[0]['retweet_num']为最新一条微博获得的转发数; - comment_num:存储微博获得的评论数。如wb.weibo[0]['comment_num']为最新一条微博获得的评论数; - publish_tool:存储微博的发布工具。如wb.weibo[0]['publish_tool']为最新一条微博的发布工具。
## 定期自动爬取微博(可选) 要想让程序每隔一段时间自动爬取,且爬取的内容为新增加的内容(不包括已经获取的微博),请查看[定期自动爬取微博](https://github.com/dataabc/weiboSpider/blob/master/docs/automation.md)。 ## 如何获取cookie 要了解获取cookie方法,请查看[cookie文档](https://github.com/dataabc/weiboSpider/blob/master/docs/cookie.md)。 ## 如何获取user_id 要了解获取user_id方法,请查看[user_id文档](https://github.com/dataabc/weiboSpider/blob/master/docs/userid.md),该文档介绍了如何获取一个及多个微博用户user_id的方法。 ## 常见问题 如果运行程序的过程中出现错误,可以查看[常见问题](https://github.com/dataabc/weiboSpider/blob/master/docs/FAQ.md)页面,里面包含了最常见的问题及解决方法。另一方面,由于当前项目所使用的技术或API的局限性,我们已知某些情况无法处理或某些需求无法实现,已将其整理总结在了[已知问题](https://github.com/dataabc/weiboSpider/blob/master/docs/known_issues.md)。除此之外,如果您在程序使用过程中遇到与预期不符的行为,可以通过[发issue](https://github.com/dataabc/weiboSpider/issues/new/choose)寻求帮助,我们会很乐意为您解答。 ## 学术研究 本项目通过获取微博数据,为写论文、做研究等非商业项目提供所需数据。[学术研究文档](https://github.com/dataabc/weiboSpider/blob/master/docs/academic.md)是一些在论文或研究等方面使用过本程序的项目,这些项目展示已征得所有者同意。在一些涉及隐私的描述上,已与所有者做了沟通,描述中只介绍所有者允许展示的部分。如果部分信息所有者之前同意展示并且已经写在了文档中,现在又不想展示了,可以通过邮件(chillychen1991@gmail.com)或issue的方式告诉我,我会删除相关信息。同时,也欢迎使用本项目写论文或做其它学术研究的朋友,将自己的研究成果展示在[学术研究文档](https://github.com/dataabc/weiboSpider/blob/master/docs/academic.md)里,这完全是自愿的。 为方便大家引用,现提供本项目的 bibtex 条目如下: ``` @misc{weibospider2020, author = {Lei Chen, Zhengyang Song, schaepher, minami9, bluerthanever, MKSP2015, moqimoqidea, windlively, eggachecat, mtuwei, codermino, duangan1}, title = {{Weibo Spider}}, howpublished = {\url{https://github.com/dataabc/weiboSpider}}, year = {2020} } ``` ## 相关项目 - [weibo-crawler](https://github.com/dataabc/weibo-crawler) - 功能和本项目完全一样,可以不添加cookie,获取的微博属性更多; - [weibo-search](https://github.com/dataabc/weibo-search) - 可以连续获取一个或多个**微博关键词搜索**结果,并将结果写入文件(可选)、数据库(可选)等。所谓微博关键词搜索即:**搜索正文中包含指定关键词的微博**,可以指定搜索的时间范围。对于非常热门的关键词,一天的时间范围,可以获得**1000万**以上的搜索结果,N天的时间范围就可以获得1000万 X N搜索结果。对于大多数关键词,一天产生的相应微博数量应该在1000万条以下,因此可以说该程序可以获得大部分关键词的全部或近似全部的搜索结果。而且该程序可以获得搜索结果的所有信息,本程序获得的微博信息该程序都能获得。 ## 贡献 欢迎为本项目贡献力量。贡献可以是提交代码,可以是通过issue提建议(如新功能、改进方案等),也可以是通过issue告知我们项目存在哪些bug、缺点等,具体贡献方式见[为本项目做贡献](https://github.com/dataabc/weiboSpider/blob/master/CONTRIBUTING.md)。 ## 贡献者 感谢所有为本项目贡献力量的朋友,贡献者详情见[贡献者](https://github.com/dataabc/weiboSpider/blob/master/docs/contributors.md)页面。 ## 注意事项 1. user_id不能为爬虫微博的user_id。因为要爬微博信息,必须先登录到某个微博账号,此账号我们姑且称为爬虫微博。爬虫微博访问自己的页面和访问其他用户的页面,得到的网页格式不同,所以无法爬取自己的微博信息;如果想要爬取爬虫微博内容,可以参考[获取自身微博信息](https://github.com/dataabc/weiboSpider/issues/113); 2. cookie有期限限制,大约三个月。若提示cookie错误或已过期,需要重新更新cookie。 ================================================ FILE: docs/FAQ.md ================================================ # 常见问题 ## 1. 程序运行出错,错误提示中包含“ImportError: cannot import name 'config_util' from '__main__'”,如何解决? 出现这种错误,说明使用者很可能是直接运行的.py文件,程序正确的运行方式是在weiboSpider目录下,运行如下命令: ```bash python3 -m weibo_spider ``` ## 2. 程序运行出错,错误提示中包含“'NoneType' object”字样,如何解决? 这是最常见的问题之一。出错原因是爬取速度太快,被暂时限制了,限制可能包含爬虫账号限制和ip限制。一般情况下,一段时间后限制会自动解除。可通过降低爬取速度避免被限制,具体修改config.json文件中的如下代码: ```json "random_wait_pages": [1, 5], "random_wait_seconds": [6, 10], "global_wait": [[1000, 3600], [500, 2000]], ``` 前两行的意思是每爬取1到5页,随机等待6到10秒。可以通过加快暂停频率(减小random_wait_pages内的值)或增加等待时间(加大random_wait_seconds内的值)避免被限制。最后一行的意思是获取1000页微博,一次性等待3600秒;之后获取500页微博一次性等待2000秒。默认只有两个global_wait配置([1000, 3600]和[500, 2000]),可以添加更多个,也可以自定义。当配置使用完,如默认配置在获取1500(1000+500)页微博后就用完了,之后程序会从第一个配置开始循环使用(获取第1501页到2500页等待3600秒,获取第2501页到第3000页等待2000秒,以此类推)。 ## 3. 程序运行出错,错误提示中包含“browser_cookie3.BrowserCookieError: Unable to get key for cookie decryption”字样,如何解决?[issue619](https://github.com/dataabc/weiboSpider/issues/619) 跟Google Chrome的安全策略有关,参考borisbabic/browser_cookie3#210 (comment), 实测换到Google Chrome旧版本就可以了。 ## 4. 程序运行出错,错误提示中包含“Failed to obtain weibo.cn cookie from Chrome browser: [Errno 13] Permission denied: 'xxxxxxx'”字样,如何解决?[issue621](https://github.com/dataabc/weiboSpider/issues/621) 可能程序运行时同时运行了chrome,关闭Chrome或者参考https://blog.csdn.net/weixin_43667972/article/details/132197618 ## 5. 如何获取微博评论? 因为限制,只能获取一部分评论,无法获取全部,因此暂时没有添加获取评论功能的计划。 ## 6. 有的长微博正文只能获取一部分内容,如何解决? 程序是可以获取长微博全文的。程序首先在微博列表页获取微博,如果发现长微博(正文没有显示完整,以“全文”代替部分内容的微博),会先保存这个不全的内容,然后去该长微博的详情页尝试获取全文,如果获取成功,获取的内容就是微博文本;如果获取失败,等待若干秒重新获取;如果连续尝试5次都失败,就用上面不全的内容代替。这样做的原因是避免因部分长微博获取失败而卡住。如果想尝试更多次,可以修改comment_parser.py文件get_long_weibo方法内for循环的次数。 ## 7. 如何按指定关键词获取微博? 请使用[weibo-search](https://github.com/dataabc/weibo-search)。该程序可以连续获取一个或多个微博关键词搜索结果,并将结果写入文件(可选)、数据库(可选)等。所谓微博关键词搜索即:搜索正文中包含指定关键词的微博,可以指定搜索的时间范围。对于非常热门的关键词,一天的时间范围,可以获得1000万以上的搜索结果,N天的时间范围就可以获得1000万 X N搜索结果。对于大多数关键词,一天产生的相应微博数量应该在1000万条以下,因此可以说该程序可以获得大部分关键词的全部或近似全部的搜索结果。而且该程序可以获得搜索结果的所有信息,本程序获得的微博信息该程序都能获得。 ## 8. 如何获取微博用户关注列表中用户的user_id? 请使用[weibo-follow](https://github.com/dataabc/weibo-follow)。该程序可以利用一个user_id,获取该user_id微博用户关注人的user_id,一个user_id最多可以获得200个user_id,并写入user_id_list.txt文件。程序支持读文件,利用这200个user_id,可以获得最多200X200=40000个user_id。再利用这40000个user_id可以得到40000X200=8000000个user_id,如此反复,以此类推,可以获得大量user_id。本项目也支持读文件,将上述程序的结果文件user_id_list.txt路径赋值给本项目config.json的user_id_list参数,就可以获得这些user_id用户所发布的大量微博。 ## 9. 如何获取自己的微博? 修改page_parser.py中__init__方法,将self.url修改为: ```python self.url = "https://weibo.cn/%s/profile?page=%d" % (self.user_uri, page) ``` ================================================ FILE: docs/academic.md ================================================ # 学术研究 本项目通过获取微博数据,为写论文、做研究等非商业项目提供所需数据。下面是一些在论文或研究等方面使用过本程序的项目。在一些涉及隐私的描述上,已与研究者做了沟通,在下面的描述中只介绍研究者 允许展示的部分。如果部分信息研究者之前同意展示并且已经写在了本文档中,现在又不想展示了,可以通过邮件(chillychen1991@gmail.com)或issue的方式告诉我,我会删除相关信息。同时,使用本项目写论文或做其它学术研究的朋友,如果想把自己的研究成果展示在下面,也可以通过邮件或issue的方式告诉我。 *** - 英国伦敦国王学院[Mak-LokGay](https://github.com/Mak-LokGay)的[毕业论文](https://github.com/Mak-LokGay/KCL_Dissertation) ================================================ FILE: docs/automation.md ================================================ # 定期自动爬取微博(可选) 我们爬取了微博以后,很多微博账号又可能发了一些新微博,定期自动爬取微博就是每隔一段时间自动运行程序,自动爬取这段时间产生的新微博(忽略以前爬过的旧微博)。本部分为可选部分,如果不需要可以忽略。 思路是**利用第三方软件,如crontab,让程序每隔一段时间运行一次**。因为是要跳过以前爬过的旧微博,只爬新微博。所以需要**设置一个动态的since_date**。很多时候我们使用的since_date是固定的,比如since_date="2018-01-01",程序就会按照这个设置从最新的微博一直爬到发布时间为2018-01-01的微博(包括这个时间)。因为我们想追加新微博,跳过旧微博。第二次爬取时since_date值就应该是当前时间到上次爬取的时间。 如果我们使用最原始的方式实现追加爬取,应该是这样: ```text 假如程序第一次执行时间是2019-06-06,since_date假如为2018-01-01,那这一次就是爬取从2018-01-01到2019-06-06这段时间用户所发的微博; 第二次爬取,我们想要接着上次的爬,那since_date的值应该是上次程序执行的日期,即2019-06-06 ``` 上面的方法太麻烦,因为每次都要手动设置since_date。因此我们需要动态设置since_date,即程序根据实际情况,自动生成since_date。 有两种方法实现动态更新since_date,**推荐使用方法二**。 ## 方法一:将since_date设置成整数 将config.json文件中的since_date设置成整数,如: ```json "since_date": 10, ``` 这个配置告诉程序爬取最近10天的微博,更准确说是爬取发布时间从**10天前到本程序开始执行时**之间的微博。这样since_date就是一个动态的变量,每次程序执行时,它的值就是当前日期减10。配合crontab每9天或10天执行一次,就实现了定期追加爬取。 ## 方法二:将上次执行程序的时间写入文件(推荐) 这个方法很简单,就是使用[程序设置](https://github.com/dataabc/weiboSpider/blob/master/docs/settings.md)中**设置user_id_list**的第二种方法设置user_id_list,这样设置就全部结束了。 说下这个方法的好处和原理,假如你的txt文件内容为: ```text 1669879400 1223178222 胡歌 1729370543 郭碧婷 2019-01-01 19:28 ``` 第一次执行时,因为第一行和第二行都没有写时间,程序会按照config.json文件中since_date的值爬取,第三行有时间“2019-01-01 19:28”,程序就会把这个时间当作since_date。每个用户爬取结束程序都会自动更新txt文件,每一行第一部分是user_id,第二部分是用户昵称,第三部分是程序**准备**爬取该用户第一条微博(最新微博)时的时间。爬完三个用户后,txt文件的内容自动更新为: ```text 1669879400 Dear-迪丽热巴 2020-01-13 19:18 1223178222 胡歌 2020-01-13 19:28 1729370543 郭碧婷 2020-01-13 19:33 ``` 下次再爬取微博的时候,程序会把每行的时间数据作为since_date。这样的好处一是不用修改since_date,程序自动更新;二是每一个用户都可以单独拥有只属于自己的since_date,每个用户的since_date相互独立,互不干扰。since_date既可以是“yyyy-mm-dd”格式,也可以是“yyyy-mm-dd hh:mm”格式。比如,现在又添加了一个新用户,例如杨紫,你想获取她从2018-01-23到现在的全部微博,只需要这样修改txt文件: ```text 1669879400 Dear-迪丽热巴 2020-01-13 19:18 1223178222 胡歌 2020-01-13 19:28 1729370543 郭碧婷 2020-01-13 19:33 1227368500 杨紫 2018-01-23 ``` 注意每一行的用户配置参数以空格分隔,如果第一个参数全部由数字组成,程序就认为此行为一个用户的配置,否则程序会认为该行只是注释,跳过该行;第二个参数可以为任意格式,建议写用户昵称;第三个如果是日期格式(yyyy-mm-dd),程序就将该日期设置为用户自己的since_date,否则使用config.json中的since_date爬取该用户的微博,第二个参数和第三个参数也可以不填。 推荐第二种方法,本方法是[Evifly](https://github.com/Evifly)想出的,非常热心非常有想法的网友,在此感谢。 ================================================ FILE: docs/contributors.md ================================================ # 贡献者 感谢所有为本项目作出贡献和将要作出贡献的朋友,感谢对开源事业的支持。大家每贡献一行code都让项目功能更丰富,每提一个建议都让程序更完善,每发现一个bug都让代码更健壮。 本项目贡献者包含三部分:主要代码开发者、代码贡献者和优质issue提出者。以下按贡献者的用户名首字母排序,若某贡献者在多部分都有贡献,则以主要贡献为准。 ## 主要代码开发者 | [dataabc](https://github.com/dataabc) | [songzy12](https://github.com/songzy12) | | - | - | ## 代码贡献者 | [codermino](https://github.com/codermino) | [duangan1](https://github.com/duangan1) | [MKSP2015](https://github.com/MKSP2015) | | - | - | - | ## 优质issue提出者 | | | | | | | | - | - | - | - | - | - | | [13531982270](https://github.com/13531982270) | [Archenemy61](https://github.com/Archenemy61) | [arctanx](https://github.com/arctanx) | [bossming](https://github.com/bossming) | [bubblesran](https://github.com/bubblesran) | [cangling](https://github.com/cangling)| | [Ccccche](https://github.com/Ccccche) | [Evifly](https://github.com/Evifly) | [gudaost](https://github.com/gudaost) | [Hylan129](https://github.com/Hylan129) | [HZzzzy](https://github.com/HZzzzy) | [kur0mi](https://github.com/kur0mi) | | [leonall](https://github.com/leonall) | [liu-song](https://github.com/liu-song) | [Issac110](https://github.com/Issac110) | [MengyingQian](https://github.com/MengyingQian) | [PandGnone](https://github.com/PandGnone) | [PLQin](https://github.com/PLQin) | | [redMUSCLE](https://github.com/redMUSCLE) | [shengdade](https://github.com/shengdade) | [softrime](https://github.com/softrime) | [SugimitoYuuji](https://github.com/SugimitoYuuji) | [sunbat](https://github.com/sunbat) | [taichifox95](https://github.com/taichifox95) | | [Twinklingcode](https://github.com/Twinklingcode) | [vincentlee5](https://github.com/vincentlee5) | [wiidi](https://github.com/wiidi) | [wwwpf](https://github.com/wwwpf) | [xiaomingdaily](https://github.com/xiaomingdaily) | [xiekeyi98](https://github.com/xiekeyi98) | | [xnzmc](https://github.com/xnzmc) | [yangy9593](https://github.com/yangy9593) | [zhangjibao](https://github.com/zhangjibao) | ================================================ FILE: docs/cookie.md ================================================ # 如何获取cookie 1. 用Chrome打开; 2. 输入微博的用户名、密码,登录,如图所示: ![weibo log in page](https://github.com/dataabc/media/blob/master/weiboSpider/images/cookie1.png) 登录成功后会跳转到; 3. 按F12键打开Chrome开发者工具,在地址栏输入并跳转到,跳转后会显示如下类似界面: ![chrome debugger network tab](https://github.com/dataabc/media/blob/master/weiboSpider/images/cookie2.png) 4. 依此点击Chrome开发者工具中的Network->Name中的weibo.cn->Headers->Request Headers,"Cookie:"后的值即为我们要找的cookie值,复制即可,如图所示: ![cookie in request headers section](https://github.com/dataabc/media/blob/master/weiboSpider/images/cookie3.png) ================================================ FILE: docs/example.md ================================================ # 实例 以爬取迪丽热巴的微博为例,我们需要修改**config.json**文件,文件内容如下: ```json { "user_id_list": ["1669879400"], "filter": 1, "since_date": "1900-01-01", "end_date": "now", "write_mode": ["csv", "txt", "json"], "pic_download": 1, "video_download": 1, "result_dir_name": 0, "cookie": "your cookie" } ``` 对于上述参数的含义以及取值范围,这里仅作简单介绍,详细信息见[程序设置](https://github.com/dataabc/weiboSpider/blob/master/docs/settings.md)。 - **user_id_list**代表我们要爬取的微博用户的user_id,可以是一个或多个,也可以是文件路径,微博用户Dear-迪丽热巴的user_id为1669879400,具体如何获取user_id见[如何获取user_id](https://github.com/dataabc/weiboSpider/blob/master/docs/userid.md); - **filter**的值为1代表爬取全部原创微博,值为0代表爬取全部微博(原创+转发); - **since_date**代表我们要爬取since_date日期之后发布的微博,因为我要爬迪丽热巴的全部原创微博,所以since_date设置了一个非常早的值; - **end_date**代表我们要爬取end_date日期之前发布的微博,since_date配合end_date,表示我们要爬取发布日期在since_date和end_date之间的微博,包含边界,如果end_date值为"now",表示爬取发布日期从since_date到现在的微博; - **write_mode**代表结果文件的保存类型,我想要把结果写入txt文件、csv文件和json文件,所以它的值为["csv", "txt", "json"],如果你想写入数据库,具体设置见[设置数据库](https://github.com/dataabc/weiboSpider/blob/master/docs/settings.md#设置数据库可选); - **pic_download**值为1代表下载微博中的图片,值为0代表不下载; - **video_download**值为1代表下载微博中的视频,值为0代表不下载; - **result_dir_name**控制结果文件夹名,值为1代表文件夹名是用户id,值为0代表文件夹名是用户昵称; - **cookie**是爬虫微博的cookie,具体如何获取cookie见[cookie文档](https://github.com/dataabc/weiboSpider/blob/master/docs/cookie.md),获取cookie后把"your cookie"替换成真实的cookie值即可。 cookie修改完成后在weiboSpider目录下运行如下命令: ```bash $ python3 -m weibo_spider ``` 程序会自动生成一个weibo文件夹,我们以后爬取的所有微博都被存储在这里。然后程序在该文件夹下生成一个名为"Dear-迪丽热巴"的文件夹,迪丽热巴的所有微博爬取结果都在这里。"Dear-迪丽热巴"文件夹里包含一个csv文件、一个txt文件、一个json文件、一个img文件夹和一个video文件夹,img文件夹用来存储下载到的图片,video文件夹用来存储下载到的视频。如果你设置了保存数据库功能,这些信息也会保存在数据库里,数据库设置见[设置数据库](https://github.com/dataabc/weiboSpider/blob/master/docs/settings.md#设置数据库可选)部分。 ## csv结果文件如下所示 *1669879400.csv* ![](https://github.com/dataabc/media/blob/master/weiboSpider/images/weibo_csv.png) ## txt结果文件如下所示 *1669879400.txt* ![](https://github.com/dataabc/media/blob/master/weiboSpider/images/weibo_txt.png) json文件包含迪丽热巴的用户信息和上千条微博信息,内容较多。为了表达清晰,这里仅展示两条微博。 ## json结果文件如下所示 *1669879400.json* ```json { "user": { "id": "1669879400", "nickname": "Dear-迪丽热巴", "gender": "女", "location": "上海", "birthday": "双子座", "description": "一只喜欢默默表演的小透明。工作联系jaywalk@jaywalk.com.cn 🍒", "verified_reason": "嘉行传媒签约演员", "talent": "", "education": "上海戏剧学院", "work": "嘉行传媒 ", "weibo_num": 1121, "following": 250, "followers": 66395910 }, "weibo": [ { "id": "IonM9ryMy", "content": "2019#微博之夜#盛典即将开启,以微博之力,让世界更美。1月11日,不见不散@微博之夜  原图 ", "original_pictures": "http://wx1.sinaimg.cn/large/63885668ly1gao0a01kfzj20ku112k98.jpg", "video_url": "无", "publish_place": "无", "publish_time": "2020-01-07 14:59", "publish_tool": "无", "up_num": 239242, "retweet_num": 71914, "comment_num": 55916 }, { "id": "InB4Df73X", "content": "#happyNEOyear#都到了2020,还不换点新pose配新装[來] 穿上@adidasneo 迪士尼联名款,让#生来好动#的我们一起玩“新”大发、自拍不重样🤳http://t.cn/AiF7nREj adidasneo的微博视频  ", "original_pictures": "无", "video_url": "http://f.video.weibocdn.com/000pYrGmlx07zPTskBQQ010412008AOY0E010.mp4?label=mp4_hd&template=852x480.25.0&trans_finger=62b30a3f061b162e421008955c73f536&Expires=1578569162&ssig=IV3JEbh3Zu&KID=unistore,video", "publish_place": "无", "publish_time": "2020-01-02 11:00", "publish_tool": "无", "up_num": 275419, "retweet_num": 376734, "comment_num": 131069 } ] } ``` ## 下载的图片如下所示 *img文件夹* ![](https://github.com/dataabc/media/blob/master/weiboSpider/images/img.png) 本次下载了793张图片,大小一共1.21GB,包括她原创微博中的图片和转发微博转发理由中的图片。图片名为yyyymmdd+微博id的形式,若某条微博存在多张图片,则图片名中还会包括它在微博图片中的序号。若某张图片因为网络等原因下载失败,程序则会以“weibo_id:pic_url”的形式将出错微博id和图片url写入同文件夹下的not_downloaded.txt里; ## 下载的视频如下所示 *video文件夹* ![](https://github.com/dataabc/media/blob/master/weiboSpider/images/video.png) 本次下载了70个视频,是她原创微博中的视频,视频名为yyyymmdd+微博id的形式。其中有一个视频因为网络原因下载失败,程序将它的微博id和视频url以“weibo_id:video_url”的形式写到了同文件夹下的not_downloaded.txt里。 因为我本地没有安装MySQL数据库和MongoDB数据库,所以暂时设置成不写入数据库。如果你想要将爬取结果写入数据库,只需要先安装数据库(MySQL或MongoDB),再安装对应包(pymysql或pymongo),然后将mysql_write或mongodb_write值设置为1即可。写入MySQL需要用户名、密码等配置信息,这些配置如何设置见[设置数据库](https://github.com/dataabc/weiboSpider/blob/master/docs/settings.md#设置数据库可选)部分。 ================================================ FILE: docs/known_issues.md ================================================ # 已知问题 该文档列出由于本项目所选用的技术局限而导致的已知的无法或难以在短时间内修复的问题。 ## 1. 程序无法爬取同时带有图片和视频的微博 参见:https://github.com/dataabc/weiboSpider/issues/668 具体原因如下: 当前项目的爬取实现是通过微博移动版来实现的(weibo.cn 而非 weibo.com),因为移动版的结构相对简单。 对于同时带有图片和视频的微博,在移动版的显示如下:https://weibo.cn/3384824824/Q11YMrQtB ``` #PS5##合金装备# [查看全部图片/视频] 08月22日 15:07 关注他 ``` 其中 `[查看全部图片/视频]` 只有一个链接到微博桌面版,而没有可供可直接爬取的数据: ``` 查看全部图片/视频 ``` ================================================ FILE: docs/settings.md ================================================ # 程序设置 **源码下载安装**的用户在weiboSpider目录下运行如下命令,**pip安装**的用户在任意有写权限的目录运行如下命令: ```bash $ python3 -m weibo_spider ``` 第一次运行会生成**config.json**文件,请打开**config.json**文件,你会看到如下内容: ```json { "user_id_list": ["1669879400"], "filter": 1, "since_date": "2018-01-01", "end_date": "now", "random_wait_pages": [1, 5], "random_wait_seconds": [6, 10], "global_wait": [[1000, 3600], [500, 2000]], "write_mode": ["csv", "txt"], "pic_download": 1, "video_download": 1, "result_dir_name": 0, "cookie": "your cookie", "mysql_config": { "host": "localhost", "port": 3306, "user": "root", "password": "123456", "charset": "utf8mb4" }, "sqlite_config": "weibo.db" } ``` 下面讲解每个参数的含义与设置方法。 ## 设置user_id_list user_id_list是我们要爬取的微博的id,可以是一个,也可以是多个,例如: ```json "user_id_list": ["1223178222", "1669879400", "1729370543"], ``` 上述代码代表我们要连续爬取user_id分别为“1223178222”、 “1669879400”、 “1729370543”的三个用户的微博,具体如何获取user_id见[如何获取user_id](https://github.com/dataabc/weiboSpider/blob/master/docs/userid.md)。 user_id_list的值也可以是文件路径,我们可以把要爬的所有微博用户的user_id都写到txt文件里,然后把文件的位置路径赋值给user_id_list,**推荐这种方式**。 在txt文件中,每个user_id占一行,也可以在user_id后面加注释(可选),如用户昵称等信息,user_id和注释之间必需要有空格,文件名任意,类型为txt,位置位于本程序的同目录下,文件内容示例如下: ```text 1223178222 胡歌 1669879400 迪丽热巴 1729370543 郭碧婷 ``` 假如文件叫user_id_list.txt,则user_id_list设置代码为: ```json "user_id_list": "user_id_list.txt", ``` ## 设置filter filter控制爬取范围,值为1代表爬取全部原创微博,值为0代表爬取全部微博(原创+转发)。例如,如果要爬全部原创微博,请使用如下代码: ```json "filter": 1, ``` ## 设置since_date since_date值可以是日期,也可以是整数。如果是日期,代表爬取该日期之后的微博,格式应为“yyyy-mm-dd”,如: ```json "since_date": "2018-01-01", ``` 代表爬取从2018年1月1日到现在的微博。 如果是整数,代表爬取最近n天的微博,如: ```json "since_date": 10, ``` 代表爬取最近10天的微博,这个说法不是特别准确,准确说是爬取发布时间从**10天前到本程序开始执行时**之间的微博。 **since_date是所有user的爬取起始时间,非常不灵活。如果你要爬多个用户,并且想单独为每个用户设置一个since_date,可以使用[定期自动爬取微博](https://github.com/dataabc/weiboSpider/blob/master/docs/automation.md)方法二中的方法,该方法可以为多个用户设置不同的since_date,非常灵活。** ## 设置end_date end_date值可以是日期,也可以是"now"。如果是日期,代表爬取该日期之前的微博,格式应为“yyyy-mm-dd”;如果是"now",代表爬取发布日期从since_date到现在的微博。since_date配合end_date,表示爬取发布日期在since_date和end_date之间的微博,包含边界。since_date是起始日期,end_date是结束日期,因此end_date时间应晚于since_date。注意,since_date即可以通过config.json文件的since_date参数设置,也可以通过user_id_list.txt设置;而end_date只能通过config.json文件的end_date参数设置,是全局变量,所有user_id都使用同一个end_date。 **推荐使用"now"作为end_date值**,当值为"now"时,获取结果是正确和稳定的;当end_date值不是"now"时,在爬微博数非常多的账号时,程序可能不稳定,得到很多空微博页,并且此时无法获取微博中的视频,如果想要获取视频,请为end_date赋值为"now"。 ## 设置random_wait_pages random_wait_pages值是一个长度为2的整数列表,代表每爬取x页微博暂停一次,x为整数,值在random_wait_pages列表两个整数之间随机获取。默认值为[1, 5],代表每爬取1到5页暂停一次,如果程序被限制,可以加快暂停频率,即适当减小random_wait_pages内的值。 ## 设置random_wait_seconds random_wait_seconds值是一个长度为2的整数列表,代表每次暂停sleep x 秒,x为整数, 值在random_wait_seconds列表两个整数之间随机获取。默认值为[6, 10],代表每次暂停sleep 6到10秒,如果程序被限制,可以增加等待时间,即适当增大random_wait_seconds内的值。 ## 设置global_wait global_wait控制全局等待时间,默认值为[[1000, 3600], [500, 2000]],代表获取1000页微博,程序一次性暂停3600秒;之后获取500页微博,程序再一次性暂停2000秒;之后如果再获取1000页微博,程序一次性暂停3600秒,以此类推。默认的只有前面的两个全局等待时间([1000, 3600]和[500, 2000]),可以设置多个,如值可以为[[1000, 3600], [500, 3000], [700, 3600]],程序会根据配置依次等待对应时间,如果配置全部被使用,程序会从第一个配置开始,依次使用,循环往复。 ## 设置write_mode write_mode控制结果文件格式,取值范围是csv、txt、json、mongo、mysql和sqlite,分别代表将结果文件写入csv、txt、json、MongoDB、MySQL和SQLite数据库。write_mode可以同时包含这些取值中的一个或几个,如: ```json "write_mode": ["csv", "txt"], ``` 代表将结果信息写入csv文件和txt文件。特别注意,如果你想写入数据库,除了在write_mode添加对应数据库的名字外,还应该安装相关数据库和对应python模块,具体操作见[设置数据库](https://github.com/dataabc/weiboSpider/blob/master/docs/settings.md#设置数据库可选)部分。 ## 设置pic_download pic_download控制是否下载微博中的图片,值为1代表下载,值为0代表不下载,如 ```json "pic_download": 1, ``` 代表下载微博中的图片。 ## 设置video_download video_download控制是否下载微博中的视频,值为1代表下载,值为0代表不下载,如 ```json "video_download": 1, ``` 代表下载微博中的视频。 ## 设置result_dir_name result_dir_name控制结果目录的名字,可选值为0和1,默认值为0: ```json "result_dir_name": 0, ``` 值为0表示将结果文件保存在以用户昵称为名的文件夹里,这样结果更清晰;值为1表示将结果保存在以用户id为名的文件夹里,这样更能保证多次爬取的一致性,因为用户昵称可以改变,用户id是不变的。 ## 设置cookie 请按照[如何获取cookie](https://github.com/dataabc/weiboSpider/blob/master/docs/cookie.md),获取cookie,然后将“your cookie”替换成真实的cookie值。 ## 设置mysql_config(可选) mysql_config控制mysql参数配置。如果你不需要将结果信息写入mysql,这个参数可以忽略,即删除或保留都无所谓;如果你需要写入mysql且config.json文件中mysql_config的配置与你的mysql配置不一样,请将该值改成你自己mysql中的参数配置。 ## 设置sqlite_config(可选) sqlite_config控制SQLite参数配置,代表SQLite数据库的保存路径,可根据自己需求修改。 ## 设置数据库(可选) 本部分是可选部分,如果不需要将爬取信息写入数据库,可跳过这一步。本程序目前支持MySQL数据库和MongoDB数据库,如果你需要写入其它数据库,可以参考这两个数据库的写法自己编写。 ## MySQL数据库写入 要想将爬取信息写入MySQL,请根据自己的系统环境安装MySQL,然后命令行执行: ```bash $ pip install pymysql ``` ## MongoDB数据库写入 要想将爬取信息写入MongoDB,请根据自己的系统环境安装MongoDB,然后命令行执行: ```bash $ pip install pymongo ``` connection_string是MongoDB标准URI: ```text mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]] ``` dba_name和dba_password对应URI中的username和password。如果没有访问限制可不填。 无访问限制的例子: ```json "connection_string": "mongodb://localhost:27017/weibo", ``` 使用用户名和密码的例子: ```json "connection_string": "mongodb://admin:password@localhost:27017/weibo", "dba_name": "", "dba_password": "", ``` 或 ```json "connection_string": "mongodb://localhost:27017/weibo", "dba_name": "admin", "dba_password": "password", ``` MySQL和MongDB数据库的写入内容一样。程序首先会创建一个名为"weibo"的数据库,然后再创建"user"表和"weibo"表,包含爬取的所有内容。爬取到的微博**用户信息**或插入或更新,都会存储到user表里;爬取到的**微博信息**或插入或更新,都会存储到weibo表里,两个表通过user_id关联。如果想了解两个表的具体字段,请点击"详情"。
详情 - **user表** - **id**:存储用户id,如"1669879400"; - **nickname**:存储用户昵称,如"Dear-迪丽热巴"; - **gender**:存储用户性别; - **location**:存储用户所在地; - **birthday**:存储用户出生日期; - **description**:存储用户简介; - **verified_reason**:存储用户认证; - **talent**:存储用户标签; - **education**:存储用户学习经历; - **work**:存储用户工作经历; - **weibo_num**:存储微博数; - **following**:存储关注数; - **followers**:存储粉丝数。 *** - **weibo表** - **id**:存储微博id; - **user_id**:存储微博发布者的用户id,如"1669879400"; - **content**:存储微博正文; - **article_url**:存储微博中头条文章的url,若微博中不存在头条文章,则值为''; - **original_pictures**:存储原创微博的原始图片url和转发微博转发理由中的图片url。若某条微博有多张图片,则存储多个url,以英文逗号分割;若某微博没有图片,则值为"无"; - **retweet_pictures**:存储被转发微博中的原始图片url。当最新微博为原创微博或者为没有图片的转发微博时,则值为"无",否则为被转发微博的图片url。若有多张图片,则存储多个url,以英文逗号分割; - **publish_place**:存储微博的发布位置。如果某条微博没有位置信息,则值为"无"; - **publish_time**:存储微博的发布时间; - **up_num**:存储微博获得的点赞数; - **retweet_num**:存储微博获得的转发数; - **comment_num**:存储微博获得的评论数; - **publish_tool**:存储微博的发布工具。
## 设置API接口POST联动(可选) 本部分是可选部分,如果不需要将爬取信息通过POST请求发送到指定API接口,可跳过这一步 请求数据格式为 `content-type : application/json`,接口响应返回也需要是 `content-type : application/json`,HTTP状态码为 `200` 数据主体与 `write_mode` 配置的 `json` 输出格式一致,是整页获取数据json,每页POST发送一次 `api_url` 为指定的API接口地址 `api_token` 为接口鉴权TOKEN,将在 Request Headers 中添加 `api-token` 字段,根据需要配置 ================================================ FILE: docs/userid.md ================================================ ## 如何获取user_id 1. 打开网址,搜索我们要找的人,如"迪丽热巴",进入她的主页; ![user home](https://github.com/dataabc/media/blob/master/weiboSpider/images/user_home.png) 2. 按照上图箭头所指,点击"资料"链接,跳转到用户资料页面; ![user info](https://github.com/dataabc/media/blob/master/weiboSpider/images/user_info.png) 如上图所示,迪丽热巴微博资料页的地址为"",其中的"1669879400"即为此微博的user_id。 事实上,此微博的user_id也包含在用户主页()中,之所以我们还要点击主页中的"资料"来获取user_id,是因为很多用户的主页不是""的形式,而是""或""的形式。其中"微号"和user_id都是一串数字,如果仅仅通过主页地址提取user_id,很容易将"微号"误认为user_id。 上述可以获得一个user_id,如果想要获得**大量**微博,见[如何获取大量user_id](#如何获取大量user_id)部分。 ## 如何获取大量user_id [如何获取user_id](#如何获取user_id)部分可以获得一个user_id,可以利用这一个user_id,获取该user_id微博用户关注人的user_id,一个user_id最多可以获得200个user_id,并写入user_id_list.txt文件。程序支持读文件,利用这200个user_id,可以获得最多200X200=40000个user_id。再利用这40000个user_id可以得到40000X200=8000000个user_id,如此反复,以此类推,可以获得大量user_id。本项目也支持读文件,将上述程序的结果文件user_id_list.txt路径赋值给本项目config.json的user_id_list参数,就可以获得这些user_id用户所发布的大量微博。 ================================================ FILE: requirements.txt ================================================ lxml requests==2.32.4 tqdm==4.66.3 absl-py==0.12.0 browser_cookie3==0.20.1 aiohttp ================================================ FILE: setup.py ================================================ import setuptools with open('README.md', 'r', encoding='utf-8') as fh: long_description = fh.read() setuptools.setup( name='weibo-spider', version='0.2.8', author='Chen Lei', author_email='chillychen1991@gmail.com', description='新浪微博爬虫,用python爬取新浪微博数据。', long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/dataabc/weiboSpider', packages=setuptools.find_packages(), package_data={'weibo_spider': ['config_sample.json', 'logging.conf']}, classifiers=[ 'Programming Language :: Python :: 3', 'Operating System :: OS Independent', ], install_requires=[ 'absl-py', 'lxml', 'requests', 'tqdm', ], python_requires='>=3.6', ) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/test_downloader_async.py ================================================ import asyncio import unittest from unittest.mock import MagicMock, AsyncMock, patch import os import shutil from weibo_spider.downloader.downloader import Downloader from weibo_spider.downloader.img_downloader import ImgDownloader class MockWeibo: def __init__(self): self.id = '12345' self.publish_time = '2023-10-27 10:00:00' self.media = {} self.original_pictures = 'http://example.com/pic.jpg' class TestDownloaderAsync(unittest.TestCase): def setUp(self): self.test_dir = 'tests/tmp_downloader' if not os.path.exists(self.test_dir): os.makedirs(self.test_dir) def tearDown(self): if os.path.exists(self.test_dir): shutil.rmtree(self.test_dir) def test_img_downloader(self): async def run_test(): downloader = ImgDownloader(self.test_dir, [1, 1, 1]) downloader.key = 'original_pictures' # Set key explicitly weibo = MockWeibo() mock_session = MagicMock() mock_response = AsyncMock() mock_response.status = 200 mock_response.read.return_value = b'fake_image_content' # Mock session.get to return an async context manager mock_context = AsyncMock() mock_context.__aenter__.return_value = mock_response mock_context.__aexit__.return_value = None mock_session.get.return_value = mock_context # Patch asyncio.sleep to speed up tests with patch('asyncio.sleep', AsyncMock()): # Test download_files await downloader.download_files([weibo], mock_session) # Check if file exists expected_file = os.path.join(self.test_dir, '图片', '20231027_12345.jpg') self.assertTrue(os.path.exists(expected_file), f"File {expected_file} does not exist") # Check content with open(expected_file, 'rb') as f: content = f.read() self.assertEqual(content, b'fake_image_content') asyncio.run(run_test()) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/test_parser/__init__.py ================================================ ================================================ FILE: tests/test_parser/test_album_parser.py ================================================ from unittest.mock import patch from .util import mock_request_get_content from weibo_spider.parser.album_parser import AlbumParser @patch('requests.get', mock_request_get_content) def test_album_parser(): album_parser = AlbumParser( cookie="", album_url="https://weibo.cn/album/166564740000001980768563?rl=1") pic_urls = album_parser.extract_pic_urls() assert (len(pic_urls) == 4) assert (pic_urls == [ 'http://wx1.sinaimg.cn/wap180/76102133ly8ga961tpte6j20u00u0q65.jpg', 'http://wx2.sinaimg.cn/wap180/76102133ly8fwr33wpn8fj20v90v9tbw.jpg', 'http://wx4.sinaimg.cn/wap180/76102133ly8fvlyn5n52gj20v90v949a.jpg', 'http://wx2.sinaimg.cn/wap180/76102133ly8fk0btnrn5zj20dp0e8q3t.jpg' ]) ================================================ FILE: tests/test_parser/test_comment_parser.py ================================================ from unittest.mock import patch from .util import mock_request_get_content from weibo_spider.parser.comment_parser import CommentParser @patch('requests.get', mock_request_get_content) def test_comment_parser(): comment_parser = CommentParser(cookie="", weibo_id="J5cVGuUNq") long_weibo = comment_parser.get_long_weibo() long_retweet = comment_parser.get_long_retweet() assert ( long_retweet == """去年和亲善大使热巴@Dear-迪丽热巴 的特别回忆。""" """我们在藏北羌塘一起爬山,探访藏羚羊、雪豹、黑颈鹤的栖息地,感受野生动物保护工作的点滴。""" """此时此刻,我们比以往更加重视与自然相处的方式,我们也从未如此迫切需要将想法付诸行动。""" """热巴已经和我们@北京绿色阳光 站在一起,希望看完视频的你们,也能获得同样感受与动力。\n""" """We Stand for Wildlife. \n""" """明日朝阳68309的优酷视频""") assert ( long_weibo == """去年和亲善大使热巴@Dear-迪丽热巴 的特别回忆。""" """我们在藏北羌塘一起爬山,探访藏羚羊、雪豹、黑颈鹤的栖息地,感受野生动物保护工作的点滴。""" """此时此刻,我们比以往更加重视与自然相处的方式,我们也从未如此迫切需要将想法付诸行动。""" """热巴已经和我们@北京绿色阳光 站在一起,希望看完视频的你们,也能获得同样感受与动力。\n""" """We Stand for Wildlife. \n""" """明日朝阳68309的优酷视频""") ================================================ FILE: tests/test_parser/test_index_parser.py ================================================ from unittest.mock import patch from .util import mock_request_get_content from weibo_spider.parser.index_parser import IndexParser @patch('requests.get', mock_request_get_content) def test_index_parser(): index_parser = IndexParser(cookie="", user_uri="1669879400") assert (index_parser.get_page_num() == 117) assert (str(index_parser.get_user()) == """用户昵称: Dear-迪丽热巴\n""" """用户id: 1669879400\n""" """微博数: 1159\n""" """关注数: 253\n""" """粉丝数: 70805574\n""") ================================================ FILE: tests/test_parser/test_info_parser.py ================================================ from unittest.mock import patch from .util import mock_request_get_content from weibo_spider.parser.info_parser import InfoParser @patch('requests.get', mock_request_get_content) def test_info_parser(): info_parser = InfoParser(cookie="", user_id="1669879400") user = info_parser.extract_user_info() # With info_parser, we can only get the nickname. assert (user.nickname == "Dear-迪丽热巴") ================================================ FILE: tests/test_parser/test_mblog_picAll_parser.py ================================================ from unittest.mock import patch from .util import mock_request_get_content from weibo_spider.parser.mblog_picAll_parser import MblogPicAllParser @patch('requests.get', mock_request_get_content) def test_mblog_picAll_parser(): mblog_picAll_parser = MblogPicAllParser(cookie="", weibo_id="J5ZcSnCAg") preview_picture_list = mblog_picAll_parser.extract_preview_picture_list() # With info_parser, we can only get the nickname. assert (len(preview_picture_list) == 18) assert ( preview_picture_list[0] == 'http://ww3.sinaimg.cn/thumb180/63885668ly1gfn5qz5m1yj20u0140472.jpg') ================================================ FILE: tests/test_parser/test_page_parser.py ================================================ from unittest.mock import patch from weibo_spider.parser.page_parser import PageParser from .util import mock_request_get_content @patch('requests.get', mock_request_get_content) def test_page_parser(): user_config = { 'user_uri': '1669879400', 'since_date': '2020-06-01', 'end_date': 'now' } page_parser = PageParser(cookie="", user_config=user_config, page=2, filter=True) weibos, weibo_id_list, to_continue = page_parser.get_one_page([]) assert (weibo_id_list == ['J4PGk4yMw', 'J4EUStJKu']) assert (len(weibos) == 2) assert (str(weibos[0]) == """生日动态 \xa0\n""" """微博发布位置:无\n""" """发布时间:2020-06-03 00:00\n""" """发布工具:生日动态\n""" """点赞数:1499675\n""" """转发数:1000000\n""" """评论数:1000000\n""" """url:https://weibo.cn/comment/J4PGk4yMw\n""") assert (str(weibos[1]) == """#微博剧场# #周放设计淡黄的长裙# 这是一幅有声音的手稿#幸福触手可及# 绿洲 \xa0原图\xa0\n""" """微博发布位置:无\n""" """发布时间:2020-06-01 20:35\n""" """发布工具:绿洲APP\n""" """点赞数:419181\n""" """转发数:1000000\n""" """评论数:1000000\n""" """url:https://weibo.cn/comment/J4EUStJKu\n""") ================================================ FILE: tests/test_parser/test_photo_parser.py ================================================ from unittest.mock import patch from weibo_spider.parser.photo_parser import PhotoParser from .util import mock_request_get_content @patch('requests.get', mock_request_get_content) def test_photo_parser(): photo_parser = PhotoParser(cookie="", user_id=1980768563) avatar_album_url = photo_parser.extract_avatar_album_url() assert (avatar_album_url == "https://weibo.cn/album/166564740000001980768563?rl=1") ================================================ FILE: tests/test_parser/util.py ================================================ import json import os from unittest.mock import Mock from weibo_spider.parser.util import TEST_DATA_DIR, URL_MAP_FILE def mock_request_get_content(url, headers): with open(os.path.join(TEST_DATA_DIR, URL_MAP_FILE)) as f: url_map = json.loads(f.read()) resp_file = url_map[url] mock = Mock() with open(resp_file, "rb") as f: mock.content = f.read() return mock ================================================ FILE: tests/testdata/2f62165fa3ca1e85e0d398d385c377a068b76eb95765f7020ffffd3e.html ================================================ Dear-迪丽热巴的微博
Dear-迪丽热巴的微博 加关注
 微博  相册 
生日动态  
赞[1499675] 转发[1000000] 评论[1000000] 收藏 2020-06-03 00:00 来自生日动态
炎炎夏日让每天的沐浴时光都变得尤其重要,精致的沙龙香相伴让沐浴也可以成为清新浪漫的享受!给大家@LUX力士 的沐浴小秘密分享,有力士植萃沐浴露,把沐浴变成“仪式感”!我的心选好物分享给你们啦 [笑而不语] LUX力士的微博视频  
赞[377578] 转发[1000000] 评论[1000000] 收藏 2020-05-31 10:59
#idoltube##周放vlog# 第二篇来啦!今天邀请大家走进生活,走进幸福的放放子一家~[喵喵]#幸福触手可及# Dear-迪丽热巴的微博视频  
赞[397970] 转发[1000000] 评论[1000000] 收藏 2020-05-30 19:02 来自国产剧集 · 视频社区
@法国娇韵诗 收到宠爱了~小娇的618#娇宠你有一套#,早晚护肤都靠它,超级喜欢这份宠爱!现在给全体爱丽丝们施法,希望你们都可以拥有这份让你变美的娇宠礼物哦~同款娇宠http://t.cn/A62cgDJp一起享用!  [组图共2张]
#微博剧场# 我为4A景区代言,酷飒周放的追剧邀请,你来吗? #4A景区触手可及#
@路易威登 PONT 9 手袋 陪你摩登一夏[嘻嘻]#LVPONT9#  [组图共3张]
#热巴手稿填色大赛#服装手稿填色游戏正式开启!图一出自迪迪子,图二出自放放子。迪迪子的面子就靠大家的后期填色了[微笑] 绿洲  [组图共2张]
图片 原图 
赞[733671] 转发[1000000] 评论[1000000] 收藏 2020-05-27 14:48 来自绿洲APP
转发了 护舒宝VM 的微博:还记得和宝宝陪着@Dear-迪丽热巴 走过的花路吗?谢谢阿丝们一直以来的陪伴[太开心][太开心]~为你甄选护舒宝天然纯棉卫生巾,给你透气亲肤的体验。现在上天猫超市购买,1套减25,第2套只要19.9。未来的花路,和宝宝一起用好物,守护热巴!#迪丽热巴[超话]#
图片 原图 赞[43521] 原文转发[1000000] 原文评论[13967]
转发理由:谢谢@护舒宝 和阿丝们的守护,每一刻都非常有意义。未来请继续指教啦~  
赞[418834] 转发[1000000] 评论[1000000] 收藏 2020-05-26 11:14
#idoltube##周放vlog# 放放子的第一支搞事业篇vlog已上线~约vlog的朋友们可以放下你们的号码牌了[可爱] #幸福触手可及# Dear-迪丽热巴的微博视频  
赞[450541] 转发[1000000] 评论[216934] 收藏 2020-05-25 20:53 来自影视剪辑 · 视频社区
下页 上页 首页  2/117页
TOP
彩版|触屏|语音
weibo.cn[06-19 00:47]
================================================ FILE: tests/testdata/4957814af5a123b82e974b5537dea736dfb34e48d8835203a45d2e67.html ================================================ Dear-迪丽热巴的微博
头像
Dear-迪丽热巴VM 女/上海   加关注
认证:嘉行传媒签约演员 
一只喜欢默默表演的小透明。工作联系jaywalk@jaywalk.com.cn ...
私信 资料 操作 特别关注 送Ta会员
 微博  相册 
#一起热爱就现在#给你们康康我眼前的画面[嘻嘻] 绿洲
图片 原图 
赞[523801] 转发[1000000] 评论[393530] 收藏 06月17日 18:12 来自绿洲APP
刚收到我定制的亓那眼镜,猜猜定制了什么[doge]好奇?没关系,你们也可以拥有自己的定制眼镜。关注@QINA亓那眼镜 解锁6月限定惊喜,#时髦寻宝计划# 线上线下都安排了[偷笑]QINA亓那眼镜的微博视频  
赞[415899] 转发[1000000] 评论[1000000] 收藏 06月15日 10:09
#idoltube##周放vlog# 什么?放放子还有两副面孔呢?[喵喵] #幸福触手可及# Dear-迪丽热巴的微博视频  
赞[318054] 转发[1000000] 评论[514546] 收藏 06月14日 20:09 来自影视剪辑 · 视频社区
图片 原图 
赞[1150265] 转发[1000000] 评论[1000000] 收藏 06月12日 19:11 来自绿洲APP
言出必行,说了18张就是18张,送给七千万的你们 ~  [组图共18张]
放放子缺个快板[偷笑] 绿洲
图片 原图 
赞[571755] 转发[1000000] 评论[1000000] 收藏 06月08日 15:17 来自绿洲APP
转发了 WCS野生生物保护学会V 的微博:去年和亲善大使热巴@Dear-迪丽热巴 的特别回忆[心]。我们在藏北羌塘一起爬山,探访藏羚羊、雪豹、黑颈鹤的栖息地,感受野生动物保护工作的点滴。此时此刻,我们比以往更加重视与自然相处的方式,我们也从未如此迫切需要将想法付诸行动。热巴已经和我们@北京绿色阳光 站在一起,希望看完视频的你们,也...全文 赞[119296] 原文转发[1000000] 原文评论[38688]
转发理由:在羌塘的美好回忆~第一次来到这片独特的荒野,看到野生动物自由生活,还有一群快乐可爱的人在守护着它们。把这些美好留存下来,关注野生动物保护,积极行动,我们每个人都能贡献力量。[心]  
赞[554415] 转发[1000000] 评论[1000000] 收藏 06月05日 11:11
要开心。要充实。
#微博live秀# 28岁的直播~@Dear-迪丽热巴 的一直播(下载App->http://t.cn/RDUuslr 
赞[435650] 转发[23584] 评论[1000000] 收藏 06月03日 19:00 来自一直播Yi
下页  1/117页
TOP
彩版|触屏|语音
weibo.cn[06-19 00:47]
================================================ FILE: tests/testdata/4d5ed0a3ebd0303cb45edd544dbc0ab5e86d43e103405f0c60515884.html ================================================ 评论列表
Dear-迪丽热巴VM  转发了 @WCS野生生物保护学会V 的微博:去年和亲善大使热巴@Dear-迪丽热巴 的特别回忆[心]。我们在藏北羌塘一起爬山,探访藏羚羊、雪豹、黑颈鹤的栖息地,感受野生动物保护工作的点滴。此时此刻,我们比以往更加重视与自然相处的方式,我们也从未如此迫切需要将想法付诸行动。热巴已经和我们@北京绿色阳光 站在一起,希望看完视频的你们,也能获得同样感受与动力。

We Stand for Wildlife.

明日朝阳68309的优酷视频
 原文转发[1000000] 原文评论[38774]
转发理由:在羌塘的美好回忆~第一次来到这片独特的荒野,看到野生动物自由生活,还有一群快乐可爱的人在守护着它们。把这些美好留存下来,关注野生动物保护,积极行动,我们每个人都能贡献力量。[心]   2020-06-05 11:11:05  关注她  举报 收藏 操作
 转发[1000000]  评论[1000000]  赞[572950] 
评论只显示前140字:

 
[热门]LUX力士V:大家和迪迪一起保护动物[心] 举报 赞[18737] 回复 2020-06-05 11:12:14 网页
[热门]护舒宝VM:跟迪迪一起好好保护野生动物[心] 举报 赞[17423] 回复 2020-06-05 11:13:30 网页
[热门]Dear迪丽热巴后援会VM:一起保护野生动物[给你小心心] 举报 赞[15945] 回复 2020-06-05 11:12:46 网页
源汐梦 M ://@Dear-迪丽热巴:在羌塘的美好回忆~第一次来到这片独特的荒野,看到野生动物自由生活,还有一群快乐可爱的人在守护着它们。把这些美好留存下来,关注野生动物保护,积极行动,我们每个人都能贡献力量。[心]  举报   赞[0]  回复   01月10日 12:43 来自来自河南
今天就中热巴亲签 :[送花花][可怜][可怜]  举报   赞[0]  回复   2024-10-24 20:56:29 来自来自安徽
qadxxghlrrr :饱饱[打call][打call][打call]  举报   赞[0]  回复   2024-05-20 13:29:58 来自来自湖南
正义终将会归来 :👍👍👍👍❤️❤️  举报   赞[1]  回复   2023-09-11 21:31:18 来自来自广东
守护热爱星球 ://@Dear-迪丽热巴:在羌塘的美好回忆~第一次来到这片独特的荒野,看到野生动物自由生活,还有一群快乐可爱的人在守护着它们。把这些美好留存下来,关注野生动物保护,积极行动,我们每个人都能贡献力量。[心]  举报   赞[0]  回复   2023-08-14 21:41:37 来自来自山东
林深时听瑜_ VM :和巴巴一起好好保护野生动物  举报   赞[0]  回复   2022-12-27 22:48:15 来自来自福建
-土豆削成丝- :宝贝  举报   赞[0]  回复   2022-08-27 01:14:35 来自来自安徽
诗润我心 :刚看完  举报   赞[0]  回复   2022-04-29 07:01:08 来自来自河北
诗润我心 :太棒了,热巴姐姐  举报   赞[0]  回复   2022-04-29 07:00:58 来自来自河北
下页  1/100000页
================================================ FILE: tests/testdata/63a98849ec82b2c87ec55bca03cbf5988f7eac233a23d86b4fdf5ffd.html ================================================ 微博
图片加载中... 1/18 原图
图片加载中... 2/18 原图
图片加载中... 3/18 原图
图片加载中... 4/18 原图
图片加载中... 5/18 原图
图片加载中... 6/18 原图
图片加载中... 7/18 原图
图片加载中... 8/18 原图
图片加载中... 9/18 原图
图片加载中... 10/18 原图
图片加载中... 11/18 原图
图片加载中... 12/18 原图
图片加载中... 13/18 原图
图片加载中... 14/18 原图
图片加载中... 15/18 原图
图片加载中... 16/18 原图
图片加载中... 17/18 原图
图片加载中... 18/18 原图
================================================ FILE: tests/testdata/76233b3f90394581aac6f19cfa5d674a610e8b442b1f83de7673ab49.html ================================================ 微博
图片加载中... 1/2 原图
图片加载中... 2/2 原图
================================================ FILE: tests/testdata/a4437630f3bdfa2757bae1595186ac063fe5ec25cf2f98116ece83cb.html ================================================ Dear-迪丽热巴的微博
头像
Dear-迪丽热巴VM 女/上海   加关注
认证:嘉行传媒签约演员 
一只喜欢默默表演的小透明。工作联系jaywalk@jaywalk.com.cn ...
私信 资料 操作 特别关注 送Ta会员
 微博  相册 
#一起热爱就现在#给你们康康我眼前的画面[嘻嘻] 绿洲
图片 原图 
赞[523801] 转发[1000000] 评论[393530] 收藏 06月17日 18:12 来自绿洲APP
刚收到我定制的亓那眼镜,猜猜定制了什么[doge]好奇?没关系,你们也可以拥有自己的定制眼镜。关注@QINA亓那眼镜 解锁6月限定惊喜,#时髦寻宝计划# 线上线下都安排了[偷笑]QINA亓那眼镜的微博视频  
赞[415899] 转发[1000000] 评论[1000000] 收藏 06月15日 10:09
#idoltube##周放vlog# 什么?放放子还有两副面孔呢?[喵喵] #幸福触手可及# Dear-迪丽热巴的微博视频  
赞[318054] 转发[1000000] 评论[514546] 收藏 06月14日 20:09 来自影视剪辑 · 视频社区
图片 原图 
赞[1150265] 转发[1000000] 评论[1000000] 收藏 06月12日 19:11 来自绿洲APP
言出必行,说了18张就是18张,送给七千万的你们 ~  [组图共18张]
放放子缺个快板[偷笑] 绿洲
图片 原图 
赞[571755] 转发[1000000] 评论[1000000] 收藏 06月08日 15:17 来自绿洲APP
转发了 WCS野生生物保护学会V 的微博:去年和亲善大使热巴@Dear-迪丽热巴 的特别回忆[心]。我们在藏北羌塘一起爬山,探访藏羚羊、雪豹、黑颈鹤的栖息地,感受野生动物保护工作的点滴。此时此刻,我们比以往更加重视与自然相处的方式,我们也从未如此迫切需要将想法付诸行动。热巴已经和我们@北京绿色阳光 站在一起,希望看完视频的你们,也...全文 赞[119296] 原文转发[1000000] 原文评论[38688]
转发理由:在羌塘的美好回忆~第一次来到这片独特的荒野,看到野生动物自由生活,还有一群快乐可爱的人在守护着它们。把这些美好留存下来,关注野生动物保护,积极行动,我们每个人都能贡献力量。[心]  
赞[554415] 转发[1000000] 评论[1000000] 收藏 06月05日 11:11
要开心。要充实。
#微博live秀# 28岁的直播~@Dear-迪丽热巴 的一直播(下载App->http://t.cn/RDUuslr 
赞[435650] 转发[23584] 评论[1000000] 收藏 06月03日 19:00 来自一直播Yi
下页  1/117页
TOP
彩版|触屏|语音
weibo.cn[06-19 00:47]
================================================ FILE: tests/testdata/b541fd1751117498b6d6f40d3321686ddf871651237c4ac854a5c3eb.html ================================================ 专辑:头像相册
专辑:头像相册
照片墙|传统列表
TOP
================================================ FILE: tests/testdata/ca5f2a555e8d62f728c66fa90afb2d54d19f8c898e164204a61bdf03.html ================================================ Dear-迪丽热巴的资料
头像
会员等级:7级 送Ta会员
微身份 语惊四座 七步成诗 谈笑风生 更多勋章
基本信息
昵称:Dear-迪丽热巴
认证:嘉行传媒签约演员 
性别:女
地区:上海
生日:双子座
认证信息:嘉行传媒签约演员 
简介:一只喜欢默默表演的小透明。工作联系jaywalk@jaywalk.com.cn 🍒
学习经历
·上海戏剧学院
工作经历
·嘉行传媒 
其他信息
互联网:http://weibo.com/u/1669879400
手机版:https://weibo.cn/u/1669879400
她的相册>>
TOP
彩版|触屏|语音
weibo.cn[06-19 00:47]
================================================ FILE: tests/testdata/d486235d4a17dd0accb0f2cc77b3648abfa03580b9e0cdb61f1e618f.html ================================================ Dear-迪丽热巴的微博
Dear-迪丽热巴的微博 加关注
 微博  相册 
粉色天空、闪耀夜色、浪漫爱意…我把我喜爱的元素和巴黎限定记忆全部定格在这一瓶#YSL反转巴黎#热爱限定中,第一次与YSL一起合作设计香水,在这#拦不住的夏天#把甜甜的曼陀罗花香送给你们,喜欢吗?💓  [组图共2张]
#幸福触手可及开播##幸福触手可及# 度量自身,方能修炼精彩人生。追梦不易,披荆斩棘。今晚八点@湖南卫视 和周放,一起守护梦想,书写初夏。
很高兴成为力士大中华区沐浴系列代言人,520就要到啦,大家快来接收告白福利哦!全新植萃泡泡沐浴露让每一位小仙女都能在浓密泡泡浴中拥有夏日嫩白肌,仙气香气都十足!关注@LUX力士 第一时间锁定新品哦!LUX力士的微博视频  
赞[741402] 转发[1000000] 评论[1000000] 收藏 05月19日 09:03
转发了 电视剧幸福触手可及VM 的微博:#幸福触手可及##幸福触手可及定档0519# 从没有一个时刻,幸福如此靠近,只因有你在身边[心]5月19日20:00锁定@湖南卫视 金鹰独播剧场,@优酷 @爱奇艺 @腾讯视频 24点同步更新,等你解锁初夏甜梦!
图片 原图 赞[129675] 原文转发[332651] 原文评论[6900]
转发理由:#幸福触手可及定档0519# 唯有热爱,不负韶华,为之全力以赴,才能成为更优秀的人。5月19日20:00锁定湖南卫视#幸福触手可及# ,愈挫愈勇的独立设计师周放来啦。  
赞[480418] 转发[57012] 评论[40966] 收藏 05月15日 20:23
哈哈哈哈哈哈👅
转发了 北京2022年冬奥会VM 的微博:【爱豆喊你来助力#北京2022#
花样滑冰,旋转跳跃 ,“迪丽”前行 @Dear-迪丽热巴 北京2022年冬奥会的微博视频
 赞[680201] 原文转发[1450782] 原文评论[50694]
转发理由:与我一起,关注花样滑冰,为中国健儿鼓劲加油[加油]  
赞[501777] 转发[1000000] 评论[1000000] 收藏 05月15日 10:20
转发了 央视网VM 的微博:【想看看战疫一线医护人员们的脸!#极限挑战致敬医护人员#】脱下防疫服,援鄂人员们原来是这个模样。八位医护人员集体分享支援一线的故事,是他们为后方的我们竖起了最坚实的屏障,感谢这群医护天使的负重前行,致敬!@央视网青年 @雷佳音 @岳云鹏 @演员王迅 @贾乃亮 @努力努力再努力x @Dear-迪丽热巴...全文 赞[364004] 原文转发[1056354] 原文评论[3645]
转发理由:#极限挑战# 感谢你们的守护,最美的逆行者们[心]  
赞[571256] 转发[1000000] 评论[362127] 收藏 05月10日 23:01
#极限挑战# 无奖填词竞答,今晚看👉登峰造_,不可_量,百里_一,南征北_~ Dear-迪丽热巴的微博视频  
赞[731516] 转发[1000000] 评论[1000000] 收藏 05月10日 16:50
转发了 中国青年报VM 的微博:#五四致敬战疫青年# #青春万岁#各地应急响应级别陆续下调,我们正在走向痊愈。回望这些年轻医务人员的脸,不应忘记,正是他们在危难之下,白衣执甲,毅然逆行,为我们筑起血肉长城。感恩提灯天使,致敬最可爱的人!春暖花开,等到疫情完全解除,无论你是从医还是就医,请记住医患之间的休戚与共、唇齿...全文 [组图共12张]
图片 原图 赞[32125] 原文转发[4801631] 原文评论[6975]
转发理由:#五四致敬战疫青年#五四青年节前夕,让我们说一声,#谢谢你保护了我们#!  
赞[721484] 转发[1000000] 评论[597487] 收藏 05月02日 16:40
转发了 东方卫视极限挑战VM 的微博:鸡条君目睹了vivo#极限挑战#第六季首发阵容@雷佳音 @岳云鹏 @演员王迅 @贾乃亮 @努力努力再努力x @Dear-迪丽热巴 @郭京飞 @邓伦 集结的整个过程,这就是欢迎新人的方式[疑问]说好要相亲相爱的呢😂东方卫视极限挑战的微博视频  赞[711013] 原文转发[1409505] 原文评论[14761]
转发理由:#极限挑战#举手之劳,岳岳哥别客气!//@岳云鹏:#极限挑战#谢谢热巴@Dear-迪丽热巴 给我p图,我这里还有好多库存 查看图片  
赞[983376] 转发[1000000] 评论[1000000] 收藏 04月30日 12:30
下页 上页 首页  3/117页
TOP
彩版|触屏|语音
weibo.cn[06-19 00:47]
================================================ FILE: tests/testdata/e4d541ecb02253c14abc1d52605fc00d91279df9ac4c1465c85b91b3.html ================================================ 微博
霜叶的相册
 微博  相册 
 
TOP
================================================ FILE: tests/testdata/e97222acd5bc7d8d1bfbd3f352f8cad3e36fdd19e40b69e1c33fb3c3.html ================================================ 微博
图片加载中... 1/2 原图
图片加载中... 2/2 原图
================================================ FILE: tests/testdata/url_map.json ================================================ { "https://weibo.cn/1669879400/profile": "tests/testdata/a4437630f3bdfa2757bae1595186ac063fe5ec25cf2f98116ece83cb.html", "https://weibo.cn/1669879400/info": "tests/testdata/ca5f2a555e8d62f728c66fa90afb2d54d19f8c898e164204a61bdf03.html", "https://weibo.cn/1669879400/profile?page=1": "tests/testdata/4957814af5a123b82e974b5537dea736dfb34e48d8835203a45d2e67.html", "https://weibo.cn/mblog/picAll/J6k49kbTc?rl=1": "tests/testdata/e97222acd5bc7d8d1bfbd3f352f8cad3e36fdd19e40b69e1c33fb3c3.html", "https://weibo.cn/mblog/picAll/J5ZcSnCAg?rl=1": "tests/testdata/63a98849ec82b2c87ec55bca03cbf5988f7eac233a23d86b4fdf5ffd.html", "https://weibo.cn/1669879400/profile?page=2": "tests/testdata/2f62165fa3ca1e85e0d398d385c377a068b76eb95765f7020ffffd3e.html", "https://weibo.cn/1669879400/profile?page=3": "tests/testdata/d486235d4a17dd0accb0f2cc77b3648abfa03580b9e0cdb61f1e618f.html", "https://weibo.cn/mblog/picAll/J3xfm61AZ?rl=1": "tests/testdata/76233b3f90394581aac6f19cfa5d674a610e8b442b1f83de7673ab49.html", "https://weibo.cn/comment/J5cVGuUNq": "tests/testdata/4d5ed0a3ebd0303cb45edd544dbc0ab5e86d43e103405f0c60515884.html", "https://weibo.cn/1980768563/photo?tf=6_008": "tests/testdata/e4d541ecb02253c14abc1d52605fc00d91279df9ac4c1465c85b91b3.html", "https://weibo.cn/album/166564740000001980768563?rl=1": "tests/testdata/b541fd1751117498b6d6f40d3321686ddf871651237c4ac854a5c3eb.html" } ================================================ FILE: weibo_spider/__init__.py ================================================ ================================================ FILE: weibo_spider/__main__.py ================================================ import os import sys from absl import app sys.path.append(os.path.abspath(os.path.dirname(os.getcwd()))) from weibo_spider.spider import main app.run(main) ================================================ FILE: weibo_spider/config_sample.json ================================================ { "user_id_list": ["1669879400"], "filter": 1, "since_date": "2018-01-01", "end_date": "now", "random_wait_pages": [1, 5], "random_wait_seconds": [6, 10], "global_wait": [[1000, 3600], [500, 2000]], "write_mode": ["csv", "txt"], "pic_download": 1, "video_download": 1, "file_download_timeout": [5, 5, 10], "result_dir_name": 0, "cookie": "your cookie", "mysql_config": { "host": "localhost", "port": 3306, "user": "root", "password": "123456", "charset": "utf8mb4" }, "kafka_config": { "bootstrap-server": "127.0.0.1:9092", "weibo_topics": ["spider_weibo"], "user_topics": ["spider_weibo"] }, "sqlite_config": "weibo.db", "mongo_config": { "connection_string": "mongodb://admin:password@localhost:27017/weibo", "dba_name": "", "dba_password": "" }, "post_config": { "api_url": "", "api_token": "" } } ================================================ FILE: weibo_spider/config_util.py ================================================ import codecs import logging import os import sys import browser_cookie3 from datetime import datetime import json logger = logging.getLogger('spider.config_util') def _is_date(date_str): """判断日期格式是否正确""" try: if ':' in date_str: datetime.strptime(date_str, '%Y-%m-%d %H:%M') else: datetime.strptime(date_str, '%Y-%m-%d') return True except ValueError: return False def validate_config(config): """验证配置是否正确""" # 验证filter、pic_download、video_download argument_list = ['filter', 'pic_download', 'video_download'] for argument in argument_list: if config[argument] != 0 and config[argument] != 1: logger.warning(u'%s值应为0或1,请重新输入', config[argument]) sys.exit() # 验证since_date since_date = config['since_date'] if (not _is_date(str(since_date))) and (not isinstance(since_date, int)): logger.warning(u'since_date值应为yyyy-mm-dd形式或整数,请重新输入') sys.exit() # 验证end_date end_date = str(config['end_date']) if (not _is_date(end_date)) and (end_date != 'now'): logger.warning(u'end_date值应为yyyy-mm-dd形式或"now",请重新输入') sys.exit() # 验证random_wait_pages random_wait_pages = config['random_wait_pages'] if not isinstance(random_wait_pages, list): logger.warning(u'random_wait_pages参数值应为list类型,请重新输入') sys.exit() if (not isinstance(min(random_wait_pages), int)) or (not isinstance( max(random_wait_pages), int)): logger.warning(u'random_wait_pages列表中的值应为整数类型,请重新输入') sys.exit() if min(random_wait_pages) < 1: logger.warning(u'random_wait_pages列表中的值应大于0,请重新输入') sys.exit() # 验证random_wait_seconds random_wait_seconds = config['random_wait_seconds'] if not isinstance(random_wait_seconds, list): logger.warning(u'random_wait_seconds参数值应为list类型,请重新输入') sys.exit() if (not isinstance(min(random_wait_seconds), int)) or (not isinstance( max(random_wait_seconds), int)): logger.warning(u'random_wait_seconds列表中的值应为整数类型,请重新输入') sys.exit() if min(random_wait_seconds) < 1: logger.warning(u'random_wait_seconds列表中的值应大于0,请重新输入') sys.exit() # 验证global_wait global_wait = config['global_wait'] if not isinstance(global_wait, list): logger.warning(u'global_wait参数值应为list类型,请重新输入') sys.exit() for g in global_wait: if not isinstance(g, list): logger.warning(u'global_wait参数内的值应为长度为2的list类型,请重新输入') sys.exit() if len(g) != 2: logger.warning(u'global_wait参数内的list长度应为2,请重新输入') sys.exit() for i in g: if (not isinstance(i, int)) or i < 1: logger.warning(u'global_wait列表中的值应为大于0的整数,请重新输入') sys.exit() # 验证write_mode write_mode = ['txt', 'csv', 'json', 'mongo', 'mysql', 'sqlite', 'kafka','post'] if not isinstance(config['write_mode'], list): logger.warning(u'write_mode值应为list类型') sys.exit() for mode in config['write_mode']: if mode not in write_mode: logger.warning( u'%s为无效模式,请从txt、csv、json、post、mongo、sqlite, kafka和mysql中挑选一个或多个作为write_mode', mode) sys.exit() # 验证user_id_list user_id_list = config['user_id_list'] if (not isinstance(user_id_list, list)) and (not user_id_list.endswith('.txt')): logger.warning(u'user_id_list值应为list类型或txt文件路径') sys.exit() if not isinstance(user_id_list, list): if not os.path.isabs(user_id_list): user_id_list = os.getcwd() + os.sep + user_id_list if not os.path.isfile(user_id_list): logger.warning(u'不存在%s文件', user_id_list) sys.exit() def get_user_config_list(file_name, default_since_date): """获取文件中的微博id信息""" with open(file_name, 'rb') as f: try: lines = f.read().splitlines() lines = [line.decode('utf-8-sig') for line in lines] except UnicodeDecodeError: logger.error(u'%s文件应为utf-8编码,请先将文件编码转为utf-8再运行程序', file_name) sys.exit() user_config_list = [] for line in lines: info = line.split(' ') if len(info) > 0 and info[0].isdigit(): user_config = {} user_config['user_uri'] = info[0] if len(info) > 2 and _is_date(info[2]): if len(info) > 3 and _is_date(info[2] + ' ' + info[3]): user_config['since_date'] = info[2] + ' ' + info[3] else: user_config['since_date'] = info[2] else: user_config['since_date'] = default_since_date if user_config not in user_config_list: user_config_list.append(user_config) return user_config_list def update_user_config_file(user_config_file_path, user_uri, nickname, start_time): """更新用户配置文件""" if not user_config_file_path: user_config_file_path = os.getcwd() + os.sep + 'user_id_list.txt' with open(user_config_file_path, 'rb') as f: lines = f.read().splitlines() lines = [line.decode('utf-8-sig') for line in lines] for i, line in enumerate(lines): info = line.split(' ') if len(info) > 0: if user_uri == info[0]: if len(info) == 1: info.append(nickname) info.append(start_time) if len(info) == 2: info.append(start_time) if len(info) > 3 and _is_date(info[2] + ' ' + info[3]): del info[3] if len(info) > 2: info[2] = start_time lines[i] = ' '.join(info) break with codecs.open(user_config_file_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) def add_user_uri_list(user_config_file_path, user_uri_list): """向user_id_list.txt文件添加若干user_uri""" if not user_config_file_path: user_config_file_path = os.getcwd() + os.sep + 'user_id_list.txt' if os.path.isfile(user_config_file_path): user_uri_list[0] = '\n' + user_uri_list[0] with codecs.open(user_config_file_path, 'a', encoding='utf-8') as f: f.write('\n'.join(user_uri_list)) def get_cookie(): """Get weibo.cn cookie from Chrome browser""" try: chrome_cookies = browser_cookie3.chrome(domain_name='weibo.cn') cookies_dict = {cookie.name: cookie.value for cookie in chrome_cookies} return cookies_dict except Exception as e: logger.error(u'Failed to obtain weibo.cn cookie from Chrome browser: %s', str(e)) raise def update_cookie_config(cookie, user_config_file_path): """Update cookie in config.json""" if not user_config_file_path: user_config_file_path = os.getcwd() + os.sep + 'config.json' try: with codecs.open(user_config_file_path, 'r', encoding='utf-8') as f: config = json.load(f) cookie_string = '; '.join(f'{name}={value}' for name, value in cookie.items()) if config['cookie'] != cookie_string: config['cookie'] = cookie_string with codecs.open(user_config_file_path, 'w', encoding='utf-8') as f: json.dump(config, f, indent=4, ensure_ascii=False) except Exception as e: logger.error(u'Failed to update cookie in config file: %s', str(e)) raise def check_cookie(user_config_file_path): """Checks if user is logged in""" try: cookie = get_cookie() if cookie.get("MLOGIN", '0') == '0': logger.warning("使用 Chrome 在此登录 %s", "https://passport.weibo.com/sso/signin?entry=wapsso&source=wapssowb&url=https://m.weibo.cn/") sys.exit() else: update_cookie_config(cookie, user_config_file_path) except Exception as e: logger.error(u'Check for cookie failed: %s', str(e)) raise ================================================ FILE: weibo_spider/datetime_util.py ================================================ from datetime import datetime def str_to_time(text): """将字符串转换成时间类型""" if ':' in text: result = datetime.strptime(text, '%Y-%m-%d %H:%M') else: result = datetime.strptime(text, '%Y-%m-%d') return result ================================================ FILE: weibo_spider/downloader/__init__.py ================================================ from .origin_picture_downloader import OriginPictureDownloader from .retweet_picture_downloader import RetweetPictureDownloader from .avatar_picture_downloader import AvatarPictureDownloader from .video_downloader import VideoDownloader __all__ = [ OriginPictureDownloader, RetweetPictureDownloader, AvatarPictureDownloader, VideoDownloader ] ================================================ FILE: weibo_spider/downloader/avatar_picture_downloader.py ================================================ import os from .img_downloader import ImgDownloader class AvatarPictureDownloader(ImgDownloader): def __init__(self, file_dir, file_download_timeout): super().__init__(file_dir, file_download_timeout) self.describe = u'头像图片' self.key = 'avatar_pictures' async def handle_download(self, urls, session): """处理下载相关操作""" file_dir = self.file_dir + os.sep + self.describe if not os.path.isdir(file_dir): os.makedirs(file_dir) for i, url in enumerate(urls): index = url.rfind('/') file_name = url[index:] file_path = file_dir + os.sep + file_name await self.download_one_file(url, file_path, 'xxx', session) ================================================ FILE: weibo_spider/downloader/downloader.py ================================================ # -*- coding: UTF-8 -*- import asyncio import logging import os import sys import random from abc import ABC, abstractmethod import aiohttp from tqdm import tqdm logger = logging.getLogger('spider.downloader') class Downloader(ABC): def __init__(self, file_dir, file_download_timeout): self.file_dir = file_dir self.describe = '' self.key = '' self.file_download_timeout = [5, 5, 10] if (isinstance(file_download_timeout, list) and len(file_download_timeout) == 3): for i in range(3): v = file_download_timeout[i] if isinstance(v, (int, float)) and v > 0: self.file_download_timeout[i] = v @abstractmethod async def handle_download(self, urls, w, session): """下载 urls 里所指向的图片或视频文件,使用 w 里的信息来生成文件名""" pass async def download_one_file(self, url, file_path, weibo_id, session): """下载单个文件(图片/视频)""" try: if not os.path.isfile(file_path): # 随机延时,模拟人工操作 await asyncio.sleep(random.uniform(0.5, 1.5)) # Retry logic retries = self.file_download_timeout[0] timeout = aiohttp.ClientTimeout( connect=self.file_download_timeout[1], total=self.file_download_timeout[2] ) last_exception = None for _ in range(retries + 1): try: async with session.get(url, timeout=timeout) as response: if response.status == 200: content = await response.read() with open(file_path, 'wb') as f: f.write(content) break except Exception as e: last_exception = e else: if last_exception: raise last_exception return os.path.isfile(file_path) except Exception as e: error_file = self.file_dir + os.sep + 'not_downloaded.txt' with open(error_file, 'ab') as f: url = weibo_id + ':' + file_path + ':' + url + '\n' f.write(url.encode(sys.stdout.encoding)) logger.exception(e) return False async def download_files(self, weibos, session): """下载文件(图片/视频)""" try: logger.info(u'即将进行%s下载', self.describe) for w in tqdm(weibos, desc='Download progress'): if getattr(w, self.key) != u'无': await self.handle_download(getattr(w, self.key), w, session) logger.info(u'%s下载完毕,保存路径:', self.describe) logger.info(self.file_dir) except Exception as e: logger.exception(e) ================================================ FILE: weibo_spider/downloader/img_downloader.py ================================================ import os from .downloader import Downloader class ImgDownloader(Downloader): def __init__(self, file_dir, file_download_timeout): super().__init__(file_dir, file_download_timeout) self.describe = u'图片' self.key = '' async def handle_download(self, urls, w, session): """处理下载相关操作""" file_prefix = w.publish_time[:10].replace('-', '') + '_' + w.id file_dir = self.file_dir + os.sep + self.describe if not os.path.isdir(file_dir): os.makedirs(file_dir) media_key = self.key or 'original_pictures' if ',' in urls: url_list = urls.split(',') for i, url in enumerate(url_list): index = url.rfind('.') if len(url) - index >= 5: file_suffix = '.jpg' else: file_suffix = url[index:] file_name = file_prefix + '_' + str(i + 1) + file_suffix file_path = file_dir + os.sep + file_name ok = await self.download_one_file(url, file_path, w.id, session) if ok: w.media.setdefault(media_key, []).append({ 'url': url, 'path': file_path }) else: index = urls.rfind('.') if len(urls) - index > 5: file_suffix = '.jpg' else: file_suffix = urls[index:] file_name = file_prefix + file_suffix file_path = file_dir + os.sep + file_name ok = await self.download_one_file(urls, file_path, w.id, session) if ok: w.media.setdefault(media_key, []).append({ 'url': urls, 'path': file_path }) ================================================ FILE: weibo_spider/downloader/origin_picture_downloader.py ================================================ from .img_downloader import ImgDownloader class OriginPictureDownloader(ImgDownloader): def __init__(self, file_dir, file_download_timeout): super().__init__(file_dir, file_download_timeout) self.describe = u'原创微博图片' self.key = 'original_pictures' ================================================ FILE: weibo_spider/downloader/retweet_picture_downloader.py ================================================ from .img_downloader import ImgDownloader class RetweetPictureDownloader(ImgDownloader): def __init__(self, file_dir, file_download_timeout): super().__init__(file_dir, file_download_timeout) self.describe = u'转发微博图片' self.key = 'retweet_pictures' ================================================ FILE: weibo_spider/downloader/video_downloader.py ================================================ import os from .downloader import Downloader class VideoDownloader(Downloader): def __init__(self, file_dir, file_download_timeout): super().__init__(file_dir, file_download_timeout) self.describe = u'视频' self.key = 'video_url' async def handle_download(self, urls, w, session): """处理下载相关操作""" file_prefix = w.publish_time[:10].replace('-', '') + '_' + w.id file_suffix = '.mp4' file_name = file_prefix + file_suffix file_path = self.file_dir + os.sep + file_name ok = await self.download_one_file(urls, file_path, w.id, session) if ok: w.media.setdefault('video', []).append({ 'url': urls, 'path': file_path }) ================================================ FILE: weibo_spider/logging.conf ================================================ [loggers] keys=root,spider [handlers] keys=consoleHandler,fileHandler,errorHandler [formatters] keys=consoleFormatter,fileFormatter,errorFormatter [logger_root] level=DEBUG handlers=consoleHandler,fileHandler,errorHandler [logger_spider] level=DEBUG handlers=consoleHandler,fileHandler,errorHandler qualname=spider propagate=0 [handler_consoleHandler] class=StreamHandler level=DEBUG formatter=consoleFormatter args=(sys.stdout,) [handler_fileHandler] class=handlers.TimedRotatingFileHandler level=INFO formatter=fileFormatter args=('all.log', 'D', 1, 5, 'utf-8', False, False) [handler_errorHandler] class=FileHandler level=WARNING formatter=errorFormatter args=('error.log', 'a','utf-8') [formatter_consoleFormatter] format=%(message)s [formatter_fileFormatter] format=%(asctime)s - %(filename)s - %(levelname)s - %(message)s [formatter_errorFormatter] format=%(asctime)s - %(levelname)s - %(filename)s[:%(lineno)d] - %(message)s ================================================ FILE: weibo_spider/parser/__init__.py ================================================ from .index_parser import IndexParser from .page_parser import PageParser from .photo_parser import PhotoParser from .album_parser import AlbumParser __all__ = [IndexParser, PageParser, PhotoParser, AlbumParser] ================================================ FILE: weibo_spider/parser/album_parser.py ================================================ from .parser import Parser from .util import handle_html class AlbumParser(Parser): def __init__(self, cookie, album_url): self.cookie = cookie self.url = album_url self.selector = handle_html(self.cookie, self.url) def extract_pic_urls(self): # pic_list = self.selector.xpath('//div[@class="c"]//img/@src') for i, pic in enumerate(pic_list): if "?" in pic: pic = pic[:pic.index("?")] pic_list[i] = pic return pic_list ================================================ FILE: weibo_spider/parser/comment_parser.py ================================================ import logging import random import requests import re from time import sleep from lxml.html import tostring from lxml.html import fromstring from lxml import etree from .parser import Parser from .util import handle_garbled, handle_html logger = logging.getLogger('spider.comment_parser') class CommentParser(Parser): def __init__(self, cookie, weibo_id): self.cookie = cookie self.url = 'https://weibo.cn/comment/' + weibo_id self.selector = handle_html(self.cookie, self.url) def get_long_weibo(self): """获取长原创微博""" try: for i in range(5): self.selector = handle_html(self.cookie, self.url) if self.selector is not None: info_div = self.selector.xpath("//div[@class='c' and @id='M_']")[0] info_span = info_div.xpath("//span[@class='ctt']")[0] # 1. 获取 info_span 中的所有 HTML 代码作为字符串 html_string = etree.tostring(info_span, encoding='unicode', method='html') # 2. 将
替换为 \n html_string = html_string.replace('
', '\n') # 3. 去掉所有 HTML 标签,但保留标签内的有效文本 new_content = fromstring(html_string).text_content() # 4. 替换多个连续的 \n 为一个 \n new_content = re.sub(r'\n+\s*', '\n', new_content) weibo_content = handle_garbled(new_content) if weibo_content is not None: return weibo_content sleep(random.randint(6, 10)) except Exception: logger.exception(u'网络出错') def get_long_retweet(self): """获取长转发微博""" return self.get_long_weibo() def get_video_page_url(self): """获取微博视频页面的链接""" video_url = '' try: self.selector = handle_html(self.cookie, self.url) if self.selector is not None: # 来自微博视频号的格式与普通格式不一致,不加 span 层级 links = self.selector.xpath("body/div[@class='c' and @id][1]/div//a") for a in links: if 'm.weibo.cn/s/video/show?object_id=' in a.xpath( '@href')[0]: video_url = a.xpath('@href')[0] break except Exception: logger.exception(u'网络出错') return video_url ================================================ FILE: weibo_spider/parser/index_parser.py ================================================ import logging from .info_parser import InfoParser from .parser import Parser from .util import handle_html, handle_html_async, string_to_int logger = logging.getLogger('spider.index_parser') class IndexParser(Parser): def __init__(self, cookie, user_uri, selector=None): self.cookie = cookie self.user_uri = user_uri self.url = 'https://weibo.cn/%s/profile' % (user_uri) self.selector = selector if selector is not None else handle_html(self.cookie, self.url) def _get_user_id(self): """获取用户id,使用者输入的user_id不一定是正确的,可能是个性域名等,需要获取真正的user_id""" user_id = self.user_uri url_list = self.selector.xpath("//div[@class='u']//a") for url in url_list: if (url.xpath('string(.)')) == u'资料': if url.xpath('@href') and url.xpath('@href')[0].endswith( '/info'): link = url.xpath('@href')[0] user_id = link[1:-5] break return user_id def get_user(self): """获取用户信息、微博数、关注数、粉丝数""" try: user_id = self._get_user_id() self.user = InfoParser(self.cookie, user_id).extract_user_info() # 获取用户信息 self.user.id = user_id user_info = self.selector.xpath("//div[@class='tip2']/*/text()") self.user.weibo_num = string_to_int(user_info[0][3:-1]) self.user.following = string_to_int(user_info[1][3:-1]) self.user.followers = string_to_int(user_info[2][3:-1]) return self.user except Exception as e: logger.exception(e) async def get_user_async(self, session): """获取用户信息、微博数、关注数、粉丝数""" try: user_id = self._get_user_id() from .util import handle_html_async # Local import if needed or top level info_url = 'https://weibo.cn/%s/info' % (user_id) info_selector = await handle_html_async(self.cookie, info_url, session) self.user = InfoParser(self.cookie, user_id, selector=info_selector).extract_user_info() # 获取用户信息 self.user.id = user_id user_info = self.selector.xpath("//div[@class='tip2']/*/text()") self.user.weibo_num = string_to_int(user_info[0][3:-1]) self.user.following = string_to_int(user_info[1][3:-1]) self.user.followers = string_to_int(user_info[2][3:-1]) return self.user except Exception as e: logger.exception(e) def get_page_num(self): """获取微博总页数""" try: if self.selector.xpath("//input[@name='mp']") == []: page_num = 1 else: page_num = (int)(self.selector.xpath("//input[@name='mp']") [0].attrib['value']) return page_num except Exception as e: logger.exception(e) ================================================ FILE: weibo_spider/parser/info_parser.py ================================================ import logging import sys from ..user import User from .parser import Parser from .util import handle_html logger = logging.getLogger('spider.info_parser') class InfoParser(Parser): def __init__(self, cookie, user_id, selector=None): self.cookie = cookie self.url = 'https://weibo.cn/%s/info' % (user_id) self.selector = selector if selector is not None else handle_html(self.cookie, self.url) def extract_user_info(self): """提取用户信息""" try: user = User() nickname = self.selector.xpath('//title/text()')[0] nickname = nickname[:-3] if nickname == u'登录 - 新' or nickname == u'新浪': logger.warning(u'cookie错误或已过期,请按照README中方法重新获取') sys.exit() user.nickname = nickname basic_info = self.selector.xpath("//div[@class='c'][3]/text()") zh_list = [u'性别', u'地区', u'生日', u'简介', u'认证', u'达人'] en_list = [ 'gender', 'location', 'birthday', 'description', 'verified_reason', 'talent' ] for i in basic_info: if i.split(':', 1)[0] in zh_list: setattr(user, en_list[zh_list.index(i.split(':', 1)[0])], i.split(':', 1)[1].replace('\u3000', '')) experienced = self.selector.xpath("//div[@class='tip'][2]/text()") if experienced and experienced[0] == u'学习经历': user.education = self.selector.xpath( "//div[@class='c'][4]/text()")[0][1:].replace( u'\xa0', u' ') if self.selector.xpath( "//div[@class='tip'][3]/text()")[0] == u'工作经历': user.work = self.selector.xpath( "//div[@class='c'][5]/text()")[0][1:].replace( u'\xa0', u' ') elif experienced and experienced[0] == u'工作经历': user.work = self.selector.xpath( "//div[@class='c'][4]/text()")[0][1:].replace( u'\xa0', u' ') return user except Exception as e: logger.exception(e) ================================================ FILE: weibo_spider/parser/mblog_picAll_parser.py ================================================ from .parser import Parser from .util import handle_html class MblogPicAllParser(Parser): def __init__(self, cookie, weibo_id): self.cookie = cookie self.url = 'https://weibo.cn/mblog/picAll/' + weibo_id + '?rl=1' self.selector = handle_html(self.cookie, self.url) def extract_preview_picture_list(self): return self.selector.xpath('//img/@src') ================================================ FILE: weibo_spider/parser/page_parser.py ================================================ import logging import re import sys from datetime import datetime, timedelta from .. import datetime_util from ..weibo import Weibo from .comment_parser import CommentParser from .mblog_picAll_parser import MblogPicAllParser from .parser import Parser from .util import handle_garbled, handle_html, to_video_download_url MAX_PINNED_COUNT = 2 logger = logging.getLogger('spider.page_parser') class PageParser(Parser): empty_count = 0 def __init__(self, cookie, user_config, page, filter, selector=None, defer_fetch=False): self.cookie = cookie if hasattr(PageParser, 'user_uri') and self.user_uri != user_config['user_uri']: PageParser.empty_count = 0 self.user_uri = user_config['user_uri'] self.since_date = user_config['since_date'] self.end_date = user_config['end_date'] self.page = page self.url = 'https://weibo.cn/%s/profile?page=%d' % (self.user_uri, page) if self.end_date != 'now': since_date = self.since_date.split(' ')[0].split('-') end_date = self.end_date.split(' ')[0].split('-') for date in [since_date, end_date]: for i in [1, 2]: if len(date[i]) == 1: date[i] = '0' + date[i] starttime = ''.join(since_date) endtime = ''.join(end_date) self.url = 'https://weibo.cn/%s/profile?starttime=%s&endtime=%s&advancedfilter=1&page=%d' % ( self.user_uri, starttime, endtime, page) self.selector = selector self.to_continue = True is_exist = '' if self.selector: info = self.selector.xpath("//div[@class='c']") if info and len(info) > 0: is_exist = info[0].xpath("div/span[@class='ctt']") elif not defer_fetch: for i in range(3): self.selector = handle_html(self.cookie, self.url) if self.selector: info = self.selector.xpath("//div[@class='c']") if info is None or len(info) == 0: continue is_exist = info[0].xpath("div/span[@class='ctt']") if is_exist: PageParser.empty_count = 0 break if not defer_fetch: if not is_exist: PageParser.empty_count += 1 if PageParser.empty_count > 2: self.to_continue = False PageParser.empty_count = 0 self.filter = filter def get_one_page(self, weibo_id_list): """获取第page页的全部微博""" cur_pinned_count = 0 try: info = self.selector.xpath("//div[@class='c']") is_exist = info[0].xpath("div/span[@class='ctt']") weibos = [] if is_exist: since_date = datetime_util.str_to_time(self.since_date) for i in range(0, len(info) - 1): weibo = self.get_one_weibo(info[i]) if weibo: if weibo.id in weibo_id_list: continue publish_time = datetime_util.str_to_time( weibo.publish_time) if publish_time < since_date: # As of 2023.05, there can be at most 2 pinned weibo. # We will continue for at most 2 times before return. if self.page == 1 and cur_pinned_count < MAX_PINNED_COUNT: cur_pinned_count += 1 continue else: return weibos, weibo_id_list, False logger.info(weibo) logger.info('-' * 100) weibos.append(weibo) weibo_id_list.append(weibo.id) return weibos, weibo_id_list, self.to_continue except Exception as e: logger.exception(e) def is_original(self, info): """判断微博是否为原创微博""" is_original = info.xpath("div/span[@class='cmt']") if len(is_original) > 3: return False else: return True def get_original_weibo(self, info, weibo_id): """获取原创微博""" try: weibo_content = handle_garbled(info) weibo_content = weibo_content[:weibo_content.rfind(u'赞')] a_text = info.xpath('div//a/text()') if u'全文' in a_text: wb_content = CommentParser(self.cookie, weibo_id).get_long_weibo() if wb_content: weibo_content = wb_content return weibo_content except Exception as e: logger.exception(e) def get_retweet(self, info, weibo_id): """获取转发微博""" try: weibo_content = handle_garbled(info) weibo_content = weibo_content[weibo_content.find(':') + 1:weibo_content.rfind(u'赞')] weibo_content = weibo_content[:weibo_content.rfind(u'赞')] a_text = info.xpath('div//a/text()') if u'全文' in a_text: wb_content = CommentParser(self.cookie, weibo_id).get_long_retweet() if wb_content: weibo_content = wb_content retweet_reason = handle_garbled(info.xpath('div')[-1]) retweet_reason = retweet_reason[:retweet_reason.rindex(u'赞')] original_user = info.xpath("div/span[@class='cmt']/a/text()") if original_user: original_user = original_user[0] weibo_content = (retweet_reason + '\n' + u'原始用户: ' + original_user + '\n' + u'转发内容: ' + weibo_content) else: weibo_content = (retweet_reason + '\n' + u'转发内容: ' + weibo_content) return weibo_content except Exception as e: logger.exception(e) def get_weibo_content(self, info, is_original): """获取微博内容""" try: weibo_id = info.xpath('@id')[0][2:] if is_original: weibo_content = self.get_original_weibo(info, weibo_id) else: weibo_content = self.get_retweet(info, weibo_id) return weibo_content except Exception as e: logger.exception(e) def get_article_url(self, info): """获取微博头条文章的url""" article_url = '' text = handle_garbled(info) if text.startswith(u'发布了头条文章') or text.startswith(u'我发表了头条文章'): url = info.xpath('.//a/@href') if url and url[0].startswith('https://weibo.com/ttarticle'): article_url = url[0] return article_url def get_publish_place(self, info): """获取微博发布位置""" try: div_first = info.xpath('div')[0] a_list = div_first.xpath('a') publish_place = u'无' for a in a_list: if ('place.weibo.com' in a.xpath('@href')[0] and a.xpath('text()')[0] == u'显示地图'): weibo_a = div_first.xpath("span[@class='ctt']/a") if len(weibo_a) >= 1: publish_place = weibo_a[-1] if (u'视频' == div_first.xpath( "span[@class='ctt']/a/text()")[-1][-2:]): if len(weibo_a) >= 2: publish_place = weibo_a[-2] else: publish_place = u'无' publish_place = handle_garbled(publish_place) break return publish_place except Exception as e: logger.exception(e) def get_publish_time(self, info): """获取微博发布时间""" try: str_time = info.xpath("div/span[@class='ct']") str_time = handle_garbled(str_time[0]) publish_time = str_time.split(u'来自')[0] if u'刚刚' in publish_time: publish_time = datetime.now().strftime('%Y-%m-%d %H:%M') elif u'分钟' in publish_time: minute = publish_time[:publish_time.find(u'分钟')] minute = timedelta(minutes=int(minute)) publish_time = (datetime.now() - minute).strftime('%Y-%m-%d %H:%M') elif u'今天' in publish_time: today = datetime.now().strftime('%Y-%m-%d') time = publish_time[3:] publish_time = today + ' ' + time if len(publish_time) > 16: publish_time = publish_time[:16] elif u'月' in publish_time: year = datetime.now().strftime('%Y') month = publish_time[0:2] day = publish_time[3:5] time = publish_time[7:12] publish_time = year + '-' + month + '-' + day + ' ' + time else: publish_time = publish_time[:16] return publish_time except Exception as e: logger.exception(e) def get_publish_tool(self, info): """获取微博发布工具""" try: str_time = info.xpath("div/span[@class='ct']") str_time = handle_garbled(str_time[0]) if len(str_time.split(u'来自')) > 1: publish_tool = str_time.split(u'来自')[1] else: publish_tool = u'无' return publish_tool except Exception as e: logger.exception(e) def get_weibo_footer(self, info): """获取微博点赞数、转发数、评论数""" try: footer = {} pattern = r'\d+' str_footer = info.xpath('div')[-1] str_footer = handle_garbled(str_footer) str_footer = str_footer[str_footer.rfind(u'赞'):] weibo_footer = re.findall(pattern, str_footer, re.M) up_num = int(weibo_footer[0]) footer['up_num'] = up_num retweet_num = int(weibo_footer[1]) footer['retweet_num'] = retweet_num comment_num = int(weibo_footer[2]) footer['comment_num'] = comment_num return footer except Exception as e: logger.exception(e) def get_picture_urls(self, info, is_original): """获取微博原始图片url""" try: weibo_id = info.xpath('@id')[0][2:] picture_urls = {} if is_original: original_pictures = self.extract_picture_urls(info, weibo_id) picture_urls['original_pictures'] = original_pictures if not self.filter: picture_urls['retweet_pictures'] = u'无' else: retweet_url = info.xpath("div/a[@class='cc']/@href")[0] retweet_id = retweet_url.split('/')[-1].split('?')[0] retweet_pictures = self.extract_picture_urls(info, retweet_id) picture_urls['retweet_pictures'] = retweet_pictures a_list = info.xpath('div[last()]/a/@href') original_picture = u'无' for a in a_list: if a.endswith(('.gif', '.jpeg', '.jpg', '.png')): original_picture = a break picture_urls['original_pictures'] = original_picture return picture_urls except Exception as e: logger.exception(e) def get_video_url(self, info): """获取微博视频url""" video_url = u'无' weibo_id = info.xpath('@id')[0][2:] try: video_page_url = '' a_text = info.xpath('./div[1]//a/text()') if u'全文' in a_text: video_page_url = CommentParser(self.cookie, weibo_id).get_video_page_url() else: # 来自微博视频号的格式与普通格式不一致,不加 span 层级 a_list = info.xpath('./div[1]//a') for a in a_list: if 'm.weibo.cn/s/video/show?object_id=' in a.xpath( '@href')[0]: video_page_url = a.xpath('@href')[0] break if video_page_url != '': video_url = to_video_download_url(self.cookie, video_page_url) except Exception as e: logger.exception(e) return video_url def get_one_weibo(self, info): """获取一条微博的全部信息""" try: weibo = Weibo() is_original = self.is_original(info) weibo.original = is_original # 是否原创微博 if (not self.filter) or is_original: weibo.id = info.xpath('@id')[0][2:] weibo.content = self.get_weibo_content(info, is_original) # 微博内容 weibo.article_url = self.get_article_url(info) # 头条文章url picture_urls = self.get_picture_urls(info, is_original) weibo.original_pictures = picture_urls[ 'original_pictures'] # 原创图片url if weibo.original_pictures != u'无': weibo.original_pictures_list = [ u.strip() for u in weibo.original_pictures.split(',') if u.strip() ] if not self.filter: weibo.retweet_pictures = picture_urls[ 'retweet_pictures'] # 转发图片url if weibo.retweet_pictures != u'无': weibo.retweet_pictures_list = [ u.strip() for u in weibo.retweet_pictures.split(',') if u.strip() ] weibo.video_url = self.get_video_url(info) # 微博视频url weibo.publish_place = self.get_publish_place(info) # 微博发布位置 weibo.publish_time = self.get_publish_time(info) # 微博发布时间 weibo.publish_tool = self.get_publish_tool(info) # 微博发布工具 footer = self.get_weibo_footer(info) weibo.up_num = footer['up_num'] # 微博点赞数 weibo.retweet_num = footer['retweet_num'] # 转发数 weibo.comment_num = footer['comment_num'] # 评论数 else: weibo = None logger.info(u'正在过滤转发微博') return weibo except Exception as e: logger.exception(e) def extract_picture_urls(self, info, weibo_id): """提取微博原始图片url""" try: a_list = info.xpath('div/a/@href') first_pic = 'https://weibo.cn/mblog/pic/' + weibo_id all_pic = 'https://weibo.cn/mblog/picAll/' + weibo_id picture_urls = u'无' if first_pic in ''.join(a_list): if all_pic in ''.join(a_list): preview_picture_list = MblogPicAllParser( self.cookie, weibo_id).extract_preview_picture_list() picture_list = [ p.replace('/thumb180/', '/large/') for p in preview_picture_list ] picture_urls = ','.join(picture_list) else: if info.xpath('.//img/@src'): for link in info.xpath('div/a'): if len(link.xpath('@href')) > 0: if first_pic in link.xpath('@href')[0]: if len(link.xpath('img/@src')) > 0: preview_picture = link.xpath( 'img/@src')[0] picture_urls = preview_picture.replace( '/wap180/', '/large/') break else: logger.warning( u'爬虫微博可能被设置成了"不显示图片",请前往' u'"https://weibo.cn/account/customize/pic",修改为"显示"' ) sys.exit() return picture_urls except Exception as e: logger.exception(e) return u'无' ================================================ FILE: weibo_spider/parser/parser.py ================================================ class Parser: def __init__(self, cookie): self.cookie = cookie self.url = '' self.selector = None ================================================ FILE: weibo_spider/parser/photo_parser.py ================================================ from .parser import Parser from .util import handle_html class PhotoParser(Parser): def __init__(self, cookie, user_id): self.cookie = cookie self.url = "https://weibo.cn/" + str(user_id) + "/photo?tf=6_008" self.selector = handle_html(self.cookie, self.url) self.user_id = user_id def extract_avatar_album_url(self): # Finds the href attribute of the table td div element with text 头像相册, e.g. # 头像相册 result = self.selector.xpath('//img[@alt="头像相册"]/../@href') if len(result) > 0: return "https://weibo.cn" + result[0] else: return "https://weibo.cn/" + str(self.user_id) + "/avatar?rl=0" ================================================ FILE: weibo_spider/parser/util.py ================================================ import hashlib import json import logging import sys import aiohttp import requests from lxml import etree # Set GENERATE_TEST_DATA to True when generating test data. GENERATE_TEST_DATA = False TEST_DATA_DIR = 'tests/testdata' URL_MAP_FILE = 'url_map.json' logger = logging.getLogger('spider.util') def hash_url(url): return hashlib.sha224(url.encode('utf8')).hexdigest() async def handle_html_async(cookie, url, session): """异步处理html""" try: user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36' headers = {'User-Agent': user_agent, 'Cookie': cookie} async with session.get(url, headers=headers) as resp: content = await resp.read() if GENERATE_TEST_DATA: import io import os resp_file = os.path.join(TEST_DATA_DIR, '%s.html' % hash_url(url)) with io.open(resp_file, 'wb') as f: f.write(content) with io.open(os.path.join(TEST_DATA_DIR, URL_MAP_FILE), 'r+') as f: url_map = json.loads(f.read()) url_map[url] = resp_file f.seek(0) f.write(json.dumps(url_map, indent=4, ensure_ascii=False)) f.truncate() selector = etree.HTML(content) return selector except Exception as e: logger.exception(e) def handle_html(cookie, url): """处理html""" try: user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36' headers = {'User-Agent': user_agent, 'Cookie': cookie} resp = requests.get(url, headers=headers) if GENERATE_TEST_DATA: import io import os resp_file = os.path.join(TEST_DATA_DIR, '%s.html' % hash_url(url)) with io.open(resp_file, 'w', encoding='utf-8') as f: f.write(resp.text) with io.open(os.path.join(TEST_DATA_DIR, URL_MAP_FILE), 'r+') as f: url_map = json.loads(f.read()) url_map[url] = resp_file f.seek(0) f.write(json.dumps(url_map, indent=4, ensure_ascii=False)) f.truncate() selector = etree.HTML(resp.content) return selector except Exception as e: logger.exception(e) def handle_garbled(info): """处理乱码""" try: if hasattr(info, 'xpath'): # 检查 info 是否具有 xpath 方法 info_str = info.xpath('string(.)') # 提取字符串内容 else: info_str = str(info) # 若不支持 xpath,将其转换为字符串 info = info_str.replace(u'\u200b', '').encode( sys.stdout.encoding, 'ignore').decode(sys.stdout.encoding) return info except Exception as e: logger.exception(e) return u'无' def bid2mid(bid): """convert string bid to string mid""" alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' base = len(alphabet) bidlen = len(bid) head = bidlen % 4 digit = int((bidlen - head) / 4) dlist = [bid[0:head]] for d in range(1, digit + 1): dlist.append(bid[head:head + d * 4]) head += 4 mid = '' for d in dlist: num = 0 idx = 0 strlen = len(d) for char in d: power = (strlen - (idx + 1)) num += alphabet.index(char) * (base**power) idx += 1 strnum = str(num) while (len(d) == 4 and len(strnum) < 7): strnum = '0' + strnum mid += strnum return mid def to_video_download_url(cookie, video_page_url): if video_page_url == '': return '' video_object_url = video_page_url.replace('m.weibo.cn/s/video/show', 'm.weibo.cn/s/video/object') try: user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36' headers = {'User_Agent': user_agent, 'Cookie': cookie} wb_info = requests.get(video_object_url, headers=headers).json() video_url = wb_info['data']['object']['stream'].get('hd_url') if not video_url: video_url = wb_info['data']['object']['stream']['url'] if not video_url: # 说明该视频为直播 video_url = '' except json.decoder.JSONDecodeError: logger.warning(u'当前账号没有浏览该视频的权限') return video_url def string_to_int(string): """字符串转换为整数""" if len(string) == 0: logger.warning("string to int, the input string is empty!") return 0 if isinstance(string, int): return string elif string.endswith(u'万+'): string = string[:-2] + '0000' elif string.endswith(u'万'): string = float(string[:-1]) * 10000 elif string.endswith(u'亿'): string = float(string[:-1]) * 100000000 return int(string) ================================================ FILE: weibo_spider/spider.py ================================================ #!/usr/bin/env python # -*- coding: UTF-8 -*- import json import logging import logging.config import os import random import shutil import sys import asyncio import aiohttp from datetime import date, datetime, timedelta from time import sleep from absl import app, flags from tqdm import tqdm from . import config_util, datetime_util from .downloader import AvatarPictureDownloader from .parser import AlbumParser, IndexParser, PageParser, PhotoParser from .parser.util import handle_html_async from .user import User FLAGS = flags.FLAGS flags.DEFINE_string('config_path', None, 'The path to config.json.') flags.DEFINE_string('u', None, 'The user_id we want to input.') flags.DEFINE_string('user_id_list', None, 'The path to user_id_list.txt.') flags.DEFINE_string('output_dir', None, 'The dir path to store results.') logging_path = os.path.split( os.path.realpath(__file__))[0] + os.sep + 'logging.conf' logging.config.fileConfig(logging_path) logger = logging.getLogger('spider') class Spider: def __init__(self, config): """Weibo类初始化""" self.filter = config[ 'filter'] # 取值范围为0、1,程序默认值为0,代表要爬取用户的全部微博,1代表只爬取用户的原创微博 since_date = config['since_date'] if isinstance(since_date, int): since_date = date.today() - timedelta(since_date) self.since_date = str( since_date) # 起始时间,即爬取发布日期从该值到结束时间的微博,形式为yyyy-mm-dd self.end_date = config[ 'end_date'] # 结束时间,即爬取发布日期从起始时间到该值的微博,形式为yyyy-mm-dd,特殊值"now"代表现在 random_wait_pages = config['random_wait_pages'] self.random_wait_pages = [ min(random_wait_pages), max(random_wait_pages) ] # 随机等待频率,即每爬多少页暂停一次 random_wait_seconds = config['random_wait_seconds'] self.random_wait_seconds = [ min(random_wait_seconds), max(random_wait_seconds) ] # 随机等待时间,即每次暂停要sleep多少秒 self.global_wait = config['global_wait'] # 配置全局等待时间,如每爬1000页等待3600秒等 self.page_count = 0 # 统计每次全局等待后,爬取了多少页,若页数满足全局等待要求就进入下一次全局等待 self.write_mode = config[ 'write_mode'] # 结果信息保存类型,为list形式,可包含txt、csv、json、mongo和mysql五种类型 self.pic_download = config[ 'pic_download'] # 取值范围为0、1,程序默认值为0,代表不下载微博原始图片,1代表下载 self.video_download = config[ 'video_download'] # 取值范围为0、1,程序默认为0,代表不下载微博视频,1代表下载 self.file_download_timeout = config.get( 'file_download_timeout', [5, 5, 10 ]) # 控制文件下载“超时”时的操作,值是list形式,包含三个数字,依次分别是最大超时重试次数、最大连接时间和最大读取时间 self.result_dir_name = config.get( 'result_dir_name', 0) # 结果目录名,取值为0或1,决定结果文件存储在用户昵称文件夹里还是用户id文件夹里 self.cookie = config['cookie'] self.mysql_config = config.get('mysql_config') # MySQL数据库连接配置,可以不填 self.sqlite_config = config.get('sqlite_config') self.kafka_config = config.get('kafka_config') self.mongo_config = config.get('mongo_config') self.post_config = config.get('post_config') self.user_config_file_path = '' user_id_list = config['user_id_list'] if FLAGS.user_id_list: user_id_list = FLAGS.user_id_list if not isinstance(user_id_list, list): if not os.path.isabs(user_id_list): user_id_list = os.getcwd() + os.sep + user_id_list if not os.path.isfile(user_id_list): logger.warning('不存在%s文件', user_id_list) sys.exit() self.user_config_file_path = user_id_list if FLAGS.u: user_id_list = FLAGS.u.split(',') if isinstance(user_id_list, list): # 第一部分是处理dict类型的 # 第二部分是其他类型,其他类型提供去重功能 user_config_list = list( map( lambda x: { 'user_uri': x['id'], 'since_date': x.get('since_date', self.since_date), 'end_date': x.get('end_date', self.end_date), }, [user_id for user_id in user_id_list if isinstance(user_id, dict)])) + list( map( lambda x: { 'user_uri': x, 'since_date': self.since_date, 'end_date': self.end_date }, set([ user_id for user_id in user_id_list if not isinstance(user_id, dict) ]))) if FLAGS.u: config_util.add_user_uri_list(self.user_config_file_path, user_id_list) else: user_config_list = config_util.get_user_config_list( user_id_list, self.since_date) for user_config in user_config_list: user_config['end_date'] = self.end_date self.user_config_list = user_config_list # 要爬取的微博用户的user_config列表 self.user_config = {} # 用户配置,包含用户id和since_date self.new_since_date = '' # 完成某用户爬取后,自动生成对应用户新的since_date self.user = User() # 存储爬取到的用户信息 self.got_num = 0 # 存储爬取到的微博数 self.weibo_id_list = [] # 存储爬取到的所有微博id self.session = None # aiohttp session async def write_weibo(self, weibos): """将爬取到的信息写入文件或数据库""" for downloader in self.downloaders: await downloader.download_files(weibos, self.session) for writer in self.writers: writer.write_weibo(weibos) def write_user(self, user): """将用户信息写入数据库""" for writer in self.writers: writer.write_user(user) async def get_user_info(self, user_uri): """获取用户信息""" url = 'https://weibo.cn/%s/profile' % (user_uri) selector = await handle_html_async(self.cookie, url, self.session) self.user = await IndexParser(self.cookie, user_uri, selector=selector).get_user_async(self.session) self.page_count += 1 async def download_user_avatar(self, user_uri): """下载用户头像""" # Note: This remains synchronous for now as it's a minor part of the flow avatar_album_url = PhotoParser(self.cookie, user_uri).extract_avatar_album_url() pic_urls = AlbumParser(self.cookie, avatar_album_url).extract_pic_urls() await AvatarPictureDownloader( self._get_filepath('img'), self.file_download_timeout).handle_download(pic_urls, self.session) async def get_weibo_info(self): """获取微博信息""" try: since_date = datetime_util.str_to_time( self.user_config['since_date']) now = datetime.now() if since_date <= now: # Async fetch page num user_uri = self.user_config['user_uri'] url = 'https://weibo.cn/%s/profile' % (user_uri) selector = await handle_html_async(self.cookie, url, self.session) page_num = IndexParser(self.cookie, user_uri, selector=selector).get_page_num() self.page_count += 1 if self.page_count > 2 and (self.page_count + page_num) > self.global_wait[0][0]: wait_seconds = int( self.global_wait[0][1] * min(1, self.page_count / self.global_wait[0][0])) logger.info(u'即将进入全局等待时间,%d秒后程序继续执行' % wait_seconds) for i in tqdm(range(wait_seconds)): await asyncio.sleep(1) self.page_count = 0 self.global_wait.append(self.global_wait.pop(0)) page1 = 0 random_pages = random.randint(*self.random_wait_pages) for page in tqdm(range(1, page_num + 1), desc='Progress'): # Get URL from parser without fetching parser_temp = PageParser( self.cookie, self.user_config, page, self.filter, defer_fetch=True) # Async fetch with retry selector = None for _ in range(3): selector = await handle_html_async(self.cookie, parser_temp.url, self.session) if selector is not None: info = selector.xpath("//div[@class='c']") if info and len(info) > 0: break parser = PageParser(self.cookie, self.user_config, page, self.filter, selector=selector) weibos, self.weibo_id_list, to_continue = parser.get_one_page(self.weibo_id_list) logger.info( u'%s已获取%s(%s)的第%d页微博%s', '-' * 30, self.user.nickname, self.user.id, page, '-' * 30, ) self.page_count += 1 if weibos: yield weibos if not to_continue: break if (page - page1) % random_pages == 0 and page < page_num: await asyncio.sleep(random.randint(*self.random_wait_seconds)) page1 = page random_pages = random.randint(*self.random_wait_pages) if self.page_count >= self.global_wait[0][0]: logger.info(u'即将进入全局等待时间,%d秒后程序继续执行' % self.global_wait[0][1]) for i in tqdm(range(self.global_wait[0][1])): await asyncio.sleep(1) self.page_count = 0 self.global_wait.append(self.global_wait.pop(0)) if self.user_config_file_path or FLAGS.u: config_util.update_user_config_file( self.user_config_file_path, self.user_config['user_uri'], self.user.nickname, self.new_since_date, ) except Exception as e: logger.exception(e) def _get_filepath(self, type): """获取结果文件路径""" try: dir_name = self.user.nickname if self.result_dir_name: dir_name = self.user.id if FLAGS.output_dir is not None: file_dir = FLAGS.output_dir + os.sep + dir_name else: file_dir = (os.getcwd() + os.sep + 'weibo' + os.sep + dir_name) if type == 'img' or type == 'video': file_dir = file_dir + os.sep + type if not os.path.isdir(file_dir): os.makedirs(file_dir) if type == 'img' or type == 'video': return file_dir file_path = file_dir + os.sep + self.user.id + '.' + type return file_path except Exception as e: logger.exception(e) def initialize_info(self, user_config): """初始化爬虫信息""" self.got_num = 0 self.user_config = user_config self.weibo_id_list = [] if self.end_date == 'now': self.new_since_date = datetime.now().strftime('%Y-%m-%d %H:%M') else: self.new_since_date = self.end_date self.writers = [] if 'csv' in self.write_mode: from .writer import CsvWriter self.writers.append( CsvWriter(self._get_filepath('csv'), self.filter)) if 'txt' in self.write_mode: from .writer import TxtWriter self.writers.append( TxtWriter(self._get_filepath('txt'), self.filter)) if 'json' in self.write_mode: from .writer import JsonWriter self.writers.append(JsonWriter(self._get_filepath('json'))) if 'mysql' in self.write_mode: from .writer import MySqlWriter self.writers.append(MySqlWriter(self.mysql_config)) if 'mongo' in self.write_mode: from .writer import MongoWriter self.writers.append(MongoWriter(self.mongo_config)) if 'sqlite' in self.write_mode: from .writer import SqliteWriter self.writers.append(SqliteWriter(self.sqlite_config)) if 'kafka' in self.write_mode: from .writer import KafkaWriter self.writers.append(KafkaWriter(self.kafka_config)) if 'post' in self.write_mode: from .writer import PostWriter self.writers.append(PostWriter(self.post_config)) self.downloaders = [] if self.pic_download == 1: from .downloader import ( OriginPictureDownloader, RetweetPictureDownloader) self.downloaders.append( OriginPictureDownloader(self._get_filepath('img'), self.file_download_timeout)) if self.pic_download and not self.filter: self.downloaders.append( RetweetPictureDownloader(self._get_filepath('img'), self.file_download_timeout)) if self.video_download == 1: from .downloader import VideoDownloader self.downloaders.append( VideoDownloader(self._get_filepath('video'), self.file_download_timeout)) async def get_one_user(self, user_config): """获取一个用户的微博""" try: await self.get_user_info(user_config['user_uri']) logger.info(self.user) logger.info('*' * 100) self.initialize_info(user_config) self.write_user(self.user) logger.info('*' * 100) # 下载用户头像相册中的图片。 if self.pic_download: await self.download_user_avatar(user_config['user_uri']) async for weibos in self.get_weibo_info(): await self.write_weibo(weibos) self.got_num += len(weibos) if not self.filter: logger.info(u'共爬取' + str(self.got_num) + u'条微博') else: logger.info(u'共爬取' + str(self.got_num) + u'条原创微博') logger.info(u'信息抓取完毕') logger.info('*' * 100) except Exception as e: logger.exception(e) async def start(self): """运行爬虫""" try: if not self.user_config_list: logger.info( u'没有配置有效的user_id,请通过config.json或user_id_list.txt配置user_id') return async with aiohttp.ClientSession() as session: self.session = session user_count = 0 user_count1 = random.randint(*self.random_wait_pages) random_users = random.randint(*self.random_wait_pages) for user_config in self.user_config_list: if (user_count - user_count1) % random_users == 0: await asyncio.sleep(random.randint(*self.random_wait_seconds)) user_count1 = user_count random_users = random.randint(*self.random_wait_pages) user_count += 1 await self.get_one_user(user_config) except Exception as e: logger.exception(e) def _get_config(): """获取config.json数据""" src = os.path.split( os.path.realpath(__file__))[0] + os.sep + 'config_sample.json' config_path = os.getcwd() + os.sep + 'config.json' if FLAGS.config_path: config_path = FLAGS.config_path elif not os.path.isfile(config_path): shutil.copy(src, config_path) logger.info(u'请先配置当前目录(%s)下的config.json文件,' u'如果想了解config.json参数的具体意义及配置方法,请访问\n' u'https://github.com/dataabc/weiboSpider#2程序设置' % os.getcwd()) sys.exit() try: with open(config_path) as f: try: config_util.check_cookie(config_path) except Exception: logger.info("Using the cookie field in config.json as the request cookie.") config = json.loads(f.read()) return config except ValueError: logger.error(u'config.json 格式不正确,请访问 ' u'https://github.com/dataabc/weiboSpider#2程序设置') sys.exit() async def async_main(_): try: config = _get_config() config_util.validate_config(config) wb = Spider(config) await wb.start() # 爬取微博信息 except Exception as e: logger.exception(e) def main(_): asyncio.run(async_main(_)) if __name__ == '__main__': app.run(main) ================================================ FILE: weibo_spider/user.py ================================================ class User: __slots__ = ( 'id', 'nickname', 'gender', 'location', 'birthday', 'description', 'verified_reason', 'talent', 'education', 'work', 'weibo_num', 'following', 'followers' ) def __init__(self): self.id = '' self.nickname = '' self.gender = '' self.location = '' self.birthday = '' self.description = '' self.verified_reason = '' self.talent = '' self.education = '' self.work = '' self.weibo_num = 0 self.following = 0 self.followers = 0 def to_dict(self): """将对象转换为字典""" return {slot: getattr(self, slot) for slot in self.__slots__ if hasattr(self, slot)} def __str__(self): """打印微博用户信息""" result = '' result += u'用户昵称: %s\n' % self.nickname result += u'用户id: %s\n' % self.id result += u'微博数: %d\n' % self.weibo_num result += u'关注数: %d\n' % self.following result += u'粉丝数: %d\n' % self.followers return result ================================================ FILE: weibo_spider/user_id_list.txt ================================================ 1669879400 Dear-迪丽热巴 2020-01-13 19:18 1223178222 胡歌 2020-01-13 19:28 1729370543 郭碧婷 2020-01-13 19:33 ================================================ FILE: weibo_spider/weibo.py ================================================ class Weibo: __slots__ = ( 'id', 'user_id', 'content', 'article_url', 'original_pictures', 'retweet_pictures', 'original', 'video_url', 'original_pictures_list', 'retweet_pictures_list', 'media', 'publish_place', 'publish_time', 'publish_tool', 'up_num', 'retweet_num', 'comment_num' ) def to_dict(self): """将对象转换为字典""" return {slot: getattr(self, slot) for slot in self.__slots__ if hasattr(self, slot)} def __init__(self): self.id = '' self.user_id = '' self.content = '' self.article_url = '' self.original_pictures = [] self.retweet_pictures = [] self.original = True self.video_url = '' self.original_pictures_list = [] self.retweet_pictures_list = [] self.media = {} self.publish_place = '' self.publish_time = '' self.publish_tool = '' self.up_num = 0 self.retweet_num = 0 self.comment_num = 0 def __str__(self): """打印一条微博""" result = self.content + '\n' result += u'微博发布位置:%s\n' % self.publish_place result += u'发布时间:%s\n' % self.publish_time result += u'发布工具:%s\n' % self.publish_tool result += u'点赞数:%d\n' % self.up_num result += u'转发数:%d\n' % self.retweet_num result += u'评论数:%d\n' % self.comment_num result += u'url:https://weibo.cn/comment/%s\n' % self.id return result ================================================ FILE: weibo_spider/writer/__init__.py ================================================ from .csv_writer import CsvWriter from .json_writer import JsonWriter from .mongo_writer import MongoWriter from .mysql_writer import MySqlWriter from .txt_writer import TxtWriter from .sqlite_writer import SqliteWriter from .kafka_writer import KafkaWriter from .post_writer import PostWriter __all__ = [CsvWriter, TxtWriter, JsonWriter, MongoWriter, MySqlWriter, SqliteWriter, KafkaWriter, PostWriter] ================================================ FILE: weibo_spider/writer/csv_writer.py ================================================ import csv import logging from .writer import Writer logger = logging.getLogger('spider.csv_writer') class CsvWriter(Writer): def __init__(self, file_path, filter): self.file_path = file_path self.result_headers = [('微博id', 'id'), ('微博正文', 'content'), ('头条文章url', 'article_url'), ('原始图片url', 'original_pictures'), ('微博视频url', 'video_url'), ('发布位置', 'publish_place'), ('发布时间', 'publish_time'), ('发布工具', 'publish_tool'), ('点赞数', 'up_num'), ('转发数', 'retweet_num'), ('评论数', 'comment_num')] if not filter: self.result_headers.insert(4, ('被转发微博原始图片url', 'retweet_pictures')) self.result_headers.insert(5, ('是否为原创微博', 'original')) try: with open(self.file_path, 'a', encoding='utf-8-sig', newline='') as f: writer = csv.writer(f) writer.writerows([[kv[0] for kv in self.result_headers]]) except Exception as e: logger.exception(e) def write_user(self, user): self.user = user def write_weibo(self, weibos): """将爬取的信息写入csv文件""" try: result_data = [[getattr(w, kv[1]) for kv in self.result_headers] for w in weibos] with open(self.file_path, 'a', encoding='utf-8-sig', newline='') as f: writer = csv.writer(f) writer.writerows(result_data) logger.info(u'%d条微博写入csv文件完毕,保存路径:%s', len(weibos), self.file_path) except Exception as e: logger.exception(e) ================================================ FILE: weibo_spider/writer/json_writer.py ================================================ import codecs import json import logging import os from .writer import Writer logger = logging.getLogger('spider.json_writer') class JsonWriter(Writer): def __init__(self, file_path): self.file_path = file_path def write_user(self, user): self.user = user def _update_json_data(self, data, weibo_info): """更新要写入json结果文件中的数据,已经存在于json中的信息更新为最新值,不存在的信息添加到data中""" data['user'] = self.user.to_dict() if data.get('weibo'): is_new = 1 # 待写入微博是否全部为新微博,即待写入微博与json中的数据不重复 for old in data['weibo']: if weibo_info[-1]['id'] == old['id']: is_new = 0 break if is_new == 0: for new in weibo_info: flag = 1 for i, old in enumerate(data['weibo']): if new['id'] == old['id']: data['weibo'][i] = new flag = 0 break if flag: data['weibo'].append(new) else: data['weibo'] += weibo_info else: data['weibo'] = weibo_info return data def write_weibo(self, weibos): """将爬到的信息写入json文件""" data = {} if os.path.isfile(self.file_path): with codecs.open(self.file_path, 'r', encoding='utf-8') as f: data = json.load(f) data = self._update_json_data(data, [w.to_dict() for w in weibos]) with codecs.open(self.file_path, 'w', encoding='utf-8') as f: f.write(json.dumps(data, indent=4, ensure_ascii=False)) logger.info(u'%d条微博写入json文件完毕,保存路径:%s', len(weibos), self.file_path) ================================================ FILE: weibo_spider/writer/kafka_writer.py ================================================ import json import logging import sys from .writer import Writer logger = logging.getLogger('spider.kafka_writer') class KafkaWriter(Writer): def __init__(self, kafka_config): try: from kafka import KafkaProducer except ImportError: logger.warning( u'系统中可能没有安装kafka库,请先运行 pip install kafka-python ,再运行程序') sys.exit() self.kafka_config = kafka_config self.producer = KafkaProducer( bootstrap_servers=str(kafka_config['bootstrap-server']).split(','), value_serializer=lambda m: json.dumps(m, ensure_ascii=False ).encode('UTF-8')) self.weibo_topics = list(kafka_config['weibo_topics']) self.user_topics = list(kafka_config['user_topics']) logger.info('{}', kafka_config) def write_weibo(self, weibo): for w in weibo: w.user_id = self.user.id for topic in self.weibo_topics: self.producer.send(topic, value=w.to_dict()) def write_user(self, user): self.user = user for topic in self.user_topics: self.producer.send(topic, value=user.to_dict()) def __del__(self): self.producer.close() ================================================ FILE: weibo_spider/writer/mongo_writer.py ================================================ import copy import logging import sys from .writer import Writer logger = logging.getLogger('spider.mongo_writer') class MongoWriter(Writer): def __init__(self, mongo_config): self.mongo_config = mongo_config self.connection_string = mongo_config['connection_string'] self.dba_name = mongo_config.get('dba_name', None) self.dba_password = mongo_config.get('dba_password', None) def _info_to_mongodb(self, collection, info_list): """将爬取的信息写入MongoDB数据库""" try: import pymongo except ImportError: logger.warning( u'系统中可能没有安装pymongo库,请先运行 pip install pymongo ,再运行程序') sys.exit() try: from pymongo import MongoClient client = MongoClient(self.connection_string) if self.dba_name or self.dba_password: # authenticate() 在PyMongo3.6版本就已弃用,这一段可能需要后续跟进 client.admin.authenticate( self.dba_name, self.dba_password, mechanism='SCRAM-SHA-1' ) db = client['weibo'] collection = db[collection] new_info_list = copy.deepcopy(info_list) for info in new_info_list: if not collection.find_one({'id': info['id']}): collection.insert_one(info) else: collection.update_one({'id': info['id']}, {'$set': info}) except pymongo.errors.ServerSelectionTimeoutError: logger.warning( u'系统中可能没有安装或启动MongoDB数据库,请先根据系统环境安装或启动MongoDB,再运行程序') sys.exit() def write_weibo(self, weibos): """将爬取的微博信息写入MongoDB数据库""" weibo_list = [] for w in weibos: w.user_id = self.user.id weibo_list.append(w.to_dict()) self._info_to_mongodb('weibo', weibo_list) logger.info(u'%d条微博写入MongoDB数据库完毕', len(weibos)) def write_user(self, user): """将爬取的用户信息写入MongoDB数据库""" self.user = user user_list = [user.to_dict()] self._info_to_mongodb('user', user_list) logger.info(u'%s信息写入MongoDB数据库完毕', user.nickname) ================================================ FILE: weibo_spider/writer/mysql_writer.py ================================================ import copy import logging import sys from .writer import Writer logger = logging.getLogger('spider.mysql_writer') class MySqlWriter(Writer): def __init__(self, mysql_config): self.mysql_config = mysql_config # 创建'weibo'数据库 create_database = """CREATE DATABASE IF NOT EXISTS weibo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci""" self._mysql_create_database(create_database) self.mysql_config['db'] = 'weibo' def _mysql_create(self, connection, sql): """创建MySQL数据库或表""" try: with connection.cursor() as cursor: cursor.execute(sql) finally: connection.close() def _mysql_create_database(self, sql): """创建MySQL数据库""" try: import pymysql except ImportError: logger.warning( u'系统中可能没有安装pymysql库,请先运行 pip install pymysql ,再运行程序') sys.exit() try: connection = pymysql.connect(**self.mysql_config) self._mysql_create(connection, sql) except pymysql.OperationalError: logger.warning(u'系统中可能没有安装或正确配置MySQL数据库,请先根据系统环境安装或配置MySQL,再运行程序') sys.exit() def _mysql_create_table(self, sql): """创建MySQL表""" import pymysql connection = pymysql.connect(**self.mysql_config) self._mysql_create(connection, sql) def _mysql_insert(self, table, data_list): """向MySQL表插入或更新数据""" import pymysql if len(data_list) > 0: # We use this to filter out unset values. data_list = [{k: v for k, v in data.items() if v is not None} for data in data_list] keys = ', '.join(data_list[0].keys()) values = ', '.join(['%s'] * len(data_list[0])) connection = pymysql.connect(**self.mysql_config) cursor = connection.cursor() sql = """INSERT INTO {table}({keys}) VALUES ({values}) ON DUPLICATE KEY UPDATE""".format(table=table, keys=keys, values=values) update = ','.join([ ' {key} = values({key})'.format(key=key) for key in data_list[0] ]) sql += update try: cursor.executemany( sql, [tuple(data.values()) for data in data_list]) connection.commit() except Exception as e: connection.rollback() logger.exception(e) finally: connection.close() def write_weibo(self, weibos): """将爬取的微博信息写入MySQL数据库""" # 创建'weibo'表 try: create_table = """ CREATE TABLE IF NOT EXISTS weibo ( id varchar(10) NOT NULL, user_id varchar(12), content varchar(5000), article_url varchar(200), original_pictures varchar(3000), retweet_pictures varchar(3000), original BOOLEAN NOT NULL DEFAULT 1, video_url varchar(300), publish_place varchar(100), publish_time DATETIME NOT NULL, publish_tool varchar(30), up_num INT NOT NULL, retweet_num INT NOT NULL, comment_num INT NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""" self._mysql_create_table(create_table) # 在'weibo'表中插入或更新微博数据 weibo_list = [] info_list = copy.deepcopy(weibos) for weibo in info_list: weibo.user_id = self.user.id weibo_list.append(weibo.to_dict()) self._mysql_insert('weibo', weibo_list) logger.info(u'%d条微博写入MySQL数据库完毕', len(weibos)) except Exception as e: logger.exception(e) def write_user(self, user): """将爬取的用户信息写入MySQL数据库""" try: self.user = user # 创建'user'表 create_table = """ CREATE TABLE IF NOT EXISTS user ( id varchar(20) NOT NULL, nickname varchar(30), gender varchar(10), location varchar(200), birthday varchar(40), description varchar(400), verified_reason varchar(140), talent varchar(200), education varchar(200), work varchar(200), weibo_num INT, following INT, followers INT, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""" self._mysql_create_table(create_table) self._mysql_insert('user', [user.to_dict()]) logger.info(u'%s信息写入MySQL数据库完毕', user.nickname) except Exception as e: logger.exception(e) ================================================ FILE: weibo_spider/writer/post_writer.py ================================================ import codecs import json import logging import os import requests from .writer import Writer from time import sleep from requests.exceptions import RequestException logger = logging.getLogger('spider.post_writer') class PostWriter(Writer): def __init__(self, post_config): self.post_config = post_config self.api_url = post_config['api_url'] self.api_token = post_config.get('api_token', None) self.dba_password = post_config.get('dba_password', None) def write_user(self, user): self.user = user def _update_json_data(self, data, weibo_info): """将获取到的微博数据转换为json输出模式一致""" data['user'] = self.user.to_dict() if data.get('weibo'): data['weibo'] += weibo_info else: data['weibo'] = weibo_info return data def send_post_request_with_token(self, url, data, token, max_retries, backoff_factor): headers = { 'Content-Type': 'application/json', 'api-token': f'{token}', } for attempt in range(max_retries + 1): try: response = requests.post(url, json=data, headers=headers) if response.status_code == requests.codes.ok: return response.json() else: raise RequestException(f"Unexpected response status: {response.status_code}") except RequestException as e: if attempt < max_retries: sleep(backoff_factor * (attempt + 1)) # 逐步增加等待时间,避免频繁重试 continue else: logger.error(f"在尝试{max_retries}次发出POST连接后,请求失败:{e}") def write_weibo(self, weibos): """将爬到的信息POST到API""" data = {} data = self._update_json_data(data, [w.to_dict() for w in weibos]) if data: self.send_post_request_with_token(self.api_url, data, self.api_token, 3, 2) logger.info(u'%d条微博通过POST发送到 %s', len(weibos), self.api_url) else: logger.info(u'没有获取到微博,略过API POST') ================================================ FILE: weibo_spider/writer/sqlite_writer.py ================================================ import copy import logging import sys from .writer import Writer logger = logging.getLogger('spider.sqlite_writer') class SqliteWriter(Writer): def __init__(self, sqlite_config): self.sqlite_config = sqlite_config def _sqlite_create(self, connection, sql): """创建sqlite数据库或表""" try: cursor = connection.cursor() cursor.execute(sql) finally: connection.close() def _sqlite_create_table(self, sql): """创建sqlite表""" import sqlite3 connection = sqlite3.connect(self.sqlite_config) self._sqlite_create(connection, sql) def _sqlite_insert(self, table, data_list): """向sqlite表插入或更新数据""" import sqlite3 if len(data_list) > 0: # We use this to filter out unset values. data_list = [{k: v for k, v in data.items() if v is not None} for data in data_list] keys = ', '.join(data_list[0].keys()) values = ', '.join(['?'] * len(data_list[0])) connection = sqlite3.connect(self.sqlite_config) cursor = connection.cursor() sql = """INSERT OR REPLACE INTO {table}({keys}) VALUES ({values})""".format( table=table, keys=keys, values=values) try: cursor.executemany( sql, [tuple(data.values()) for data in data_list]) connection.commit() except Exception as e: connection.rollback() logger.exception(e) finally: connection.close() def write_weibo(self, weibos): """将爬取的微博信息写入sqlite数据库""" # 创建'weibo'表 create_table = """ CREATE TABLE IF NOT EXISTS weibo ( id varchar(10) NOT NULL, user_id varchar(12), content varchar(2000), article_url varchar(200), original_pictures varchar(3000), retweet_pictures varchar(3000), original BOOLEAN NOT NULL DEFAULT 1, video_url varchar(300), publish_place varchar(100), publish_time DATETIME NOT NULL, publish_tool varchar(30), up_num INT NOT NULL, retweet_num INT NOT NULL, comment_num INT NOT NULL, PRIMARY KEY (id) )""" self._sqlite_create_table(create_table) # 在'weibo'表中插入或更新微博数据 weibo_list = [] info_list = copy.deepcopy(weibos) for weibo in info_list: weibo.user_id = self.user.id weibo_list.append(weibo.to_dict()) self._sqlite_insert('weibo', weibo_list) logger.info(u'%d条微博写入sqlite数据库完毕', len(weibos)) def write_user(self, user): """将爬取的用户信息写入sqlite数据库""" self.user = user # 创建'user'表 create_table = """ CREATE TABLE IF NOT EXISTS user ( id varchar(20) NOT NULL, nickname varchar(30), gender varchar(10), location varchar(200), birthday varchar(40), description varchar(400), verified_reason varchar(140), talent varchar(200), education varchar(200), work varchar(200), weibo_num INT, following INT, followers INT, PRIMARY KEY (id) )""" self._sqlite_create_table(create_table) self._sqlite_insert('user', [user.to_dict()]) logger.info(u'%s信息写入sqlite数据库完毕', user.nickname) ================================================ FILE: weibo_spider/writer/txt_writer.py ================================================ import logging import sys from .writer import Writer logger = logging.getLogger('spider.txt_writer') class TxtWriter(Writer): def __init__(self, file_path, filter): self.file_path = file_path self.user_header = u'用户信息' self.user_desc = [('nickname', '用户昵称'), ('id', '用户id'), ('weibo_num', '微博数'), ('following', '关注数'), ('followers', '粉丝数')] if filter: self.weibo_header = u'原创微博内容' else: self.weibo_header = u'微博内容' self.weibo_desc = [('publish_place', '微博位置'), ('publish_time', '发布时间'), ('up_num', '点赞数'), ('retweet_num', '转发数'), ('comment_num', '评论数'), ('publish_tool', '发布工具')] def write_user(self, user): self.user = user user_info = '\n'.join( [v + ':' + str(getattr(self.user, k)) for k, v in self.user_desc]) with open(self.file_path, 'ab') as f: f.write((self.user_header + ':\n' + user_info + '\n\n').encode( sys.stdout.encoding)) logger.info(u'%s信息写入txt文件完毕,保存路径:%s', self.user.nickname, self.file_path) def write_weibo(self, weibo): """将爬取的信息写入txt文件""" weibo_header = '' if self.weibo_header: weibo_header = self.weibo_header + ':\n' self.weibo_header = '' try: temp_result = [] for w in weibo: temp_result.append(getattr(w, 'content') + '\n' + '\n'.join( [v + ':' + str(getattr(w, k)) for k, v in self.weibo_desc])) result = '\n\n'.join(temp_result) + '\n\n' with open(self.file_path, 'ab') as f: f.write((weibo_header + result).encode(sys.stdout.encoding)) logger.info(u'%d条微博写入txt文件完毕,保存路径:%s', len(weibo), self.file_path) except Exception as e: logger.exception(e) ================================================ FILE: weibo_spider/writer/writer.py ================================================ from abc import ABC, abstractmethod class Writer(ABC): def __init__(self): """根据需要,初始化结果路径、初始化表头、初始化数据库等""" pass @abstractmethod def write_weibo(self, weibo): """给定微博信息,写入对应文本或数据库""" pass @abstractmethod def write_user(self, user): """给定用户信息,写入对应文本或数据库""" pass