├── swcr ├── __init__.py ├── template.docx └── swcr.py ├── MANIFEST.in ├── .gitignore ├── LICENSE.txt ├── setup.py └── README.md /swcr/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE.txt -------------------------------------------------------------------------------- /swcr/template.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenley2021/swcr/HEAD/swcr/template.docx -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ 3 | dist 4 | build 5 | *.pyc 6 | swcr.egg-info 7 | swcr.spec 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright <2020> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import io 4 | import os 5 | 6 | from setuptools import setup, find_packages 7 | 8 | # Package meta-data. 9 | NAME = 'swcr' 10 | DESCRIPTION = '计算机软件著作权程序鉴别材料(即源代码)生成器' 11 | URL = 'https://github.com/kenley2021/swcr' 12 | EMAIL = 'kenley2021@gmail.com' 13 | AUTHOR = 'kenley' 14 | REQUIRES_PYTHON = '>=3.6.0' 15 | VERSION = '1.0.1' 16 | 17 | # What packages are required for this module to be executed? 18 | REQUIRED = [ 19 | 'click', 20 | 'scandir', 21 | 'python-docx', 22 | ] 23 | 24 | # What packages are optional? 25 | EXTRAS = { 26 | } 27 | 28 | 29 | # console scripts 30 | CONSOLE_SCRIPTS = [ 31 | 'swcr=swcr.swcr:main' 32 | ] 33 | 34 | 35 | # package data 36 | PACKAGE_DATA = { 37 | 'swcr': ['template.docx'] 38 | } 39 | 40 | # The rest you shouldn't have to touch too much :) 41 | # ------------------------------------------------ 42 | # Except, perhaps the License and Trove Classifiers! 43 | # If you do change the License, remember to change the Trove Classifier 44 | # for that! 45 | 46 | here = os.path.abspath(os.path.dirname(__file__)) 47 | 48 | # Import the README and use it as the long-description. 49 | # Note: this will only work if 'README.md' is present in your MANIFEST.in file! 50 | try: 51 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 52 | long_description = '\n' + f.read() 53 | except FileNotFoundError: 54 | long_description = DESCRIPTION 55 | 56 | # Load the package's __version__.py module as a dictionary. 57 | about = {} 58 | if not VERSION: 59 | project_slug = NAME.lower().replace("-", "_").replace(" ", "_") 60 | with open(os.path.join(here, project_slug, '__version__.py')) as f: 61 | exec(f.read(), about) 62 | else: 63 | about['__version__'] = VERSION 64 | 65 | # Where the magic happens: 66 | setup( 67 | name=NAME, 68 | version=about['__version__'], 69 | description=DESCRIPTION, 70 | long_description=long_description, 71 | long_description_content_type='text/markdown', 72 | author=AUTHOR, 73 | author_email=EMAIL, 74 | python_requires=REQUIRES_PYTHON, 75 | url=URL, 76 | packages=find_packages(), 77 | package_data=PACKAGE_DATA, 78 | entry_points={ 79 | 'console_scripts': CONSOLE_SCRIPTS 80 | }, 81 | install_requires=REQUIRED, 82 | extras_require=EXTRAS, 83 | include_package_data=True, 84 | license='MIT', 85 | classifiers=[ 86 | # Trove classifiers 87 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 88 | 'License :: OSI Approved :: MIT License', 89 | 'Programming Language :: Python', 90 | 'Programming Language :: Python :: 3', 91 | 'Programming Language :: Python :: 3.6', 92 | 'Programming Language :: Python :: Implementation :: CPython', 93 | 'Programming Language :: Python :: Implementation :: PyPy', 94 | ], 95 | # $ setup.py publish support. 96 | cmdclass={ 97 | }, 98 | ) 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swcr:计算机软件著作权程序鉴别材料(即源代码)生成器 2 | 3 | 目录 4 | ================= 5 | 6 | * [安装](#安装) 7 | * [背景](#背景) 8 | * [使用](#使用) 9 | * [程序鉴别材料要求](#程序鉴别材料要求) 10 | * [如何实现每页50行](#如何实现每页50行) 11 | * [参数](#参数) 12 | * [示例](#示例) 13 | * [克隆代码](#克隆代码) 14 | * [生成文档](#生成文档) 15 | * [常见问题](#常见问题) 16 | * [如何指定页眉?](#如何指定页眉?) 17 | * [如何添加其他格式的代码?](#如何添加其他格式的代码?) 18 | * [如何排除指定文件或文件夹?](#如何排除指定文件或文件夹?) 19 | * [如何调整默认的注释风格?](#如何调整默认的注释风格?) 20 | * [如何调整字体?](#如何调整字体?) 21 | * [虽然我知道默认的字体、字号、段前间距、段后间距、行间距可以实现每页50行,但是我还是想调整,怎么办?](#虽然我知道默认的字体、字号、段前间距、段后间距、行间距可以实现每页50行,但是我还是想调整,怎么办?) 22 | * [能不能输出查找文件的详细过程呢?](#能不能输出查找文件的详细过程呢?) 23 | 24 | ## 安装 25 | 26 | ```shell script 27 | pip install swcr 28 | ``` 29 | 30 | ## 背景 31 | 32 | 工作中需要申请软件著作权,软件著作权需要提供以下材料: 33 | 34 | 1. 申请表:可以在官网通过网页生成 35 | 2. 身份证明:企业的话一般就是营业执照 36 | 3. 程序鉴别材料:一般就是源代码整理出的PDF文件 37 | 4. 文档鉴别材料:一般就是该软件的操作手册 38 | 39 | 申请表身份证明比较好准备,文档鉴别材料则必须手写,`swcr`则用于生成程序鉴别材料。目前支持如下功能: 40 | 41 | 1. 指定多个源代码目录 42 | 2. 指定多中注释风格 43 | 3. 指定字体、字号、段前间距、段后间距、行距 44 | 4. 排除特定文件、文件夹 45 | 46 | ## 使用 47 | 48 | ### 程序鉴别材料要求 49 | 50 | 1. 每页至少50行 51 | 2. 不能含有注释、空行 52 | 3. 页眉部分必须包含软件名称、版本号、页码(软件名+版本号居中,页码右侧对齐) 53 | 54 | ### 如何实现每页50行 55 | 56 | 上述3点,第2、3两点比较好实现,第1点我通过测试发现,当: 57 | 58 | 1. 字号为10.5pt 59 | 2. 行间距为10.5pt 60 | 3. 段前间距为0 61 | 4. 段后间距为2.3pt 62 | 63 | 时,刚好实现每页50行。 64 | 65 | ### 参数 66 | 67 | ``` 68 | Usage: swcr [OPTIONS] 69 | 70 | Options: 71 | -t, --title TEXT 软件名称+版本号,默认为软件著作权程序鉴别材料生成器V1.0,此名称用于生成页眉 72 | -i, --indir PATH 源码所在文件夹,可以指定多个,默认为当前目录 73 | -e, --ext TEXT 源代码后缀,可以指定多个,默认为Python源代码 74 | -c, --comment-char TEXT 注释字符串,可以指定多个,默认为#、// 75 | --font-name TEXT 字体,默认为宋体 76 | --font-size FLOAT RANGE 字号,默认为五号,即10.5号 77 | --space-before FLOAT RANGE 段前间距,默认为0 78 | --space-after FLOAT RANGE 段后间距,默认为2.3 79 | --line-spacing FLOAT RANGE 行距,默认为固定值10.5 80 | --exclude PATH 需要排除的文件或路径,可以指定多个 81 | -o, --outfile PATH 输出文件(docx格式),默认为当前目录的code.docx 82 | -v, --verbose 打印调试信息 83 | --help Show this message and exit. 84 | ``` 85 | 86 | ### 示例 87 | 88 | 下面以[django-guardian项目](https://github.com/django-guardian/django-guardian)为例来说明`swcr`的用法。 89 | 90 | #### 克隆代码 91 | 92 | ```shell script 93 | git clone git@github.com:django-guardian/django-guardian.git 94 | ``` 95 | 96 | #### 生成文档 97 | 98 | ```shell script 99 | swcr -i django-guardian -o django-guardian.docx 100 | ``` 101 | 102 | ### 常见问题 103 | 104 | #### 如何指定页眉? 105 | 106 | ```shell script 107 | swcr -i django-guardian -t django-guardian -o django-guardian.docx 108 | ``` 109 | 110 | #### 如何添加其他格式的代码? 111 | 112 | 上述方法只能识别Python源码,如果需要识别html、css、js代码,可以指定`-e`参数。 113 | 114 | ```shell script 115 | swcr -i django-guardian \ 116 | -t django-guardian \ 117 | -e py -e html -e js \ 118 | -o django-guardian.docx 119 | ``` 120 | 121 | #### 如何排除指定文件或文件夹? 122 | 123 | ```shell script 124 | swcr -i django-guardian \ 125 | -t django-guardian \ 126 | --exclude django-guardian/contrib/ \ 127 | --exclude django-guardian/docs/ \ 128 | --exclude django-guardian/benchmarks/ \ 129 | --exclude django-guardian/example_project/ \ 130 | -o django-guardian.docx 131 | ``` 132 | 133 | #### 如何调整默认的注释风格? 134 | 135 | 默认情况下,`swcr`把以`#`、`//`开头的行作为注释行删除,例如我想删除以`"""`开头的行(Python另一种注释风格): 136 | 137 | ```shell script 138 | swcr -i django-guardian \ 139 | -t django-guardian \ 140 | -c '#' -c '//' -c '"""' \ 141 | -o django-guardian.docx 142 | ``` 143 | 144 | 注意,`swcr`目前不支持删除多行注释。 145 | 146 | #### 如何调整字体? 147 | 148 | `swcr`默认使用宋体,如果需要调整可以使用`--font-name`参数。 149 | 150 | ```shell script 151 | swcr -i django-guardian \ 152 | -t django-guardian \ 153 | --font-name menlo \ 154 | -o django-guardian.docx 155 | ``` 156 | 157 | #### 虽然我知道默认的字体、字号、段前间距、段后间距、行间距可以实现每页50行,但是我还是想调整,怎么办? 158 | 159 | ```shell script 160 | swcr -i django-guardian \ 161 | -t django-guardian \ 162 | --font-name menlo \ 163 | --font-size 12 \ 164 | --space-before 1 \ 165 | --space-after 5 \ 166 | --line-spacing 12 \ 167 | -o django-guardian.docx 168 | ``` 169 | 170 | #### 能不能输出查找文件的详细过程呢? 171 | 172 | ```shell script 173 | swcr -i django-guardian -o django-guardian.docx -v 174 | ``` 175 | 176 | ``` 177 | ... 178 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/posts/templates/posts目录下找到0个代码文件. 179 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/posts/templates目录下找到0个代码文件. 180 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/posts目录下找到8个代码文件. 181 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/core/migrations目录下找到3个代码文件. 182 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/core目录下找到7个代码文件. 183 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/articles/migrations目录下找到3个代码文件. 184 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/articles/templates/articles目录下找到0个代码文件. 185 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/articles/templates目录下找到0个代码文件. 186 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/articles目录下找到10个代码文件. 187 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/static/css目录下找到0个代码文件. 188 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/static/js目录下找到0个代码文件. 189 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/static/img目录下找到0个代码文件. 190 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/static目录下找到0个代码文件. 191 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project/templates目录下找到0个代码文件. 192 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian/example_project目录下找到29个代码文件. 193 | DEBUG:swcr.swcr:在/Users/dev/Temp/django-guardian目录下找到94个代码文件. 194 | ``` 195 | -------------------------------------------------------------------------------- /swcr/swcr.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import pkg_resources 4 | from os.path import abspath 5 | try: 6 | from os import scandir 7 | except ImportError: 8 | from scandir import scandir 9 | 10 | import click 11 | from docx import Document 12 | from docx.shared import Pt 13 | from docx.enum.text import WD_PARAGRAPH_ALIGNMENT 14 | 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | # 默认源代码文件目录 20 | DEFAULT_INDIRS = ['.'] 21 | # 默认支持的代码格式 22 | DEFAULT_EXTS = ['py'] 23 | # 默认的注释前缀 24 | DEFAULT_COMMENT_CHARS = ( 25 | '#', '//' 26 | ) 27 | 28 | 29 | def del_slash(dirs): 30 | """ 31 | 删除文件夹最后一位的/ 32 | 33 | Args: 34 | dirs: 文件夹列表 35 | Returns: 36 | 删除之后的文件夹 37 | """ 38 | no_slash_dirs = [] 39 | for dir_ in dirs: 40 | if dir_[-1] == '/': 41 | no_slash_dirs.append(dir_[: -1]) 42 | else: 43 | no_slash_dirs.append(dir_) 44 | return no_slash_dirs 45 | 46 | 47 | class CodeFinder(object): 48 | """ 49 | 给定一个目录,和若干个后缀名, 50 | 递归地遍历该目录,找到该目录下 51 | 所有以这些后缀结束的文件 52 | """ 53 | def __init__(self, exts=None): 54 | """ 55 | Args: 56 | exts: 后缀名,默认为以py结尾 57 | """ 58 | self.exts = exts if exts else ['py'] 59 | 60 | def is_code(self, file): 61 | for ext in self.exts: 62 | if file.endswith(ext): 63 | return True 64 | return False 65 | 66 | @staticmethod 67 | def is_hidden_file(file): 68 | """ 69 | 是否是隐藏文件 70 | """ 71 | return file[0] == '.' 72 | 73 | @staticmethod 74 | def should_be_excluded(file, excludes=None): 75 | """ 76 | 是否需要略过此文件 77 | """ 78 | if not excludes: 79 | return False 80 | if not isinstance(excludes, list): 81 | excludes = [excludes] 82 | should_be_excluded = False 83 | for exclude in excludes: 84 | if file.startswith(exclude): 85 | should_be_excluded = True 86 | break 87 | return should_be_excluded 88 | 89 | def find(self, indir, excludes=None): 90 | """ 91 | 给定一个文件夹查找这个文件夹下所有的代码 92 | 93 | Args: 94 | indir: 需要查到代码的目录 95 | excludes: 排除文件或目录 96 | Returns: 97 | 代码文件列表 98 | """ 99 | files = [] 100 | for entry in scandir(indir): 101 | # 防止根目录有一些含有非常多文件的隐藏文件夹 102 | # 例如,.git文件,如果不排除,此程序很难运行 103 | entry_name = entry.name 104 | entry_path = abspath(entry.path) 105 | if self.is_hidden_file(entry_name): 106 | continue 107 | if self.should_be_excluded(entry_path, excludes): 108 | continue 109 | if entry.is_file(): 110 | if self.is_code(entry_name): 111 | files.append(entry_path) 112 | continue 113 | for file in self.find(entry_path, excludes=excludes): 114 | files.append(file) 115 | logger.debug('在%s目录下找到%d个代码文件.', indir, len(files)) 116 | return files 117 | 118 | 119 | class CodeWriter(object): 120 | def __init__( 121 | self, font_name='宋体', 122 | font_size=10.5, space_before=0.0, 123 | space_after=2.3, line_spacing=10.5, 124 | command_chars=None, document=None 125 | ): 126 | self.font_name = font_name 127 | self.font_size = font_size 128 | self.space_before = space_before 129 | self.space_after = space_after 130 | self.line_spacing = line_spacing 131 | self.command_chars = command_chars if command_chars else DEFAULT_COMMENT_CHARS 132 | self.document = Document(pkg_resources.resource_filename( 133 | 'swcr', 'template.docx' 134 | )) if not document else document 135 | 136 | @staticmethod 137 | def is_blank_line(line): 138 | """ 139 | 判断是否是空行 140 | """ 141 | return not bool(line) 142 | 143 | def is_comment_line(self, line): 144 | line = line.lstrip() # 去除左侧缩进 145 | is_comment = False 146 | for comment_char in self.command_chars: 147 | if line.startswith(comment_char): 148 | is_comment = True 149 | break 150 | return is_comment 151 | 152 | def write_header(self, title): 153 | """ 154 | 写入页眉 155 | """ 156 | paragraph = self.document.sections[0].header.paragraphs[0] 157 | paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER 158 | run = paragraph.add_run(title) 159 | run.font.name = self.font_name 160 | run.font.size = Pt(self.font_size) 161 | return self 162 | 163 | def write_file(self, file): 164 | """ 165 | 把单个文件添加到程序文档里面 166 | """ 167 | with open(file) as fp: 168 | for line in fp: 169 | line = line.rstrip() 170 | if self.is_blank_line(line): 171 | continue 172 | if self.is_comment_line(line): 173 | continue 174 | paragraph = self.document.add_paragraph() 175 | paragraph.paragraph_format.space_before = Pt(self.space_before) 176 | paragraph.paragraph_format.space_after = Pt(self.space_after) 177 | paragraph.paragraph_format.line_spacing = Pt(self.line_spacing) 178 | run = paragraph.add_run(line) 179 | run.font.name = self.font_name 180 | run.font.size = Pt(self.font_size) 181 | return self 182 | 183 | def save(self, file): 184 | self.document.save(file) 185 | 186 | 187 | @click.command(name='swcr') 188 | @click.option( 189 | '-t', '--title', default='软件著作权程序鉴别材料生成器V1.0', 190 | help='软件名称+版本号,默认为软件著作权程序鉴别材料生成器V1.0,此名称用于生成页眉' 191 | ) 192 | @click.option( 193 | '-i', '--indir', 'indirs', 194 | multiple=True, type=click.Path(exists=True), 195 | help='源码所在文件夹,可以指定多个,默认为当前目录' 196 | ) 197 | @click.option( 198 | '-e', '--ext', 'exts', 199 | multiple=True, help='源代码后缀,可以指定多个,默认为Python源代码' 200 | ) 201 | @click.option( 202 | '-c', '--comment-char', 'comment_chars', 203 | multiple=True, help='注释字符串,可以指定多个,默认为#、//' 204 | ) 205 | @click.option( 206 | '--font-name', default='宋体', 207 | help='字体,默认为宋体' 208 | ) 209 | @click.option( 210 | '--font-size', default=10.5, 211 | type=click.FloatRange(min=1.0), 212 | help='字号,默认为五号,即10.5号' 213 | ) 214 | @click.option( 215 | '--space-before', default=0.0, 216 | type=click.FloatRange(min=0.0), 217 | help='段前间距,默认为0' 218 | ) 219 | @click.option( 220 | '--space-after', default=2.3, 221 | type=click.FloatRange(min=0.0), 222 | help='段后间距,默认为2.3' 223 | ) 224 | @click.option( 225 | '--line-spacing', default=10.5, 226 | type=click.FloatRange(min=0.0), 227 | help='行距,默认为固定值10.5' 228 | ) 229 | @click.option( 230 | '--exclude', 'excludes', 231 | multiple=True, type=click.Path(exists=True), 232 | help='需要排除的文件或路径,可以指定多个' 233 | ) 234 | @click.option( 235 | '-o', '--outfile', default='code.docx', 236 | type=click.Path(exists=False), 237 | help='输出文件(docx格式),默认为当前目录的code.docx' 238 | ) 239 | @click.option('-v', '--verbose', is_flag=True, help='打印调试信息') 240 | def main( 241 | title, indirs, exts, 242 | comment_chars, font_name, 243 | font_size, space_before, 244 | space_after, line_spacing, 245 | excludes, outfile, verbose 246 | ): 247 | if not indirs: 248 | indirs = DEFAULT_INDIRS 249 | if not exts: 250 | exts = DEFAULT_EXTS 251 | if not comment_chars: 252 | comment_chars = DEFAULT_COMMENT_CHARS 253 | if verbose: 254 | logging.basicConfig(level=logging.DEBUG) 255 | 256 | # 第零步,把所有的路径都转换为绝对路径 257 | indirs = [abspath(indir) for indir in indirs] 258 | excludes = del_slash( 259 | [abspath(exclude) for exclude in excludes] if excludes else [] 260 | ) 261 | 262 | # 第一步,查找代码文件 263 | finder = CodeFinder(exts) 264 | files = [file for indir in indirs for file in finder.find( 265 | indir, excludes=excludes 266 | )] 267 | 268 | # 第二步,逐个把代码文件写入到docx中 269 | writer = CodeWriter( 270 | command_chars=comment_chars, 271 | font_name=font_name, 272 | font_size=font_size, 273 | space_before=space_before, 274 | space_after=space_after, 275 | line_spacing=line_spacing 276 | ) 277 | writer.write_header(title) 278 | for file in files: 279 | writer.write_file(file) 280 | writer.save(outfile) 281 | return 0 282 | 283 | 284 | if __name__ == '__main__': 285 | main() 286 | --------------------------------------------------------------------------------